sunspot 2.1.1 → 2.2.0

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