UnderpantsGnome-sunspot 0.9.1.1 → 0.9.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -28,6 +28,8 @@ module Sunspot
28
28
  )
29
29
  end
30
30
  DateFieldFacet.new(field, options)
31
+ elsif options.has_key?(:only)
32
+ QueryFieldFacet.new(field, options.delete(:only))
31
33
  else
32
34
  FieldFacet.new(field, options)
33
35
  end
@@ -86,8 +88,7 @@ module Sunspot
86
88
  :"facet.date" => [@field.indexed_name],
87
89
  param_key('date.start') => start_time.utc.xmlschema,
88
90
  param_key('date.end') => end_time.utc.xmlschema,
89
- param_key('date.gap') => "+#{interval}SECONDS",
90
- param_key('date.other') => others
91
+ param_key('date.gap') => "+#{interval}SECONDS"
91
92
  )
92
93
  end
93
94
 
@@ -18,7 +18,14 @@ module Sunspot
18
18
  # FieldFacet:: The field facet object
19
19
  #
20
20
  def add_field_facet(field_name, options = nil)
21
- add_component(FieldFacet.build(build_field(field_name), options || {}))
21
+ options ||= {}
22
+ facet =
23
+ if only = options.delete(:only)
24
+ query_facets[field_name.to_sym] = QueryFieldFacet.new(@setup.field(field_name), only)
25
+ else
26
+ FieldFacet.build(build_field(field_name), options)
27
+ end
28
+ add_component(facet)
22
29
  end
23
30
 
24
31
  #
@@ -38,7 +45,6 @@ module Sunspot
38
45
  def add_query_facet(name)
39
46
  add_component(facet = QueryFacet.new(name, setup))
40
47
  query_facets[name.to_sym] = facet
41
- facet
42
48
  end
43
49
 
44
50
  #
@@ -14,8 +14,9 @@ module Sunspot
14
14
  #
15
15
  class QueryFacet
16
16
  attr_reader :name #:nodoc:
17
+ attr_reader :field #:nodoc:
17
18
 
18
- def initialize(name, setup) #:nodoc:
19
+ def initialize(name, setup = nil) #:nodoc:
19
20
  @name = name
20
21
  @setup = setup
21
22
  @components = []
@@ -0,0 +1,13 @@
1
+ module Sunspot
2
+ module Query
3
+ class QueryFieldFacet < QueryFacet
4
+ def initialize(field, values)
5
+ super(field.name)
6
+ @field = field
7
+ values.each do |value|
8
+ add_row(value).add_component(Restriction::EqualTo.new(field, value))
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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
  #
@@ -132,15 +136,15 @@ module Sunspot
132
136
  unless @value.nil?
133
137
  super
134
138
  else
135
- "-#{escape(@field.indexed_name)}:[* TO *]"
139
+ "#{escape(@field.indexed_name)}:[* TO *]"
136
140
  end
137
141
  end
138
142
 
139
- def to_negative_boolean_phrase
140
- unless @value.nil?
141
- super
143
+ def negated?
144
+ if @value.nil?
145
+ !super
142
146
  else
143
- "#{escape(@field.indexed_name)}:[* TO *]"
147
+ super
144
148
  end
145
149
  end
146
150
 
@@ -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
@@ -90,15 +90,14 @@ module Sunspot
90
90
  def facet(field_name)
91
91
  (@facets_cache ||= {})[field_name.to_sym] ||=
92
92
  begin
93
- query_facet(field_name) ||
93
+ facet_data = query_facet_data(field_name) ||
94
94
  begin
95
95
  field = field(field_name)
96
- date_facet(field) ||
97
- begin
98
- facet_class = field.reference ? InstantiatedFacet : Facet
99
- facet_class.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
100
- end
96
+ date_facet_data(field) ||
97
+ FacetData::FieldFacetData.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
101
98
  end
99
+ facet_class = facet_data.reference ? InstantiatedFacet : Facet
100
+ facet_class.new(facet_data)
102
101
  end
103
102
  end
104
103
 
@@ -132,7 +131,7 @@ module Sunspot
132
131
  (@dynamic_facets_cache ||= {})[[base_name.to_sym, dynamic_name.to_sym]] ||=
133
132
  begin
134
133
  field = @setup.dynamic_field_factory(base_name).build(dynamic_name)
135
- Facet.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
134
+ Facet.new(FacetData::FieldFacetData.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field))
136
135
  end
137
136
  end
138
137
 
@@ -159,16 +158,14 @@ module Sunspot
159
158
  end
160
159
 
161
160
  def populate_hits! #:nodoc:
162
- type_hit_hash = Hash.new { |h, k| h[k] = [] }
163
- id_hit_hash = {}
164
- for hit in hits
165
- type_hit_hash[hit.class_name] << hit
166
- id_hit_hash[hit.primary_key] = hit
161
+ id_hit_hash = Hash.new { |h, k| h[k] = {} }
162
+ hits.each do |hit|
163
+ id_hit_hash[hit.class_name][hit.primary_key] = hit
167
164
  end
168
- type_hit_hash.each_pair do |class_name, hits|
169
- ids = hits.map { |hit| hit.primary_key }
170
- for instance in data_accessor_for(Util.full_const_get(class_name)).load_all(ids)
171
- hit = id_hit_hash[Adapters::InstanceAdapter.adapt(instance).id.to_s]
165
+ id_hit_hash.each_pair do |class_name, hits|
166
+ ids = hits.map { |id, hit| hit.primary_key }
167
+ data_accessor_for(Util.full_const_get(class_name)).load_all(ids).each do |instance|
168
+ hit = id_hit_hash[class_name][Adapters::InstanceAdapter.adapt(instance).id.to_s]
172
169
  hit.instance = instance
173
170
  end
174
171
  end
@@ -194,23 +191,23 @@ module Sunspot
194
191
  end || @solr_result['facet_counts']['facet_fields'][field.indexed_name]
195
192
  end
196
193
 
197
- def date_facet(field)
194
+ def date_facet_data(field)
198
195
  if field.type == Type::TimeType
199
196
  if @solr_result['facet_counts'].has_key?('facet_dates')
200
197
  if facet_result = @solr_result['facet_counts']['facet_dates'][field.indexed_name]
201
- DateFacet.new(facet_result, field)
198
+ FacetData::DateFacetData.new(facet_result, field)
202
199
  end
203
200
  end
204
201
  end
205
202
  end
206
203
 
207
- def query_facet(name)
204
+ def query_facet_data(name)
208
205
  if query_facet = @query.query_facet(name.to_sym)
209
206
  if @solr_result['facet_counts'].has_key?('facet_queries')
210
- QueryFacet.new(
211
- query_facet,
212
- @solr_result['facet_counts']['facet_queries']
213
- )
207
+ FacetData::QueryFacetData.new(
208
+ query_facet,
209
+ @solr_result['facet_counts']['facet_queries']
210
+ )
214
211
  end
215
212
  end
216
213
  end
@@ -57,6 +57,10 @@ module Sunspot
57
57
  end
58
58
  @instance
59
59
  end
60
+
61
+ def inspect
62
+ "#<Sunspot::Search::Hit:#{@class_name} #{@primary_key}>"
63
+ end
60
64
  end
61
65
  end
62
66
  end
@@ -15,7 +15,7 @@ module Sunspot
15
15
  # For testing purposes
16
16
  #
17
17
  def connection_class #:nodoc:
18
- @connection_class ||= RSolr::Connection
18
+ @connection_class ||= RSolr
19
19
  end
20
20
  end
21
21
 
@@ -46,8 +46,8 @@ module Sunspot
46
46
  Setup.for(types.first)
47
47
  else
48
48
  CompositeSetup.for(types)
49
- end
50
- Search.new(connection, setup, Query::Query.new(setup, @config))
49
+ end
50
+ Search.new(connection, setup, Query::Query.new(types, setup, @config))
51
51
  end
52
52
 
53
53
  #
@@ -186,11 +186,12 @@ module Sunspot
186
186
  def connection
187
187
  @connection ||=
188
188
  begin
189
- connection = self.class.connection_class.new(
190
- RSolr::Adapter::HTTP.new(:url => config.solr.url)
191
- )
192
- connection.adapter.connector.adapter_name = config.http_client
193
- connection
189
+ self.class.connection_class.connect(:url => config.solr.url, :adapter => config.http_client)
190
+ # connection = self.class.connection_class.new(
191
+ # RSolr::Adapter::HTTP.new(:url => config.solr.url)
192
+ # )
193
+ # connection.adapter.connector.adapter_name = config.http_client
194
+ # connection
194
195
  end
195
196
  end
196
197
 
@@ -27,6 +27,16 @@ describe 'Search' do
27
27
  connection.should have_last_search_with(:fq => 'type:Post')
28
28
  end
29
29
 
30
+ it 'should search types in main query if keywords not used' do
31
+ session.search Post
32
+ connection.should have_last_search_with(:q => 'type:Post')
33
+ end
34
+
35
+ it 'should search type of subclass when superclass is configured' do
36
+ session.search PhotoPost
37
+ connection.should have_last_search_with(:q => 'type:PhotoPost')
38
+ end
39
+
30
40
  it 'should search all text fields for searched class' do
31
41
  session.search Post do
32
42
  keywords 'keyword search'
@@ -288,7 +298,85 @@ describe 'Search' do
288
298
  end
289
299
  end
290
300
  connection.should have_last_search_with(
291
- :fq => '(category_ids_im:1 OR -average_rating_f:[3\.0 TO *])'
301
+ :fq => '-(-category_ids_im:1 AND average_rating_f:[3\.0 TO *])'
302
+ )
303
+ end
304
+
305
+ it 'should create a disjunction with nested conjunction with negated restrictions' do
306
+ session.search Post do
307
+ any_of do
308
+ with :category_ids, 1
309
+ all_of do
310
+ without(:average_rating).greater_than(3.0)
311
+ with(:blog_id, 1)
312
+ end
313
+ end
314
+ end
315
+ connection.should have_last_search_with(
316
+ :fq => '(category_ids_im:1 OR (-average_rating_f:[3\.0 TO *] AND blog_id_i:1))'
317
+ )
318
+ end
319
+
320
+ it 'should create a disjunction with nested conjunction with nested disjunction with negated restriction' do
321
+ session.search(Post) do
322
+ any_of do
323
+ with(:title, 'Yes')
324
+ all_of do
325
+ with(:blog_id, 1)
326
+ any_of do
327
+ with(:category_ids, 4)
328
+ without(:average_rating, 2.0)
329
+ end
330
+ end
331
+ end
332
+ end
333
+ connection.should have_last_search_with(
334
+ :fq => '(title_ss:Yes OR (blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
335
+ )
336
+ end
337
+
338
+ it 'should create a disjunction with a negated restriction and a nested disjunction in a conjunction with a negated restriction' do
339
+ session.search(Post) do
340
+ any_of do
341
+ without(:title, 'Yes')
342
+ all_of do
343
+ with(:blog_id, 1)
344
+ any_of do
345
+ with(:category_ids, 4)
346
+ without(:average_rating, 2.0)
347
+ end
348
+ end
349
+ end
350
+ end
351
+ connection.should have_last_search_with(
352
+ :fq => '-(title_ss:Yes AND -(blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
353
+ )
354
+ end
355
+
356
+ #
357
+ # This is important because if a disjunction could be nested in another
358
+ # disjunction, then the inner disjunction could denormalize (and thus
359
+ # become negated) after the outer disjunction denormalized (checking to
360
+ # see if the inner one is negated). Since conjunctions never need to
361
+ # denormalize, if a disjunction can only contain conjunctions or restrictions,
362
+ # we can guarantee that the negation state of a disjunction's components will
363
+ # not change when #to_params is called on them.
364
+ #
365
+ # Since disjunction is associative, this behavior has no effect on the actual
366
+ # logical semantics of the disjunction.
367
+ #
368
+ it 'should create a single disjunction when disjunctions nested' do
369
+ session.search(Post) do
370
+ any_of do
371
+ with(:title, 'Yes')
372
+ any_of do
373
+ with(:blog_id, 1)
374
+ with(:category_ids, 4)
375
+ end
376
+ end
377
+ end
378
+ connection.should have_last_search_with(
379
+ :fq => '(title_ss:Yes OR blog_id_i:1 OR category_ids_im:4)'
292
380
  )
293
381
  end
294
382
 
@@ -301,7 +389,19 @@ describe 'Search' do
301
389
  end
302
390
  end
303
391
  connection.should have_last_search_with(
304
- :fq => "(-id:Post\\ #{post.id} OR category_ids_im:1)"
392
+ :fq => "-(id:Post\\ #{post.id} AND -category_ids_im:1)"
393
+ )
394
+ end
395
+
396
+ it 'should create a disjunction with empty restriction' do
397
+ session.search Post do
398
+ any_of do
399
+ with(:average_rating, nil)
400
+ with(:average_rating).greater_than(3.0)
401
+ end
402
+ end
403
+ connection.should have_last_search_with(
404
+ :fq => '-(average_rating_f:[* TO *] AND -average_rating_f:[3\.0 TO *])'
305
405
  )
306
406
  end
307
407
 
@@ -608,28 +708,6 @@ describe 'Search' do
608
708
  connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+3600SECONDS")
609
709
  end
610
710
 
611
- it 'should allow computation of one other time' do
612
- session.search Post do |query|
613
- query.facet :published_at, :time_range => @time_range, :time_other => :before
614
- end
615
- connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before))
616
- end
617
-
618
- it 'should allow computation of two other times' do
619
- session.search Post do |query|
620
- query.facet :published_at, :time_range => @time_range, :time_other => [:before, :after]
621
- end
622
- connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before after))
623
- end
624
-
625
- it 'should not allow computation of bogus other time' do
626
- lambda do
627
- session.search Post do |query|
628
- query.facet :published_at, :time_range => @time_range, :time_other => :bogus
629
- end
630
- end.should raise_error(ArgumentError)
631
- end
632
-
633
711
  it 'should not allow date faceting on a non-date field' do
634
712
  lambda do
635
713
  session.search Post do |query|
@@ -760,6 +838,27 @@ describe 'Search' do
760
838
  end
761
839
  end
762
840
 
841
+ it 'builds query facets when passed :only argument to field facet declaration' do
842
+ session.search Post do
843
+ facet :category_ids, :only => [1, 3]
844
+ end
845
+ connection.should have_last_search_with(
846
+ :"facet.query" => ['category_ids_im:1', 'category_ids_im:3']
847
+ )
848
+ end
849
+
850
+ it 'converts limited query facet values to the correct type' do
851
+ session.search Post do
852
+ facet :published_at, :only => [Time.utc(2009, 8, 28, 15, 33), Time.utc(2008,8, 28, 15, 33)]
853
+ end
854
+ connection.should have_last_search_with(
855
+ :"facet.query" => [
856
+ 'published_at_d:2009\-08\-28T15\:33\:00Z',
857
+ 'published_at_d:2008\-08\-28T15\:33\:00Z'
858
+ ]
859
+ )
860
+ end
861
+
763
862
  it 'should allow faceting by dynamic string field' do
764
863
  session.search Post do
765
864
  dynamic :custom_string do