sunspot 2.3.0 → 2.6.0

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