outoftime-sunspot 0.9.2 → 0.9.3

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