sunspot 2.1.1 → 2.2.0

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 (47) hide show
  1. data/lib/sunspot.rb +13 -9
  2. data/lib/sunspot/dsl.rb +4 -3
  3. data/lib/sunspot/dsl/fields.rb +11 -16
  4. data/lib/sunspot/dsl/paginatable.rb +4 -1
  5. data/lib/sunspot/dsl/spellcheckable.rb +14 -0
  6. data/lib/sunspot/dsl/standard_query.rb +63 -35
  7. data/lib/sunspot/field.rb +54 -8
  8. data/lib/sunspot/field_factory.rb +2 -4
  9. data/lib/sunspot/indexer.rb +1 -2
  10. data/lib/sunspot/query.rb +2 -2
  11. data/lib/sunspot/query/abstract_fulltext.rb +69 -0
  12. data/lib/sunspot/query/common_query.rb +13 -2
  13. data/lib/sunspot/query/composite_fulltext.rb +58 -8
  14. data/lib/sunspot/query/dismax.rb +14 -67
  15. data/lib/sunspot/query/function_query.rb +1 -2
  16. data/lib/sunspot/query/geo.rb +1 -1
  17. data/lib/sunspot/query/join.rb +90 -0
  18. data/lib/sunspot/query/pagination.rb +12 -4
  19. data/lib/sunspot/query/restriction.rb +3 -4
  20. data/lib/sunspot/query/sort.rb +6 -0
  21. data/lib/sunspot/query/sort_composite.rb +7 -0
  22. data/lib/sunspot/query/spellcheck.rb +19 -0
  23. data/lib/sunspot/query/standard_query.rb +24 -2
  24. data/lib/sunspot/query/text_field_boost.rb +1 -3
  25. data/lib/sunspot/search/abstract_search.rb +10 -1
  26. data/lib/sunspot/search/cursor_paginated_collection.rb +32 -0
  27. data/lib/sunspot/search/paginated_collection.rb +1 -0
  28. data/lib/sunspot/search/standard_search.rb +71 -3
  29. data/lib/sunspot/session.rb +6 -6
  30. data/lib/sunspot/setup.rb +6 -1
  31. data/lib/sunspot/util.rb +46 -13
  32. data/lib/sunspot/version.rb +1 -1
  33. data/spec/api/query/fulltext_examples.rb +150 -1
  34. data/spec/api/query/geo_examples.rb +2 -6
  35. data/spec/api/query/join_spec.rb +3 -3
  36. data/spec/api/query/ordering_pagination_examples.rb +14 -0
  37. data/spec/api/query/spellcheck_examples.rb +20 -0
  38. data/spec/api/query/standard_spec.rb +1 -0
  39. data/spec/api/search/cursor_paginated_collection_spec.rb +35 -0
  40. data/spec/api/search/paginated_collection_spec.rb +1 -0
  41. data/spec/api/session_spec.rb +36 -2
  42. data/spec/integration/spellcheck_spec.rb +74 -0
  43. data/spec/mocks/connection.rb +5 -3
  44. data/spec/mocks/photo.rb +12 -4
  45. data/spec/spec_helper.rb +4 -0
  46. metadata +24 -5
  47. checksums.yaml +0 -7
@@ -38,7 +38,6 @@ module Sunspot
38
38
  #
39
39
  class Base #:nodoc:
40
40
  include Filter
41
- include RSolr::Char
42
41
 
43
42
  RESERVED_WORDS = Set['AND', 'OR', 'NOT']
44
43
 
@@ -92,7 +91,7 @@ module Sunspot
92
91
  # String:: Boolean phrase for restriction in the positive
93
92
  #
94
93
  def to_positive_boolean_phrase
95
- "#{escape(@field.indexed_name)}:#{to_solr_conditional}"
94
+ "#{Util.escape(@field.indexed_name)}:#{to_solr_conditional}"
96
95
  end
97
96
 
98
97
  #
@@ -138,7 +137,7 @@ module Sunspot
138
137
  # String:: Solr API representation of given value
139
138
  #
140
139
  def solr_value(value = @value)
141
- solr_value = escape(@field.to_indexed(value))
140
+ solr_value = Util.escape(@field.to_indexed(value))
142
141
  if RESERVED_WORDS.include?(solr_value)
143
142
  %Q("#{solr_value}")
144
143
  else
@@ -168,7 +167,7 @@ module Sunspot
168
167
  unless @value.nil?
169
168
  super
170
169
  else
171
- "#{escape(@field.indexed_name)}:[* TO *]"
170
+ "#{Util.escape(@field.indexed_name)}:[* TO *]"
172
171
  end
173
172
  end
174
173
 
@@ -115,6 +115,12 @@ module Sunspot
115
115
  end
116
116
  end
117
117
 
118
+ class SolrIdSort < Abstract
119
+ def to_param
120
+ "id #{direction_for_solr}"
121
+ end
122
+ end
123
+
118
124
  #
119
125
  # A FunctionSort sorts by solr function.
120
126
  # FunctionComp recursively parses arguments for nesting
@@ -18,6 +18,13 @@ module Sunspot
18
18
  @sorts << sort
19
19
  end
20
20
 
21
+ #
22
+ # Check sort presence
23
+ #
24
+ def include?(sort)
25
+ @sorts.any? { |s| s.to_param.include?(sort) }
26
+ end
27
+
21
28
  #
22
29
  # Combine the sorts into a single param by joining them
23
30
  #
@@ -0,0 +1,19 @@
1
+ module Sunspot
2
+ module Query
3
+ class Spellcheck < Connective::Conjunction
4
+ attr_accessor :options
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ end
9
+
10
+ def to_params
11
+ options = {}
12
+ @options.each do |key, val|
13
+ options["spellcheck." + Sunspot::Util.method_case(key.to_s)] = val
14
+ end
15
+ { :spellcheck => true }.merge(options)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,11 +5,33 @@ module Sunspot
5
5
 
6
6
  def initialize(types)
7
7
  super
8
- @components << @fulltext = CompositeFulltext.new
8
+ @components << @fulltext = Conjunction.new
9
9
  end
10
10
 
11
11
  def add_fulltext(keywords)
12
- @fulltext.add(keywords)
12
+ @fulltext.add_fulltext(keywords)
13
+ end
14
+
15
+ def add_join(keywords, target, from, to)
16
+ @fulltext.add_join(keywords, target, from, to)
17
+ end
18
+
19
+ def disjunction
20
+ parent_fulltext = @fulltext
21
+ @fulltext = @fulltext.add_disjunction
22
+
23
+ yield
24
+ ensure
25
+ @fulltext = parent_fulltext
26
+ end
27
+
28
+ def conjunction
29
+ parent_fulltext = @fulltext
30
+ @fulltext = @fulltext.add_conjunction
31
+
32
+ yield
33
+ ensure
34
+ @fulltext = parent_fulltext
13
35
  end
14
36
  end
15
37
  end
@@ -8,9 +8,7 @@ module Sunspot
8
8
  end
9
9
 
10
10
  def to_boosted_field
11
- boosted_field = @field.indexed_name
12
- boosted_field.concat("^#{@boost}") if @boost
13
- boosted_field
11
+ @boost ? "#{@field.indexed_name}^#{@boost}" : @field.indexed_name
14
12
  end
15
13
  end
16
14
  end
@@ -1,4 +1,5 @@
1
1
  require 'sunspot/search/paginated_collection'
2
+ require 'sunspot/search/cursor_paginated_collection'
2
3
  require 'sunspot/search/hit_enumerable'
3
4
 
4
5
  module Sunspot
@@ -276,12 +277,20 @@ module Sunspot
276
277
  solr_response['docs']
277
278
  end
278
279
 
280
+ def next_cursor
281
+ @solr_result['nextCursorMark'] if @query.cursor
282
+ end
283
+
279
284
  def verified_hits
280
285
  @verified_hits ||= paginate_collection(super)
281
286
  end
282
287
 
283
288
  def paginate_collection(collection)
284
- PaginatedCollection.new(collection, @query.page, @query.per_page, total)
289
+ if @query.cursor
290
+ CursorPaginatedCollection.new(collection, @query.per_page, total, @query.cursor, next_cursor)
291
+ else
292
+ PaginatedCollection.new(collection, @query.page, @query.per_page, total)
293
+ end
285
294
  end
286
295
 
287
296
  def add_facet(name, facet)
@@ -0,0 +1,32 @@
1
+ module Sunspot
2
+ module Search
3
+
4
+ class CursorPaginatedCollection < Array
5
+ attr_reader :per_page, :total_count, :current_cursor, :next_page_cursor
6
+ alias :total_entries :total_count
7
+ alias :limit_value :per_page
8
+
9
+ def initialize(collection, per_page, total, current_cursor, next_page_cursor)
10
+ @per_page = per_page
11
+ @total_count = total
12
+ @current_cursor = current_cursor
13
+ @next_page_cursor = next_page_cursor
14
+
15
+ replace collection
16
+ end
17
+
18
+ def total_pages
19
+ (total_count.to_f / per_page).ceil
20
+ end
21
+ alias :num_pages :total_pages
22
+
23
+ def first_page?
24
+ current_cursor == '*'
25
+ end
26
+
27
+ def last_page?
28
+ count < per_page
29
+ end
30
+ end
31
+ end
32
+ end
@@ -32,6 +32,7 @@ module Sunspot
32
32
  def previous_page
33
33
  current_page > 1 ? (current_page - 1) : nil
34
34
  end
35
+ alias :prev_page :previous_page
35
36
 
36
37
  def next_page
37
38
  current_page < total_pages ? (current_page + 1) : nil
@@ -1,6 +1,6 @@
1
1
  module Sunspot
2
2
  module Search
3
- #
3
+ #
4
4
  # This class encapsulates the results of a Solr search. It provides access
5
5
  # to search results, total result count, facets, and pagination information.
6
6
  # Instances of Search are returned by the Sunspot.search and
@@ -10,9 +10,77 @@ module Sunspot
10
10
  def request_handler
11
11
  super || :select
12
12
  end
13
-
13
+
14
+ # Return the raw spellcheck block from the Solr response
15
+ def solr_spellcheck
16
+ @solr_spellcheck ||= @solr_result['spellcheck'] || {}
17
+ end
18
+
19
+ # Reformat the oddly-formatted spellcheck suggestion array into a
20
+ # more useful hash.
21
+ #
22
+ # Original: [term, suggestion, term, suggestion, ..., "correctlySpelled", bool, "collation", str]
23
+ # "collation" is only included if spellcheck.collation was set to true
24
+ # Returns: { term => suggestion, term => suggestion }
25
+ def spellcheck_suggestions
26
+ unless defined?(@spellcheck_suggestions)
27
+ @spellcheck_suggestions = {}
28
+ count = ((solr_spellcheck['suggestions'] || []).length) / 2
29
+ (0..(count - 1)).each do |i|
30
+ break if ["correctlySpelled", "collation"].include? solr_spellcheck[i]
31
+ term = solr_spellcheck['suggestions'][i * 2]
32
+ suggestion = solr_spellcheck['suggestions'][(i * 2) + 1]
33
+ @spellcheck_suggestions[term] = suggestion
34
+ end
35
+ end
36
+ @spellcheck_suggestions
37
+ end
38
+
39
+ # Return the suggestion with the single highest frequency.
40
+ # Requires the extended results format.
41
+ def spellcheck_suggestion_for(term)
42
+ spellcheck_suggestions[term]['suggestion'].sort_by do |suggestion|
43
+ suggestion['freq']
44
+ end.last['word']
45
+ end
46
+
47
+ # Provide a collated query. If the user provides a query string,
48
+ # tokenize it on whitespace and replace terms strictly not present in
49
+ # the index. Otherwise return Solr's suggested collation.
50
+ #
51
+ # Solr's suggested collation is more liberal, replacing even terms that
52
+ # are present in the index. This may not be useful if only one term is
53
+ # misspelled and preventing useful results.
54
+ #
55
+ # Mix and match in your views for a blend of strict and liberal collations.
56
+ def spellcheck_collation(*terms)
57
+ if solr_spellcheck['suggestions'] && solr_spellcheck['suggestions'].length > 2
58
+ collation = terms.join(" ").dup if terms
59
+
60
+ # If we are given a query string, tokenize it and strictly replace
61
+ # the terms that aren't present in the index
62
+ if terms.length > 0
63
+ terms.each do |term|
64
+ if (spellcheck_suggestions[term]||{})['origFreq'] == 0
65
+ collation[term] = spellcheck_suggestion_for(term)
66
+ end
67
+ end
68
+ end
69
+
70
+ # If no query was given, or all terms are present in the index,
71
+ # return Solr's suggested collation.
72
+ if terms.length == 0
73
+ collation = solr_spellcheck['suggestions'][-1]
74
+ end
75
+
76
+ collation
77
+ else
78
+ nil
79
+ end
80
+ end
81
+
14
82
  private
15
-
83
+
16
84
  def dsl
17
85
  DSL::Search.new(self, @setup)
18
86
  end
@@ -102,9 +102,9 @@ module Sunspot
102
102
  #
103
103
  # See Sunspot.commit
104
104
  #
105
- def commit
105
+ def commit(soft_commit = false)
106
106
  @adds = @deletes = 0
107
- connection.commit
107
+ connection.commit :commit_attributes => {:softCommit => soft_commit}
108
108
  end
109
109
 
110
110
  #
@@ -200,8 +200,8 @@ module Sunspot
200
200
  #
201
201
  # See Sunspot.commit_if_dirty
202
202
  #
203
- def commit_if_dirty
204
- commit if dirty?
203
+ def commit_if_dirty(soft_commit = false)
204
+ commit soft_commit if dirty?
205
205
  end
206
206
 
207
207
  #
@@ -214,8 +214,8 @@ module Sunspot
214
214
  #
215
215
  # See Sunspot.commit_if_delete_dirty
216
216
  #
217
- def commit_if_delete_dirty
218
- commit if delete_dirty?
217
+ def commit_if_delete_dirty(soft_commit = false)
218
+ commit soft_commit if delete_dirty?
219
219
  end
220
220
 
221
221
  #
@@ -41,7 +41,12 @@ module Sunspot
41
41
  def add_join_field_factory(name, type, options = {}, &block)
42
42
  field_factory = FieldFactory::Join.new(name, type, options, &block)
43
43
  @field_factories[field_factory.signature] = field_factory
44
- @field_factories_cache[field_factory.name] = field_factory
44
+
45
+ if type.is_a?(Type::TextType)
46
+ @text_field_factories_cache[field_factory.name] = field_factory
47
+ else
48
+ @field_factories_cache[field_factory.name] = field_factory
49
+ end
45
50
  end
46
51
 
47
52
  #
@@ -1,14 +1,14 @@
1
1
  module Sunspot
2
- #
2
+ #
3
3
  # The Sunspot::Util module provides utility methods used elsewhere in the
4
4
  # library.
5
5
  #
6
6
  module Util #:nodoc:
7
7
  class <<self
8
- #
8
+ #
9
9
  # Get all of the superclasses for a given class, including the class
10
10
  # itself.
11
- #
11
+ #
12
12
  # ==== Parameters
13
13
  #
14
14
  # clazz<Class>:: class for which to get superclasses
@@ -23,7 +23,7 @@ module Sunspot
23
23
  superclasses
24
24
  end
25
25
 
26
- #
26
+ #
27
27
  # Convert a string to snake case
28
28
  #
29
29
  # ==== Parameters
@@ -38,7 +38,7 @@ module Sunspot
38
38
  string.scan(/(^|[A-Z])([^A-Z]+)/).map! { |word| word.join.downcase }.join('_')
39
39
  end
40
40
 
41
- #
41
+ #
42
42
  # Convert a string to camel case
43
43
  #
44
44
  # ==== Parameters
@@ -53,7 +53,23 @@ module Sunspot
53
53
  string.split('_').map! { |word| word.capitalize }.join
54
54
  end
55
55
 
56
- #
56
+ #
57
+ # Convert snake case string to method case (Java style)
58
+ #
59
+ # ==== Parameters
60
+ #
61
+ # string<String>:: String to convert to method case
62
+ #
63
+ # ==== Returns
64
+ #
65
+ # String:: String in method case
66
+ #
67
+ def method_case(string)
68
+ first = true
69
+ string.split('_').map! { |word| word = first ? word : word.capitalize; first = false; word }.join
70
+ end
71
+
72
+ #
57
73
  # Get a constant from a fully qualified name
58
74
  #
59
75
  # ==== Parameters
@@ -70,13 +86,13 @@ module Sunspot
70
86
  end
71
87
  end
72
88
 
73
- #
89
+ #
74
90
  # Evaluate the given proc in the context of the given object if the
75
91
  # block's arity is non-positive, or by passing the given object as an
76
92
  # argument if it is negative.
77
- #
93
+ #
78
94
  # ==== Parameters
79
- #
95
+ #
80
96
  # object<Object>:: Object to pass to the proc
81
97
  #
82
98
  def instance_eval_or_call(object, &block)
@@ -108,7 +124,7 @@ module Sunspot
108
124
  end
109
125
  end
110
126
 
111
- #
127
+ #
112
128
  # When generating boosts, Solr requires that the values be in standard
113
129
  # (not scientific) notation. We would like to ensure a minimum number of
114
130
  # significant digits (i.e., digits that are not prefix zeros) for small
@@ -122,7 +138,7 @@ module Sunspot
122
138
  end
123
139
  end
124
140
 
125
- #
141
+ #
126
142
  # Perform a deep merge of hashes, returning the result as a new hash.
127
143
  # See #deep_merge_into for rules used to merge the hashes
128
144
  #
@@ -139,7 +155,7 @@ module Sunspot
139
155
  deep_merge_into({}, left, right)
140
156
  end
141
157
 
142
- #
158
+ #
143
159
  # Perform a deep merge of the right hash into the left hash
144
160
  #
145
161
  # ==== Parameters
@@ -155,9 +171,26 @@ module Sunspot
155
171
  deep_merge_into(left, left, right)
156
172
  end
157
173
 
174
+ #
175
+ # Escapes characters for the Solr query parser
176
+ #
177
+ # ==== Parameters
178
+ #
179
+ # string<String>:: String to escape
180
+ #
181
+ # ==== Returns
182
+ #
183
+ # String:: escaped string
184
+ #
185
+ def escape(value)
186
+ # RSolr.solr_escape doesn't handle spaces or period chars,
187
+ # which do need to be escaped
188
+ RSolr.solr_escape(value).gsub(/([\s\.])/, '\\\\\1')
189
+ end
190
+
158
191
  private
159
192
 
160
- #
193
+ #
161
194
  # Deep merge two hashes into a third hash, using rules that produce nice
162
195
  # merged parameter hashes. The rules are as follows, for a given key:
163
196
  #