sunspot 2.3.0 → 2.6.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +4 -4
  3. data/lib/sunspot/adapters.rb +15 -1
  4. data/lib/sunspot/data_extractor.rb +36 -6
  5. data/lib/sunspot/dsl/fields.rb +16 -0
  6. data/lib/sunspot/dsl/fulltext.rb +1 -1
  7. data/lib/sunspot/dsl/group.rb +10 -0
  8. data/lib/sunspot/dsl/scope.rb +17 -17
  9. data/lib/sunspot/dsl/standard_query.rb +29 -1
  10. data/lib/sunspot/dsl.rb +2 -2
  11. data/lib/sunspot/field.rb +15 -4
  12. data/lib/sunspot/indexer.rb +37 -8
  13. data/lib/sunspot/query/abstract_fulltext.rb +7 -3
  14. data/lib/sunspot/query/abstract_json_field_facet.rb +3 -0
  15. data/lib/sunspot/query/composite_fulltext.rb +21 -2
  16. data/lib/sunspot/query/date_field_json_facet.rb +2 -16
  17. data/lib/sunspot/query/dismax.rb +10 -4
  18. data/lib/sunspot/query/function_query.rb +25 -1
  19. data/lib/sunspot/query/group.rb +4 -5
  20. data/lib/sunspot/query/join.rb +3 -5
  21. data/lib/sunspot/query/range_json_facet.rb +5 -2
  22. data/lib/sunspot/query/restriction.rb +18 -13
  23. data/lib/sunspot/query/standard_query.rb +12 -0
  24. data/lib/sunspot/search/abstract_search.rb +1 -1
  25. data/lib/sunspot/search/field_json_facet.rb +14 -3
  26. data/lib/sunspot/search/hit.rb +6 -1
  27. data/lib/sunspot/session.rb +9 -1
  28. data/lib/sunspot/setup.rb +69 -0
  29. data/lib/sunspot/util.rb +4 -11
  30. data/lib/sunspot/version.rb +1 -1
  31. data/lib/sunspot.rb +9 -1
  32. data/spec/api/adapters_spec.rb +13 -0
  33. data/spec/api/data_extractor_spec.rb +39 -0
  34. data/spec/api/indexer/removal_spec.rb +87 -0
  35. data/spec/api/query/connective_boost_examples.rb +85 -0
  36. data/spec/api/query/fulltext_examples.rb +6 -12
  37. data/spec/api/query/join_spec.rb +2 -2
  38. data/spec/api/query/standard_spec.rb +10 -0
  39. data/spec/api/search/hits_spec.rb +14 -0
  40. data/spec/api/setup_spec.rb +99 -0
  41. data/spec/api/sunspot_spec.rb +3 -0
  42. data/spec/helpers/indexer_helper.rb +22 -0
  43. data/spec/integration/atomic_updates_spec.rb +169 -5
  44. data/spec/integration/faceting_spec.rb +68 -34
  45. data/spec/integration/field_grouping_spec.rb +19 -0
  46. data/spec/integration/field_lists_spec.rb +16 -0
  47. data/spec/integration/geospatial_spec.rb +15 -0
  48. data/spec/integration/join_spec.rb +64 -0
  49. data/spec/integration/scoped_search_spec.rb +78 -0
  50. data/spec/mocks/adapters.rb +33 -0
  51. data/spec/mocks/connection.rb +6 -0
  52. data/spec/mocks/photo.rb +19 -5
  53. data/spec/mocks/post.rb +35 -1
  54. data/sunspot.gemspec +0 -2
  55. metadata +14 -8
  56. data/gemfiles/.gitkeep +0 -0
@@ -78,14 +78,14 @@ module Sunspot
78
78
  # on whether this restriction is negated.
79
79
  #
80
80
  def to_boolean_phrase
81
- phrase = []
82
- phrase << @field.local_params if @field.respond_to? :local_params
83
- unless negated?
84
- phrase << to_positive_boolean_phrase
85
- else
86
- phrase << to_negated_boolean_phrase
87
- end
88
- phrase.join
81
+ value = if negated?
82
+ to_negated_boolean_phrase
83
+ else
84
+ to_positive_boolean_phrase
85
+ end
86
+
87
+ @field.respond_to?(:local_params) ?
88
+ @field.local_params(value) : value
89
89
  end
90
90
 
91
91
  #
@@ -135,7 +135,7 @@ module Sunspot
135
135
 
136
136
  protected
137
137
 
138
- #
138
+ #
139
139
  # Return escaped Solr API representation of given value
140
140
  #
141
141
  # ==== Parameters
@@ -158,9 +158,13 @@ module Sunspot
158
158
  end
159
159
 
160
160
  class InRadius < Base
161
- def initialize(negated, field, lat, lon, radius)
162
- @lat, @lon, @radius = lat, lon, radius
163
- super negated, field, [lat, lon, radius]
161
+ def initialize(negated, field, *value)
162
+ @lat, @lon, @radius = value
163
+ super negated, field, value
164
+ end
165
+
166
+ def negate
167
+ self.class.new(!@negated, @field, *@value)
164
168
  end
165
169
 
166
170
  private
@@ -204,7 +208,8 @@ module Sunspot
204
208
  private
205
209
 
206
210
  def to_solr_conditional
207
- "#{solr_value}"
211
+ @field.respond_to?(:to_solr_conditional) ?
212
+ @field.to_solr_conditional(solr_value) : "#{solr_value}"
208
213
  end
209
214
  end
210
215
 
@@ -16,6 +16,18 @@ module Sunspot
16
16
  @fulltext.add_join(keywords, target, from, to)
17
17
  end
18
18
 
19
+ def add_boost_query(factor)
20
+ @fulltext.add_boost_query(factor)
21
+ end
22
+
23
+ def add_boost_function(function)
24
+ @fulltext.add_boost_function(function)
25
+ end
26
+
27
+ def add_multiplicative_boost_function(function)
28
+ @fulltext.add_multiplicative_boost_function(function)
29
+ end
30
+
19
31
  def disjunction
20
32
  parent_fulltext = @fulltext
21
33
  @fulltext = @fulltext.add_disjunction
@@ -16,7 +16,7 @@ module Sunspot
16
16
  # Retrieve all facet objects defined for this search, in order they were
17
17
  # defined. To retrieve an individual facet by name, use #facet()
18
18
  #
19
- attr_reader :facets, :groups, :stats
19
+ attr_reader :facets, :groups
20
20
  attr_reader :query #:nodoc:
21
21
  attr_accessor :request_handler
22
22
 
@@ -5,15 +5,14 @@ module Sunspot
5
5
  attr_reader :name
6
6
 
7
7
  def initialize(field, search, options)
8
- @name, @search, @options = name, search, options
8
+ @name, @search, @options = (options[:name] || field.name), search, options
9
9
  @field = field
10
10
  end
11
11
 
12
12
  def rows
13
13
  @rows ||=
14
14
  begin
15
- json_facet_response = @search.json_facet_response[@field.name.to_s]
16
- data = json_facet_response.nil? ? [] : json_facet_response['buckets']
15
+ data = no_data? ? [] : @search.json_facet_response[@field.name.to_s]['buckets']
17
16
  rows = []
18
17
  data.each do |d|
19
18
  rows << JsonFacetRow.new(d, self)
@@ -28,6 +27,18 @@ module Sunspot
28
27
  end
29
28
 
30
29
  end
30
+
31
+ def no_data?
32
+ @search.json_facet_response[@field.name.to_s].nil?
33
+ end
34
+
35
+ def other_count(type)
36
+ json_facet_for_field = @search.json_facet_response[@field.name.to_s]
37
+ return 0 if json_facet_for_field.nil?
38
+
39
+ other = json_facet_for_field[type.to_s] || {}
40
+ other['count']
41
+ end
31
42
  end
32
43
  end
33
44
  end
@@ -17,6 +17,10 @@ module Sunspot
17
17
  # Class name of object associated with this hit, as string.
18
18
  #
19
19
  attr_reader :class_name
20
+ #
21
+ # ID prefix used for compositeId shard router
22
+ #
23
+ attr_reader :id_prefix
20
24
  #
21
25
  # Keyword relevance score associated with this result. Nil if this hit
22
26
  # is not from a keyword search.
@@ -27,7 +31,8 @@ module Sunspot
27
31
  attr_writer :result #:nodoc:
28
32
 
29
33
  def initialize(raw_hit, highlights, search) #:nodoc:
30
- @class_name, @primary_key = *raw_hit['id'].match(/([^ ]+) (.+)/)[1..2]
34
+ @id_prefix, @class_name, @primary_key =
35
+ *raw_hit['id'].match(/((?:[^!]+!)+)*([^\s]+)\s(.+)/)[1..3]
31
36
  @score = raw_hit['score']
32
37
  @search = search
33
38
  @stored_values = raw_hit
@@ -259,7 +259,7 @@ module Sunspot
259
259
  read_timeout: config.solr.read_timeout,
260
260
  open_timeout: config.solr.open_timeout,
261
261
  proxy: config.solr.proxy,
262
- update_format: config.solr.update_format || :xml
262
+ update_format: update_format
263
263
  )
264
264
  end
265
265
 
@@ -277,5 +277,13 @@ module Sunspot
277
277
  CompositeSetup.for(types)
278
278
  end
279
279
  end
280
+
281
+ def update_format
282
+ if config.solr.update_format && config.solr.update_format.to_s.match(/xml|json/i)
283
+ config.solr.update_format.downcase.to_sym
284
+ else
285
+ :xml
286
+ end
287
+ end
280
288
  end
281
289
  end
data/lib/sunspot/setup.rb CHANGED
@@ -15,6 +15,7 @@ module Sunspot
15
15
  @more_like_this_field_factories_cache = Hash.new { |h, k| h[k] = [] }
16
16
  @dsl = DSL::Fields.new(self)
17
17
  @document_boost_extractor = nil
18
+ @id_prefix_extractor = nil
18
19
  add_field_factory(:class, Type::ClassType.instance)
19
20
  end
20
21
 
@@ -61,6 +62,7 @@ module Sunspot
61
62
  field_factory = FieldFactory::Static.new(name, Type::TextType.instance, options, &block)
62
63
  @text_field_factories[name] = field_factory
63
64
  @text_field_factories_cache[field_factory.name] = field_factory
65
+ @field_factories_cache[field_factory.name] = field_factory
64
66
  if stored
65
67
  @stored_field_factories_cache[field_factory.name] << field_factory
66
68
  end
@@ -81,6 +83,7 @@ module Sunspot
81
83
  field_factory = FieldFactory::Dynamic.new(name, type, options, &block)
82
84
  @dynamic_field_factories[field_factory.signature] = field_factory
83
85
  @dynamic_field_factories_cache[field_factory.name] = field_factory
86
+ @field_factories_cache[field_factory.name] = field_factory
84
87
  if stored
85
88
  @stored_field_factories_cache[field_factory.name] << field_factory
86
89
  end
@@ -107,6 +110,24 @@ module Sunspot
107
110
  end
108
111
  end
109
112
 
113
+ #
114
+ # Add id prefix for compositeId router
115
+ #
116
+ def add_id_prefix(attr_name, &block)
117
+ @id_prefix_extractor =
118
+ case attr_name
119
+ when Symbol
120
+ DataExtractor::AttributeExtractor.new(attr_name)
121
+ when String
122
+ DataExtractor::Constant.new(attr_name)
123
+ when nil
124
+ DataExtractor::BlockExtractor.new(&block) if block_given?
125
+ else
126
+ raise ArgumentError,
127
+ "The ID prefix has to be either a Symbol, a String or a Proc"
128
+ end
129
+ end
130
+
110
131
  #
111
132
  # Builder method for evaluating the setup DSL
112
133
  #
@@ -271,6 +292,54 @@ module Sunspot
271
292
  end
272
293
  end
273
294
 
295
+ def id_prefix_for(model)
296
+ if @id_prefix_extractor
297
+ value = @id_prefix_extractor.value_for(model)
298
+
299
+ if value.is_a?(String) and value.size > 0
300
+ value[-1] == "!" ? value : "#{value}!"
301
+ end
302
+ end
303
+ end
304
+
305
+ #
306
+ # Get value for `id_prefix` defined as String
307
+ #
308
+ # ==== Returns
309
+ #
310
+ # String:: value for `id_prefix` defined as String
311
+ #
312
+ def id_prefix_for_class
313
+ return if !id_prefix_defined? || id_prefix_requires_instance?
314
+
315
+ @id_prefix_extractor.value_for(nil)
316
+ end
317
+
318
+ #
319
+ # Check if `id_prefix` is defined for class associated with this setup.
320
+ #
321
+ # ==== Returns
322
+ #
323
+ # Boolean:: True if class associated with this setup has defined `id_prefix`
324
+ #
325
+ def id_prefix_defined?
326
+ !@id_prefix_extractor.nil?
327
+ end
328
+
329
+ #
330
+ # Check if instance is required to get `id_prefix` value (instance is required for Proc and
331
+ # Symbol `id_prefix` only. Value for String `id_prefix` can be get on class level)
332
+ #
333
+ # ==== Returns
334
+ #
335
+ # Boolean:: True if instance is required to get `id_prefix` value
336
+ #
337
+ def id_prefix_requires_instance?
338
+ return false unless id_prefix_defined?
339
+
340
+ !@id_prefix_extractor.is_a?(DataExtractor::Constant)
341
+ end
342
+
274
343
  protected
275
344
 
276
345
  #
data/lib/sunspot/util.rb CHANGED
@@ -190,22 +190,15 @@ module Sunspot
190
190
 
191
191
  def parse_json_facet(field_name, options, setup)
192
192
  field = setup.field(field_name)
193
- if options[:time_range]
194
- unless field.type.is_a?(Sunspot::Type::TimeType)
195
- raise(
196
- ArgumentError,
197
- ':time_range can only be specified for Date or Time fields'
198
- )
199
- end
200
- Sunspot::Query::DateFieldJsonFacet.new(field, options, setup)
201
- elsif options[:range]
193
+ if options[:range] || options[:time_range]
202
194
  unless [Sunspot::Type::TimeType, Sunspot::Type::FloatType, Sunspot::Type::IntegerType ].find{|type| field.type.is_a?(type)}
203
195
  raise(
204
196
  ArgumentError,
205
- ':range can only be specified for date or numeric fields'
197
+ ':range can only be specified for date, time, or numeric fields'
206
198
  )
207
199
  end
208
- Sunspot::Query::RangeJsonFacet.new(field, options, setup)
200
+ facet_klass = field.type.is_a?(Sunspot::Type::TimeType) ? Sunspot::Query::DateFieldJsonFacet : Sunspot::Query::RangeJsonFacet
201
+ facet_klass.new(field, options, setup)
209
202
  else
210
203
  Sunspot::Query::FieldJsonFacet.new(field, options, setup)
211
204
  end
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '2.3.0'
2
+ VERSION = '2.6.0'
3
3
  end
data/lib/sunspot.rb CHANGED
@@ -40,6 +40,12 @@ module Sunspot
40
40
  NoSetupError = Class.new(StandardError)
41
41
  IllegalSearchError = Class.new(StandardError)
42
42
  NotImplementedError = Class.new(StandardError)
43
+ AtomicUpdateRequireInstanceForCompositeIdMessage = lambda do |class_name|
44
+ "WARNING: `id_prefix` is defined for #{class_name}. Use instance as key for `atomic_update` instead of ID."
45
+ end
46
+ RemoveByIdNotSupportCompositeIdMessage = lambda do |class_name|
47
+ "WARNING: `id_prefix` is defined for #{class_name}. `remove_by_id` does not support it. Use `remove` instead."
48
+ end
43
49
 
44
50
  autoload :Installer, File.join(File.dirname(__FILE__), 'sunspot', 'installer')
45
51
 
@@ -208,6 +214,8 @@ module Sunspot
208
214
  #
209
215
  # post1, post2 = new Array(2) { Post.create }
210
216
  # Sunspot.atomic_update(Post, post1.id => {title: 'New Title'}, post2.id => {description: 'new description'})
217
+ # Or
218
+ # Sunspot.atomic_update(Post, post1 => {title: 'New Title'}, post2 => {description: 'new description'})
211
219
  #
212
220
  # Note that indexed objects won't be reflected in search until a commit is
213
221
  # sent - see Sunspot.index! and Sunspot.commit
@@ -223,7 +231,7 @@ module Sunspot
223
231
  # ==== Parameters
224
232
  #
225
233
  # clazz<Class>:: the class of the objects to be updated
226
- # updates<Hash>:: hash of updates where keys are model ids
234
+ # updates<Hash>:: hash of updates where keys are models or model ids
227
235
  # and values are hash with property name/values to be updated
228
236
  #
229
237
  def atomic_update!(clazz, updates = {})
@@ -20,6 +20,19 @@ describe Sunspot::Adapters::InstanceAdapter do
20
20
  Sunspot::Adapters::InstanceAdapter::for(UnseenModel)
21
21
  expect(Sunspot::Adapters::InstanceAdapter::registered_adapter_for(UnseenModel)).to be(AbstractModelInstanceAdapter)
22
22
  end
23
+
24
+ it "appends ID prefix when configured" do
25
+ expect(AbstractModelInstanceAdapter.new(ModelWithPrefixId.new).index_id).to eq "USERDATA!ModelWithPrefixId 1"
26
+ end
27
+
28
+ it "supports nested ID prefixes" do
29
+ expect(AbstractModelInstanceAdapter.
30
+ new(ModelWithNestedPrefixId.new).index_id).to eq "USER!USERDATA!ModelWithNestedPrefixId 1"
31
+ end
32
+
33
+ it "doesn't appends ID prefix when not configured" do
34
+ expect(AbstractModelInstanceAdapter.new(ModelWithoutPrefixId.new).index_id).to eq "ModelWithoutPrefixId 1"
35
+ end
23
36
  end
24
37
 
25
38
  describe Sunspot::Adapters::DataAccessor do
@@ -0,0 +1,39 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe Sunspot::DataExtractor do
4
+ it "removes special characters from strings" do
5
+ extractor = Sunspot::DataExtractor::AttributeExtractor.new(:name)
6
+ blog = Blog.new(:name => "Te\x0\x1\x7\x6\x8st\xB\xC\xE Bl\x1Fo\x7fg")
7
+
8
+ expect(extractor.value_for(blog)).to eq "Test Blog"
9
+ end
10
+
11
+ it "removes special characters from arrays" do
12
+ extractor = Sunspot::DataExtractor::BlockExtractor.new { tags }
13
+ post = Post.new(:tags => ["Te\x0\x1\x7\x6\x8st Ta\x1Fg\x7f 1", "Test\xB\xC\xE Tag 2"])
14
+
15
+ expect(extractor.value_for(post)).to eq ["Test Tag 1", "Test Tag 2"]
16
+ end
17
+
18
+ it "removes special characters from hashes" do
19
+ extractor = Sunspot::DataExtractor::Constant.new({ "Te\x0\x1\x7\x6\x8st" => "Ta\x1Fg\x7f" })
20
+
21
+ expect(extractor.value_for(Post.new)).to eq({ "Test" => "Tag" })
22
+ end
23
+
24
+ it "skips other data types" do
25
+ [
26
+ :"Te\x0\x1\x7\x6\x8st",
27
+ 123,
28
+ 123.0,
29
+ nil,
30
+ false,
31
+ true,
32
+ Sunspot::Util::Coordinates.new(40.7, -73.5)
33
+ ].each do |value|
34
+ extractor = Sunspot::DataExtractor::Constant.new(value)
35
+
36
+ expect(extractor.value_for(Post.new)).to eq value
37
+ end
38
+ end
39
+ end
@@ -60,4 +60,91 @@ describe 'document removal', :type => :indexer do
60
60
  end
61
61
  expect(connection).to have_delete_by_query("(type:Post AND title_ss:monkeys)")
62
62
  end
63
+
64
+ context 'when call #remove_by_id' do
65
+ let(:post) { clazz.new(title: 'A Title') }
66
+ before(:each) { index_post(post) }
67
+
68
+ context 'and `id_prefix` is defined on model' do
69
+ context 'as Proc' do
70
+ let(:clazz) { PostWithProcPrefixId }
71
+ let(:id_prefix) { lambda { |post| "USERDATA-#{post.id}!" } }
72
+
73
+ it 'prints warning' do
74
+ expect do
75
+ session.remove_by_id(clazz, post.id)
76
+ end.to output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
77
+ end
78
+
79
+ it 'does not remove record' do
80
+ session.remove_by_id(clazz, post.id)
81
+ expect(connection).to have_no_delete(post_solr_id)
82
+ end
83
+ end
84
+
85
+ context 'as Symbol' do
86
+ let(:clazz) { PostWithSymbolPrefixId }
87
+ let(:id_prefix) { lambda { |post| "#{post.title}!" } }
88
+
89
+ it 'prints warning' do
90
+ expect do
91
+ session.remove_by_id(clazz, post.id)
92
+ end.to output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
93
+ end
94
+
95
+ it 'does not remove record' do
96
+ session.remove_by_id(clazz, post.id)
97
+ expect(connection).to have_no_delete(post_solr_id)
98
+ end
99
+ end
100
+
101
+ context 'as String' do
102
+ let(:clazz) { PostWithStringPrefixId }
103
+ let(:id_prefix) { 'USERDATA!' }
104
+
105
+ it 'does not print warning' do
106
+ expect do
107
+ session.remove_by_id(clazz, post.id)
108
+ end.to_not output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
109
+ end
110
+
111
+ it 'removes record' do
112
+ session.remove_by_id(clazz, post.id)
113
+ expect(connection).to have_delete(post_solr_id)
114
+ end
115
+ end
116
+ end
117
+
118
+ context 'and `id_prefix` is not defined on model' do
119
+ let(:clazz) { PostWithoutPrefixId }
120
+ let(:id_prefix) { nil }
121
+
122
+ it 'does not print warning' do
123
+ expect do
124
+ session.remove_by_id(clazz, post.id)
125
+ end.to_not output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
126
+ end
127
+
128
+ it 'removes record' do
129
+ session.remove_by_id(clazz, post.id)
130
+ expect(connection).to have_delete(post_solr_id)
131
+ end
132
+ end
133
+
134
+ context 'and `id_prefix` is passed along with `class_name`' do
135
+ let(:clazz) { PostWithProcPrefixId }
136
+ let(:id_prefix) { lambda { |post| "USERDATA-#{post.id}!" } }
137
+
138
+ it 'does not print warning' do
139
+ expect do
140
+ session.remove_by_id("USERDATA-#{post.id}!#{clazz.name}", post.id)
141
+ end.to_not output(Sunspot::RemoveByIdNotSupportCompositeIdMessage.call(clazz) + "\n").to_stderr
142
+ end
143
+
144
+ it 'removes record' do
145
+ session.remove_by_id("USERDATA-#{post.id}!#{clazz.name}", post.id)
146
+ expect(connection).to have_delete(post_solr_id)
147
+ end
148
+ end
149
+ end
63
150
  end
@@ -0,0 +1,85 @@
1
+ shared_examples_for "query with connective scope and boost" do
2
+ it 'creates a boost query' do
3
+ search do
4
+ boost(10) do
5
+ any_of do
6
+ with(:coordinates_new).in_bounding_box([23, -46], [25, -44])
7
+ with(:coordinates_new).in_bounding_box([42, 56], [43, 58])
8
+ end
9
+ end
10
+ end
11
+
12
+ expect(connection).to have_last_search_including(
13
+ :bq, '(coordinates_new_ll:[23,-46 TO 25,-44] OR coordinates_new_ll:[42,56 TO 43,58])^10'
14
+ )
15
+
16
+ expect(connection).to have_last_search_including(
17
+ :defType, 'edismax'
18
+ )
19
+ end
20
+
21
+ it 'creates a boost function' do
22
+ search do
23
+ boost(function() { field(:average_rating) })
24
+ end
25
+
26
+ expect(connection).to have_last_search_including(
27
+ :bf, 'field(average_rating_ft)'
28
+ )
29
+
30
+ expect(connection).to have_last_search_including(
31
+ :defType, 'edismax'
32
+ )
33
+ end
34
+
35
+ it 'creates a multiplicative boost function' do
36
+ search do
37
+ boost_multiplicative(function() { field(:average_rating) })
38
+ end
39
+
40
+ expect(connection).to have_last_search_including(
41
+ :boost, 'field(average_rating_ft)'
42
+ )
43
+
44
+ expect(connection).to have_last_search_including(
45
+ :defType, 'edismax'
46
+ )
47
+ end
48
+
49
+ it 'creates combined boost search' do
50
+ search do
51
+ boost(10) do
52
+ any_of do
53
+ with(:coordinates_new).in_bounding_box([23, -46], [25, -44])
54
+ with(:coordinates_new).in_bounding_box([42, 56], [43, 58])
55
+ end
56
+ end
57
+
58
+ boost(function() { field(:average_rating) })
59
+ boost_multiplicative(function() { field(:average_rating) })
60
+ end
61
+
62
+ expect(connection).to have_last_search_including(
63
+ :bq, '(coordinates_new_ll:[23,-46 TO 25,-44] OR coordinates_new_ll:[42,56 TO 43,58])^10'
64
+ )
65
+
66
+ expect(connection).to have_last_search_including(
67
+ :bf, 'field(average_rating_ft)'
68
+ )
69
+
70
+ expect(connection).to have_last_search_including(
71
+ :boost, 'field(average_rating_ft)'
72
+ )
73
+ end
74
+
75
+ it 'avoids duplicate boost functions' do
76
+ search do
77
+ boost(function() { field(:average_rating) })
78
+ boost(function() { field(:average_rating) })
79
+ boost_multiplicative(function() { field(:average_rating) })
80
+ end
81
+
82
+ expect(connection.searches.last[:bf]).to eq ['field(average_rating_ft)']
83
+ expect(connection.searches.last[:boost]).to eq ['field(average_rating_ft)']
84
+ end
85
+ end
@@ -418,11 +418,9 @@ shared_examples_for 'fulltext query' do
418
418
 
419
419
  obj_id = find_ob_id(srch)
420
420
  q_name = "qPhoto#{obj_id}"
421
- fq_name = "f#{q_name}"
422
421
 
423
- expect(connection.searches.last[:q]).to eq "(_query_:\"{!join from=photo_container_id_i to=id_i v=$#{q_name} fq=$#{fq_name}}\" OR _query_:\"{!edismax qf='description_text^1.2'}keyword2\")"
424
- expect(connection.searches.last[q_name]).to eq "_query_:\"{!edismax qf='caption_text'}keyword1\""
425
- expect(connection.searches.last[fq_name]).to eq "type:Photo"
422
+ expect(connection.searches.last[:q]).to eq "(_query_:\"{!join from=photo_container_id_i to=id_i v=$#{q_name}}\" OR _query_:\"{!edismax qf='description_text^1.2'}keyword2\")"
423
+ expect(connection.searches.last[q_name]).to eq "_query_:\"{!field f=type}Photo\"+_query_:\"{!edismax qf='caption_text'}keyword1\""
426
424
  end
427
425
 
428
426
  it "should be able to resolve name conflicts with the :prefix option" do
@@ -435,11 +433,9 @@ shared_examples_for 'fulltext query' do
435
433
 
436
434
  obj_id = find_ob_id(srch)
437
435
  q_name = "qPhoto#{obj_id}"
438
- fq_name = "f#{q_name}"
439
436
 
440
- expect(connection.searches.last[:q]).to eq "(_query_:\"{!edismax qf='description_text^1.2'}keyword1\" OR _query_:\"{!join from=photo_container_id_i to=id_i v=$#{q_name} fq=$#{fq_name}}\")"
441
- expect(connection.searches.last[q_name]).to eq "_query_:\"{!edismax qf='description_text'}keyword2\""
442
- expect(connection.searches.last[fq_name]).to eq "type:Photo"
437
+ expect(connection.searches.last[:q]).to eq "(_query_:\"{!edismax qf='description_text^1.2'}keyword1\" OR _query_:\"{!join from=photo_container_id_i to=id_i v=$#{q_name}}\")"
438
+ expect(connection.searches.last[q_name]).to eq "_query_:\"{!field f=type}Photo\"+_query_:\"{!edismax qf='description_text'}keyword2\""
443
439
  end
444
440
 
445
441
  it "should recognize fields when adding from DSL, e.g. when calling boost_fields" do
@@ -453,11 +449,9 @@ shared_examples_for 'fulltext query' do
453
449
 
454
450
  obj_id = find_ob_id(srch)
455
451
  q_name = "qPhoto#{obj_id}"
456
- fq_name = "f#{q_name}"
457
452
 
458
- expect(connection.searches.last[:q]).to eq "(_query_:\"{!edismax qf='description_text^1.5'}keyword1\" OR _query_:\"{!join from=photo_container_id_i to=id_i v=$#{q_name} fq=$#{fq_name}}\")"
459
- expect(connection.searches.last[q_name]).to eq "_query_:\"{!edismax qf='description_text^1.3'}keyword1\""
460
- expect(connection.searches.last[fq_name]).to eq "type:Photo"
453
+ expect(connection.searches.last[:q]).to eq "(_query_:\"{!edismax qf='description_text^1.5'}keyword1\" OR _query_:\"{!join from=photo_container_id_i to=id_i v=$#{q_name}}\")"
454
+ expect(connection.searches.last[q_name]).to eq "_query_:\"{!field f=type}Photo\"+_query_:\"{!edismax qf='description_text^1.3'}keyword1\""
461
455
  end
462
456
 
463
457
  private
@@ -6,7 +6,7 @@ describe 'join' do
6
6
  with(:caption, 'blah')
7
7
  end
8
8
  expect(connection).to have_last_search_including(
9
- :fq, "{!join from=photo_container_id_i to=id_i}caption_s:blah")
9
+ :fq, "{!join from=photo_container_id_i to=id_i v='type:\"Photo\" AND caption_s:\"blah\"'}")
10
10
  end
11
11
 
12
12
  it 'should greater_than search by join' do
@@ -14,6 +14,6 @@ describe 'join' do
14
14
  with(:photo_rating).greater_than(3)
15
15
  end
16
16
  expect(connection).to have_last_search_including(
17
- :fq, "{!join from=photo_container_id_i to=id_i}average_rating_ft:{3\\.0 TO *}")
17
+ :fq, "{!join from=photo_container_id_i to=id_i v='type:\"Photo\" AND average_rating_ft:{3\\.0 TO *}'}")
18
18
  end
19
19
  end
@@ -4,6 +4,7 @@ describe 'standard query', :type => :query do
4
4
  it_should_behave_like "scoped query"
5
5
  it_should_behave_like "query with advanced manipulation"
6
6
  it_should_behave_like "query with connective scope"
7
+ it_should_behave_like "query with connective scope and boost"
7
8
  it_should_behave_like "query with dynamic field support"
8
9
  it_should_behave_like "facetable query"
9
10
  it_should_behave_like "fulltext query"
@@ -22,6 +23,15 @@ describe 'standard query', :type => :query do
22
23
  expect(connection).to have_last_search_with(:q => '*:*')
23
24
  end
24
25
 
26
+ it 'adds a no-op query to :q parameter when only a boost query provided' do
27
+ session.search Post do
28
+ boost(2) do
29
+ with :title, 'My Pet Post'
30
+ end
31
+ end
32
+ expect(connection).to have_last_search_with(:q => '*:*')
33
+ end
34
+
25
35
  private
26
36
 
27
37
  def search(*classes, &block)