sunspot_suggest 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +32 -0
  5. data/Rakefile +1 -0
  6. data/lib/sunspot_suggest/plugin.rb +20 -0
  7. data/lib/sunspot_suggest/sunspot/dsl/standard_query.rb +15 -0
  8. data/lib/sunspot_suggest/sunspot/query/common_query.rb +16 -0
  9. data/lib/sunspot_suggest/sunspot/query/spellcheck.rb +22 -0
  10. data/lib/sunspot_suggest/sunspot/query/suggest.rb +28 -0
  11. data/lib/sunspot_suggest/sunspot/search/abstract_search.rb +130 -0
  12. data/lib/sunspot_suggest/version.rb +3 -0
  13. data/lib/sunspot_suggest.rb +6 -0
  14. data/spec/api/query/spellcheck_spec.rb +21 -0
  15. data/spec/api/search/spellcheck_search_spec.rb +116 -0
  16. data/spec/article.rb +12 -0
  17. data/spec/helpers/indexer_helper.rb +17 -0
  18. data/spec/helpers/integration_helper.rb +8 -0
  19. data/spec/helpers/mock_session_helper.rb +13 -0
  20. data/spec/helpers/query_helper.rb +26 -0
  21. data/spec/helpers/search_helper.rb +68 -0
  22. data/spec/helpers/spellcheck_helper.rb +66 -0
  23. data/spec/mocks/adapters.rb +36 -0
  24. data/spec/mocks/blog.rb +3 -0
  25. data/spec/mocks/comment.rb +21 -0
  26. data/spec/mocks/connection.rb +128 -0
  27. data/spec/mocks/mock_adapter.rb +30 -0
  28. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  29. data/spec/mocks/mock_record.rb +52 -0
  30. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  31. data/spec/mocks/photo.rb +11 -0
  32. data/spec/mocks/post.rb +86 -0
  33. data/spec/mocks/super_class.rb +2 -0
  34. data/spec/mocks/user.rb +13 -0
  35. data/spec/spec_helper.rb +50 -0
  36. data/sunspot_suggest.gemspec +26 -0
  37. metadata +184 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sunspot_hierarchical_facets.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 rainkinz
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # SunspotSpellcheck
2
+
3
+ Adds spellchecking to sunspot
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'sunspot_suggest'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install sunspot_suggest
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ * Need jar added to solr installation, schema config etc
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create new Pull Request
32
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ require 'sunspot_suggest/sunspot/query/suggest'
2
+ require 'sunspot_suggest/sunspot/query/spellcheck'
3
+ require 'sunspot_suggest/sunspot/query/common_query'
4
+
5
+ require 'sunspot_suggest/sunspot/search/abstract_search'
6
+ require 'sunspot_suggest/sunspot/dsl/standard_query'
7
+
8
+ module Sunspot
9
+ module Util
10
+ class<<self
11
+ def method_case(string_or_symbol)
12
+ string = string_or_symbol.to_s
13
+ first = true
14
+ string.split('_').map! { |word| word = first ? word : word.capitalize; first = false; word }.join
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+
@@ -0,0 +1,15 @@
1
+ module Sunspot
2
+ module DSL #:nodoc:
3
+
4
+ class StandardQuery
5
+ def suggest(options = {})
6
+ @query.suggest(options)
7
+ end
8
+
9
+ def spellcheck(options = {})
10
+ @query.spellcheck(options)
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Sunspot
2
+ module Query
3
+
4
+ class CommonQuery
5
+
6
+ def suggest(options = {})
7
+ @components << Suggest.new(options)
8
+ end
9
+
10
+ def spellcheck(options = {})
11
+ @components << Spellcheck.new(options)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Sunspot
2
+ module Query
3
+
4
+ class Spellcheck < Connective::Conjunction
5
+ attr_accessor :options
6
+
7
+ def initialize(options = {})
8
+ @options = options
9
+ end
10
+
11
+ def to_params
12
+ options = {}
13
+ @options.each do |key, val|
14
+ options["spellcheck." + Sunspot::Util.method_case(key)] = val
15
+ end
16
+ { :spellcheck => true }.merge(options)
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,28 @@
1
+ module Sunspot
2
+ module Query
3
+
4
+ class Suggest < Connective::Conjunction
5
+ attr_accessor :options
6
+
7
+ def initialize(opts = {})
8
+ @options = opts
9
+ end
10
+
11
+ def to_params
12
+ opts = {}
13
+ @options.each do |key, val|
14
+ opts['spellcheck.' + Sunspot::Util.method_case(key)] = val
15
+ end
16
+
17
+ {
18
+ :spellcheck => true,
19
+ :rows => 0,
20
+ 'spellcheck.count' => 10,
21
+ }.merge(opts)
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,130 @@
1
+ module Sunspot
2
+
3
+ module Search
4
+
5
+ class Spellcheck
6
+
7
+ # TODO: Firm up the names of these a bit
8
+ class Collation
9
+ attr_reader :query, :suggestions, :hits
10
+
11
+ def initialize(args = {})
12
+ @query = args['collationQuery']
13
+ @hits = args['hits']
14
+ @suggestions = parse_corrections(args['misspellingsAndCorrections'])
15
+ end
16
+
17
+ private
18
+
19
+ def parse_corrections(raw)
20
+ suggestions = []
21
+ raw.each_slice(2) do |k, v|
22
+ suggestions << Suggestions.new(k, [Suggestion.new(v)])
23
+ end
24
+ suggestions
25
+ end
26
+ end
27
+
28
+ # TODO: Rename this, make it enumerable?
29
+ class Suggestions
30
+ attr_reader :misspelling, :suggestions
31
+
32
+ def initialize(misspelling, suggestions)
33
+ @misspelling = misspelling
34
+ # TODO: FIXME
35
+ @suggestions = parse_suggestions(suggestions)
36
+ end
37
+
38
+ private
39
+
40
+ # TODO: FIXME
41
+ def parse_suggestions(suggestions)
42
+ []
43
+ end
44
+ end
45
+
46
+ class Suggestion
47
+ attr_reader :word
48
+
49
+ def initialize(word, freq = -1)
50
+ @word = word
51
+ @freq = freq
52
+ end
53
+
54
+ def to_s
55
+ @word
56
+ end
57
+ end
58
+
59
+ attr_reader :suggestions, :collations
60
+ attr_writer :correctly_spelled
61
+
62
+ def initialize
63
+ @suggestions = []
64
+ @collations = []
65
+ end
66
+
67
+ def correctly_spelled?
68
+ @correctly_spelled
69
+ end
70
+
71
+ def add_collation(collation_values)
72
+ if collation_values.is_a?(Array)
73
+ @collations << Collation.new(Hash[*collation_values])
74
+ else
75
+ raise ArgumentError, "Don't know how to handle collation: #{collation_values}"
76
+ end
77
+ end
78
+
79
+ def add_suggestion(word, suggest_values)
80
+ @suggestions << Suggestions.new(word, suggest_values)
81
+ end
82
+
83
+ end
84
+
85
+ class AbstractSearch
86
+
87
+ attr_accessor :solr_result
88
+
89
+
90
+ def suggested
91
+ raw = raw_suggestions
92
+ return nil unless raw.is_a?(Array)
93
+
94
+ s = SuggestedResult.new
95
+ Hash[*raw].each do |k, v|
96
+ if k == 'correctlySpelled'
97
+ s.correctly_spelled = v
98
+ else
99
+ s.query = k
100
+ s.suggestions = v['suggestion']
101
+ end
102
+ end
103
+ s
104
+ end
105
+
106
+
107
+ def spellcheck
108
+ spellcheck = Spellcheck.new
109
+
110
+ raw_suggestions.each_slice(2) do |k, v|
111
+ if k == 'collation'
112
+ spellcheck.add_collation(v)
113
+ elsif k == 'correctlySpelled'
114
+ spellcheck.correctly_spelled = (v == 'true')
115
+ else
116
+ spellcheck.add_suggestion(k, v)
117
+ end
118
+ end
119
+ spellcheck
120
+ end
121
+
122
+ private
123
+
124
+ def raw_suggestions
125
+ ["spellcheck", "suggestions"].inject(@solr_result) {|h, k| h && h[k]}
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module SunspotSuggest
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'sunspot_suggest/version'
2
+ require 'sunspot_suggest/plugin'
3
+
4
+ module SunspotSuggest
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'standard query', :type => :query do
4
+
5
+ it 'turns faceting on if facet is requested' do
6
+ search do
7
+ spellcheck
8
+ end
9
+
10
+ connection.should have_last_search_with(:spellcheck => true)
11
+ end
12
+
13
+ # TODO: Check other options
14
+
15
+ private
16
+
17
+ def search(*classes, &block)
18
+ classes[0] ||= Article
19
+ session.search(*classes, &block)
20
+ end
21
+ end
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+ # Check that the responses from solr are parsed correctly by
4
+ # sunspot
5
+ describe 'spellcheck', :type => :search do
6
+
7
+ context "spellcheck with extended results but no collations" do
8
+ before(:each) do
9
+ stub_spellcheck [
10
+ 'perform',{
11
+ 'numFound'=>3,
12
+ 'startOffset'=>0,
13
+ 'endOffset'=>7,
14
+ 'origFreq'=>4,
15
+ 'suggestion'=>[{
16
+ 'word'=>'performed',
17
+ 'freq'=>1},
18
+ {
19
+ 'word'=>'performance',
20
+ 'freq'=>3},
21
+ {
22
+ 'word'=>'inform',
23
+ 'freq'=>2}]},
24
+ 'hvac',{
25
+ 'numFound'=>1,
26
+ 'startOffset'=>8,
27
+ 'endOffset'=>12,
28
+ 'origFreq'=>4,
29
+ 'suggestion'=>[{
30
+ 'word'=>'has',
31
+ 'freq'=>1}]},
32
+ 'correctlySpelled',false
33
+ ]
34
+ end
35
+
36
+ it 'creates corrections' do
37
+ result = session.search Article do
38
+ fulltext 'perfrm hvc'
39
+ spellcheck
40
+ end
41
+
42
+ spellcheck = result.spellcheck
43
+ collations = spellcheck.collations
44
+ expect(collations.size).to eq(0)
45
+
46
+ corrections = spellcheck.suggestions
47
+ expect(corrections.size).to eq(2)
48
+ end
49
+
50
+ end
51
+
52
+ context "spellcheck with extended results and collations" do
53
+ before(:each) do
54
+ stub_spellcheck [
55
+ "perfrm",
56
+ {
57
+ "numFound"=>3,
58
+ "startOffset"=>14,
59
+ "endOffset"=>20,
60
+ "origFreq"=>0,
61
+ "suggestion"=>
62
+ [
63
+ {"word"=>"perform", "freq"=>4},
64
+ {"word"=>"performed", "freq"=>1},
65
+ {"word"=>"performance", "freq"=>3}
66
+ ]
67
+ },
68
+
69
+ "hvc",
70
+ {
71
+ "numFound"=>2,
72
+ "startOffset"=>21,
73
+ "endOffset"=>24,
74
+ "origFreq"=>0,
75
+ "suggestion"=>[
76
+ {"word"=>"hvac", "freq"=>4},
77
+ {"word"=>"have", "freq"=>5}
78
+ ]
79
+ },
80
+
81
+ "correctlySpelled", false,
82
+
83
+ "collation",
84
+ [
85
+ "collationQuery", "markup_texts:(perform hvac)",
86
+ "hits", 4,
87
+ "misspellingsAndCorrections", ["perfrm", "perform", "hvc", "hvac"]
88
+ ],
89
+
90
+ "collation",
91
+ [
92
+ "collationQuery", "markup_texts:(performed hvac)",
93
+ "hits", 4,
94
+ "misspellingsAndCorrections", ["perfrm", "performed", "hvc", "hvac"]
95
+ ]
96
+ ]
97
+
98
+
99
+ end
100
+
101
+ it 'parses the suggestions' do
102
+ result = session.search Article do
103
+ fulltext 'perfrm hvc'
104
+ spellcheck
105
+ end
106
+
107
+ spelling_suggestions = result.spellcheck
108
+ collations = spelling_suggestions.collations
109
+ expect(collations.size).to eq(2)
110
+ expect(collations.first.query).to eq('markup_texts:(perform hvac)')
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
data/spec/article.rb ADDED
@@ -0,0 +1,12 @@
1
+ class Article < SuperClass
2
+ attr_accessor :title, :body, :category
3
+ end
4
+
5
+ Sunspot.setup(Post) do
6
+ text :title
7
+ end
8
+
9
+ Sunspot.setup(Article) do
10
+ text :title
11
+ text :body
12
+ end
@@ -0,0 +1,17 @@
1
+ module IndexerHelper
2
+ def post(attrs = {})
3
+ @post ||= Post.new(attrs)
4
+ end
5
+
6
+ def last_add
7
+ @connection.adds.last
8
+ end
9
+
10
+ def value_in_last_document_for(field_name)
11
+ @connection.adds.last.last.field_by_name(field_name).value
12
+ end
13
+
14
+ def values_in_last_document_for(field_name)
15
+ @connection.adds.last.last.fields_by_name(field_name).map { |field| field.value }
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module IntegrationHelper
2
+ def self.included(base)
3
+ base.before(:all) do
4
+ Sunspot.config.solr.url = ENV['SOLR_URL'] || 'http://localhost:8983/solr/default'
5
+ Sunspot.reset!(true)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module MockSessionHelper
2
+ def config
3
+ @config ||= Sunspot::Configuration.build
4
+ end
5
+
6
+ def connection
7
+ @connection ||= Mock::Connection.new
8
+ end
9
+
10
+ def session
11
+ @session ||= Sunspot::Session.new(config, connection)
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module QueryHelper
2
+ def get_filter_tag(boolean_query)
3
+ connection.searches.last[:fq].each do |fq|
4
+ if match = fq.match(/^\{!tag=(.+)\}#{Regexp.escape(boolean_query)}$/)
5
+ return match[1]
6
+ end
7
+ end
8
+ nil
9
+ end
10
+
11
+ def subqueries(param)
12
+ q = connection.searches.last[:q]
13
+ subqueries = []
14
+ subqueries = q.scan(%r(_query_:"\{!dismax (.*?)\}(.*?)"))
15
+ subqueries.map do |subquery|
16
+ params = {}
17
+ subquery[0].scan(%r((\S+?)='(.+?)')) do |key, value|
18
+ params[key.to_sym] = value
19
+ end
20
+ unless subquery[1].empty?
21
+ params[:v] = subquery[1]
22
+ end
23
+ params
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,68 @@
1
+ module SearchHelper
2
+ def stub_nil_results
3
+ connection.response = { 'response' => nil }
4
+ end
5
+
6
+ def stub_full_results(*results)
7
+ count =
8
+ if results.last.is_a?(Integer) then results.pop
9
+ else results.length
10
+ end
11
+ docs = results.map do |result|
12
+ instance = result.delete('instance')
13
+ result.merge('id' => "#{instance.class.name} #{instance.id}")
14
+ end
15
+ response = {
16
+ 'response' => {
17
+ 'docs' => docs,
18
+ 'numFound' => count
19
+ }
20
+ }
21
+ connection.response = response
22
+ response
23
+ end
24
+
25
+ def stub_results(*results)
26
+ stub_full_results(
27
+ *results.map do |result|
28
+ if result.is_a?(Integer)
29
+ result
30
+ else
31
+ { 'instance' => result }
32
+ end
33
+ end
34
+ )
35
+ end
36
+
37
+ def stub_facet(name, values)
38
+ connection.response = {
39
+ 'facet_counts' => {
40
+ 'facet_fields' => {
41
+ name.to_s => values.to_a.sort_by { |value, count| -count }.flatten
42
+ }
43
+ }
44
+ }
45
+ end
46
+
47
+ def stub_date_facet(name, gap, values)
48
+ connection.response = {
49
+ 'facet_counts' => {
50
+ 'facet_dates' => {
51
+ name.to_s => { 'gap' => "+#{gap}SECONDS" }.merge(values)
52
+ }
53
+ }
54
+ }
55
+ end
56
+
57
+ def stub_query_facet(values)
58
+ connection.response = { 'facet_counts' => { 'facet_queries' => values } }
59
+ end
60
+
61
+ def facet_values(result, field_name)
62
+ result.facet(field_name).rows.map { |row| row.value }
63
+ end
64
+
65
+ def facet_counts(result, field_name)
66
+ result.facet(field_name).rows.map { |row| row.count }
67
+ end
68
+ end
@@ -0,0 +1,66 @@
1
+ module SpellcheckHelper
2
+
3
+ # TODO: Need a better way to build up suggestions
4
+ def stub_spellcheck(suggestions)
5
+ connection.response = {
6
+ 'spellcheck' => {
7
+ 'suggestions' => suggestions
8
+ }
9
+ }
10
+
11
+ if connection.response['responseHeader'].nil?
12
+ connection.response['responseHeader'] = {}
13
+ end
14
+
15
+
16
+ end
17
+
18
+ {"suggestions"=>
19
+ [
20
+ "perfrm",
21
+ {
22
+ "numFound"=>3,
23
+ "startOffset"=>14,
24
+ "endOffset"=>20,
25
+ "origFreq"=>0,
26
+ "suggestion"=>
27
+ [
28
+ {"word"=>"perform", "freq"=>4},
29
+ {"word"=>"performed", "freq"=>1},
30
+ {"word"=>"performance", "freq"=>3}
31
+ ]
32
+ },
33
+
34
+ "hvc",
35
+ {
36
+ "numFound"=>2,
37
+ "startOffset"=>21,
38
+ "endOffset"=>24,
39
+ "origFreq"=>0,
40
+ "suggestion"=>[
41
+ {"word"=>"hvac", "freq"=>4},
42
+ {"word"=>"have", "freq"=>5}
43
+ ]
44
+ },
45
+
46
+ "correctlySpelled", false,
47
+
48
+ "collation",
49
+ [
50
+ "collationQuery", "markup_texts:(perform hvac)",
51
+ "hits", 4,
52
+ "misspellingsAndCorrections", ["perfrm", "perform", "hvc", "hvac"]
53
+ ],
54
+
55
+ "collation",
56
+ [
57
+ "collationQuery", "markup_texts:(performed hvac)",
58
+ "hits", 4,
59
+ "misspellingsAndCorrections", ["perfrm", "performed", "hvc", "hvac"]
60
+ ]
61
+ ]
62
+ }
63
+
64
+
65
+ end
66
+