outoftime-sunspot 0.9.2 → 0.9.3

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.
data/TODO CHANGED
@@ -1,4 +1,9 @@
1
- === 0.9 ===
2
- * Query-based faceting (?)
1
+ === 0.9.X ===
2
+ * Deal with empty facet queries
3
+ * Passing an integer into the second argument of dynamic_facet() when multiple facets are requested gives the wrong value
3
4
  === 0.10 ===
5
+ * Highlighting
6
+ * LocalSolr
7
+ * Text field restrictions
8
+ * Prefixes
4
9
  * Intelligently decide whether to instantiate all facet rows at once
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 2
2
+ :patch: 3
3
3
  :major: 0
4
4
  :minor: 9
@@ -5,8 +5,8 @@ module Sunspot
5
5
  # Base class for connectives (conjunctions and disjunctions).
6
6
  #
7
7
  class Abstract < Scope
8
- def initialize(setup) #:nodoc:
9
- @setup = setup
8
+ def initialize(setup, negated = false) #:nodoc:
9
+ @setup, @negated = setup, negated
10
10
  @components = []
11
11
  end
12
12
 
@@ -21,7 +21,7 @@ module Sunspot
21
21
  # Express the connective as a Lucene boolean phrase.
22
22
  #
23
23
  def to_boolean_phrase #:nodoc:
24
- if @components.length == 1
24
+ phrase = if @components.length == 1
25
25
  @components.first.to_boolean_phrase
26
26
  else
27
27
  component_phrases = @components.map do |component|
@@ -29,6 +29,11 @@ module Sunspot
29
29
  end
30
30
  "(#{component_phrases.join(" #{connector} ")})"
31
31
  end
32
+ if negated?
33
+ "-#{phrase}"
34
+ else
35
+ phrase
36
+ end
32
37
  end
33
38
 
34
39
  #
@@ -38,12 +43,38 @@ module Sunspot
38
43
  def add_component(component) #:nodoc:
39
44
  @components << component
40
45
  end
46
+
47
+ def negated?
48
+ @negated
49
+ end
50
+
51
+ def negate
52
+ negated = self.class.new(@setup, !negated?)
53
+ for component in @components
54
+ negated.add_component(component)
55
+ end
56
+ negated
57
+ end
41
58
  end
42
59
 
43
60
  #
44
61
  # Disjunctions combine their components with an OR operator.
45
62
  #
46
63
  class Disjunction < Abstract
64
+ class <<self
65
+ def inverse
66
+ Conjunction
67
+ end
68
+ end
69
+
70
+ def to_boolean_phrase
71
+ if @components.any? { |component| component.negated? }
72
+ denormalize.to_boolean_phrase
73
+ else
74
+ super
75
+ end
76
+ end
77
+
47
78
  #
48
79
  # Add a conjunction to the disjunction. This overrides the method in
49
80
  # the Scope class since scopes are implicitly conjunctive and thus
@@ -55,17 +86,35 @@ module Sunspot
55
86
  conjunction
56
87
  end
57
88
 
89
+ def add_disjunction
90
+ self
91
+ end
92
+
58
93
  private
59
94
 
60
95
  def connector
61
96
  'OR'
62
97
  end
98
+
99
+ def denormalize
100
+ denormalized = self.class.inverse.new(@setup, !negated?)
101
+ for component in @components
102
+ denormalized.add_component(component.negate)
103
+ end
104
+ denormalized
105
+ end
63
106
  end
64
107
 
65
108
  #
66
109
  # Conjunctions combine their components with an AND operator.
67
110
  #
68
111
  class Conjunction < Abstract
112
+ class <<self
113
+ def inverse
114
+ Disjunction
115
+ end
116
+ end
117
+
69
118
  private
70
119
 
71
120
  def connector
@@ -26,17 +26,17 @@ module Sunspot
26
26
  # API for instances of this class.
27
27
  #
28
28
  # Implementations of this class must respond to #to_params and
29
- # #to_negative_params. Instead of implementing those methods, they may
29
+ # #to_negated_params. Instead of implementing those methods, they may
30
30
  # choose to implement any of:
31
31
  #
32
- # * #to_positive_boolean_phrase, and optionally #to_negative_boolean_phrase
32
+ # * #to_positive_boolean_phrase, and optionally #to_negated_boolean_phrase
33
33
  # * #to_solr_conditional
34
34
  #
35
35
  class Base #:nodoc:
36
36
  include RSolr::Char
37
37
 
38
- def initialize(field, value, negative = false)
39
- @field, @value, @negative = field, value, negative
38
+ def initialize(field, value, negated = false)
39
+ @field, @value, @negated = field, value, negated
40
40
  end
41
41
 
42
42
  #
@@ -56,14 +56,14 @@ module Sunspot
56
56
 
57
57
  #
58
58
  # Return the boolean phrase associated with this restriction object.
59
- # Differentiates between positive and negative boolean phrases depending
59
+ # Differentiates between positive and negated boolean phrases depending
60
60
  # on whether this restriction is negated.
61
61
  #
62
62
  def to_boolean_phrase
63
- unless negative?
63
+ unless negated?
64
64
  to_positive_boolean_phrase
65
65
  else
66
- to_negative_boolean_phrase
66
+ to_negated_boolean_phrase
67
67
  end
68
68
  end
69
69
 
@@ -85,27 +85,31 @@ module Sunspot
85
85
  end
86
86
 
87
87
  #
88
- # Boolean phrase representing this restriction in the negative. Subclasses
88
+ # Boolean phrase representing this restriction in the negated. Subclasses
89
89
  # may choose to implement this method, but it is not necessary, as the
90
90
  # base implementation delegates to #to_positive_boolean_phrase.
91
91
  #
92
92
  # ==== Returns
93
93
  #
94
- # String:: Boolean phrase for restriction in the negative
94
+ # String:: Boolean phrase for restriction in the negated
95
95
  #
96
- def to_negative_boolean_phrase
96
+ def to_negated_boolean_phrase
97
97
  "-#{to_positive_boolean_phrase}"
98
98
  end
99
99
 
100
- protected
101
-
102
100
  #
103
101
  # Whether this restriction should be negated from its original meaning
104
102
  #
105
- def negative?
106
- !!@negative
103
+ def negated? #:nodoc:
104
+ !!@negated
105
+ end
106
+
107
+ def negate
108
+ self.class.new(@field, @value, !@negated)
107
109
  end
108
110
 
111
+ protected
112
+
109
113
  #
110
114
  # Return escaped Solr API representation of given value
111
115
  #
@@ -136,7 +140,7 @@ module Sunspot
136
140
  end
137
141
  end
138
142
 
139
- def to_negative_boolean_phrase
143
+ def to_negated_boolean_phrase
140
144
  unless @value.nil?
141
145
  super
142
146
  else
@@ -211,14 +215,18 @@ module Sunspot
211
215
  # Result must be the exact instance given (only useful when negated).
212
216
  #
213
217
  class SameAs < Base
214
- def initialize(object, negative = false)
215
- @object, @negative = object, negative
218
+ def initialize(object, negated = false)
219
+ @object, @negated = object, negated
216
220
  end
217
221
 
218
222
  def to_positive_boolean_phrase
219
223
  adapter = Adapters::InstanceAdapter.adapt(@object)
220
224
  "id:#{escape(adapter.index_id)}"
221
225
  end
226
+
227
+ def negate
228
+ SameAs.new(@object, !@negated)
229
+ end
222
230
  end
223
231
  end
224
232
  end
@@ -288,7 +288,85 @@ describe 'Search' do
288
288
  end
289
289
  end
290
290
  connection.should have_last_search_with(
291
- :fq => '(category_ids_im:1 OR -average_rating_f:[3\.0 TO *])'
291
+ :fq => '-(-category_ids_im:1 AND average_rating_f:[3\.0 TO *])'
292
+ )
293
+ end
294
+
295
+ it 'should create a disjunction with nested conjunction with negated restrictions' do
296
+ session.search Post do
297
+ any_of do
298
+ with :category_ids, 1
299
+ all_of do
300
+ without(:average_rating).greater_than(3.0)
301
+ with(:blog_id, 1)
302
+ end
303
+ end
304
+ end
305
+ connection.should have_last_search_with(
306
+ :fq => '(category_ids_im:1 OR (-average_rating_f:[3\.0 TO *] AND blog_id_i:1))'
307
+ )
308
+ end
309
+
310
+ it 'should create a disjunction with nested conjunction with nested disjunction with negated restriction' do
311
+ session.search(Post) do
312
+ any_of do
313
+ with(:title, 'Yes')
314
+ all_of do
315
+ with(:blog_id, 1)
316
+ any_of do
317
+ with(:category_ids, 4)
318
+ without(:average_rating, 2.0)
319
+ end
320
+ end
321
+ end
322
+ end
323
+ connection.should have_last_search_with(
324
+ :fq => '(title_ss:Yes OR (blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
325
+ )
326
+ end
327
+
328
+ it 'should create a disjunction with a negated restriction and a nested disjunction in a conjunction with a negated restriction' do
329
+ session.search(Post) do
330
+ any_of do
331
+ without(:title, 'Yes')
332
+ all_of do
333
+ with(:blog_id, 1)
334
+ any_of do
335
+ with(:category_ids, 4)
336
+ without(:average_rating, 2.0)
337
+ end
338
+ end
339
+ end
340
+ end
341
+ connection.should have_last_search_with(
342
+ :fq => '-(title_ss:Yes AND -(blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
343
+ )
344
+ end
345
+
346
+ #
347
+ # This is important because if a disjunction could be nested in another
348
+ # disjunction, then the inner disjunction could denormalize (and thus
349
+ # become negated) after the outer disjunction denormalized (checking to
350
+ # see if the inner one is negated). Since conjunctions never need to
351
+ # denormalize, if a disjunction can only contain conjunctions or restrictions,
352
+ # we can guarantee that the negation state of a disjunction's components will
353
+ # not change when #to_params is called on them.
354
+ #
355
+ # Since disjunction is associative, this behavior has no effect on the actual
356
+ # logical semantics of the disjunction.
357
+ #
358
+ it 'should create a single disjunction when disjunctions nested' do
359
+ session.search(Post) do
360
+ any_of do
361
+ with(:title, 'Yes')
362
+ any_of do
363
+ with(:blog_id, 1)
364
+ with(:category_ids, 4)
365
+ end
366
+ end
367
+ end
368
+ connection.should have_last_search_with(
369
+ :fq => '(title_ss:Yes OR blog_id_i:1 OR category_ids_im:4)'
292
370
  )
293
371
  end
294
372
 
@@ -301,7 +379,7 @@ describe 'Search' do
301
379
  end
302
380
  end
303
381
  connection.should have_last_search_with(
304
- :fq => "(-id:Post\\ #{post.id} OR category_ids_im:1)"
382
+ :fq => "-(id:Post\\ #{post.id} AND -category_ids_im:1)"
305
383
  )
306
384
  end
307
385
 
@@ -153,6 +153,107 @@ describe 'scoped_search' do
153
153
  end
154
154
  end
155
155
 
156
+ describe 'connectives' do
157
+ before :each do
158
+ Sunspot.remove_all
159
+ end
160
+
161
+ it 'should return results that match any restriction in a disjunction' do
162
+ posts = (1..3).map { |i| Post.new(:blog_id => i)}
163
+ Sunspot.index!(posts)
164
+ Sunspot.search(Post) do
165
+ any_of do
166
+ with(:blog_id, 1)
167
+ with(:blog_id, 2)
168
+ end
169
+ end.results.should == posts[0..1]
170
+ end
171
+
172
+ it 'should return results that match a nested conjunction in a disjunction' do
173
+ posts = [
174
+ Post.new(:title => 'No', :blog_id => 1),
175
+ Post.new(:title => 'Yes', :blog_id => 2),
176
+ Post.new(:title => 'Yes', :blog_id => 3),
177
+ Post.new(:title => 'No', :blog_id => 2)
178
+ ]
179
+ Sunspot.index!(posts)
180
+ Sunspot.search(Post) do
181
+ any_of do
182
+ with(:blog_id, 1)
183
+ all_of do
184
+ with(:blog_id, 2)
185
+ with(:title, 'Yes')
186
+ end
187
+ end
188
+ end.results.should == posts[0..1]
189
+ end
190
+
191
+ it 'should return results that match a conjunction with a negated restriction' do
192
+ posts = [
193
+ Post.new(:title => 'No', :blog_id => 1),
194
+ Post.new(:title => 'Yes', :blog_id => 2),
195
+ Post.new(:title => 'No', :blog_id => 2)
196
+ ]
197
+ Sunspot.index!(posts)
198
+ search = Sunspot.search(Post) do
199
+ any_of do
200
+ with(:blog_id, 1)
201
+ without(:title, 'No')
202
+ end
203
+ end
204
+ search.results.should == posts[0..1]
205
+ end
206
+
207
+ it 'should return results that match a conjunction with a disjunction with a conjunction with a negated restriction' do
208
+ posts = [
209
+ Post.new(:title => 'Yes', :ratings_average => 2.0),
210
+ Post.new(:blog_id => 1, :category_ids => [4], :ratings_average => 2.0),
211
+ Post.new(:blog_id => 1),
212
+ Post.new(:blog_id => 2),
213
+ Post.new(:blog_id => 1, :ratings_average => 2.0)
214
+ ]
215
+ Sunspot.index!(posts)
216
+ search = Sunspot.search(Post) do
217
+ any_of do
218
+ with(:title, 'Yes')
219
+ all_of do
220
+ with(:blog_id, 1)
221
+ any_of do
222
+ with(:category_ids, 4)
223
+ without(:average_rating, 2.0)
224
+ end
225
+ end
226
+ end
227
+ end
228
+ search.results.should == posts[0..2]
229
+ end
230
+
231
+ it 'should return results that match a disjunction with a negated restriction and a nested disjunction in a conjunction with a negated restriction' do
232
+ posts = [
233
+ Post.new,
234
+ Post.new(:title => 'Yes', :blog_id => 1, :category_ids => [4], :ratings_average => 2.0),
235
+ Post.new(:title => 'Yes', :blog_id => 1),
236
+ Post.new(:title => 'Yes'),
237
+ Post.new(:title => 'Yes', :category_ids => [4], :ratings_average => 2.0),
238
+ Post.new(:title => 'Yes', :blog_id => 1, :ratings_average => 2.0)
239
+ ]
240
+ Sunspot.index!(posts)
241
+ search = Sunspot.search(Post) do
242
+ any_of do
243
+ without(:title, 'Yes')
244
+ all_of do
245
+ with(:blog_id, 1)
246
+ any_of do
247
+ with(:category_ids, 4)
248
+ without(:average_rating, 2.0)
249
+ end
250
+ end
251
+ end
252
+ end
253
+ search.results.should == posts[0..2]
254
+ end
255
+ end
256
+
156
257
  describe 'multiple column ordering' do
157
258
  before do
158
259
  Sunspot.remove_all
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: outoftime-sunspot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mat Brown
@@ -12,7 +12,7 @@ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
14
 
15
- date: 2009-07-23 00:00:00 -07:00
15
+ date: 2009-07-29 00:00:00 -07:00
16
16
  default_executable:
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
@@ -189,6 +189,7 @@ files:
189
189
  - templates/schema.xml.haml
190
190
  has_rdoc: true
191
191
  homepage: http://github.com/outoftime/sunspot
192
+ licenses:
192
193
  post_install_message:
193
194
  rdoc_options:
194
195
  - --charset=UTF-8
@@ -214,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
215
  requirements: []
215
216
 
216
217
  rubyforge_project:
217
- rubygems_version: 1.2.0
218
+ rubygems_version: 1.3.5
218
219
  signing_key:
219
220
  specification_version: 3
220
221
  summary: Library for expressive, powerful interaction with the Solr search engine