sunspot_suggest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+