sunspot 2.5.0 → 2.7.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sunspot/adapters.rb +14 -3
  3. data/lib/sunspot/data_extractor.rb +1 -1
  4. data/lib/sunspot/dsl/fulltext.rb +1 -1
  5. data/lib/sunspot/dsl/standard_query.rb +29 -1
  6. data/lib/sunspot/dsl.rb +2 -2
  7. data/lib/sunspot/field.rb +21 -4
  8. data/lib/sunspot/indexer.rb +37 -8
  9. data/lib/sunspot/query/abstract_fulltext.rb +7 -3
  10. data/lib/sunspot/query/abstract_json_field_facet.rb +3 -0
  11. data/lib/sunspot/query/composite_fulltext.rb +21 -2
  12. data/lib/sunspot/query/date_field_json_facet.rb +2 -16
  13. data/lib/sunspot/query/dismax.rb +10 -4
  14. data/lib/sunspot/query/function_query.rb +25 -1
  15. data/lib/sunspot/query/join.rb +1 -1
  16. data/lib/sunspot/query/range_json_facet.rb +5 -2
  17. data/lib/sunspot/query/restriction.rb +16 -10
  18. data/lib/sunspot/query/standard_query.rb +12 -0
  19. data/lib/sunspot/search/field_json_facet.rb +14 -3
  20. data/lib/sunspot/session.rb +7 -5
  21. data/lib/sunspot/setup.rb +38 -0
  22. data/lib/sunspot/util.rb +24 -21
  23. data/lib/sunspot/version.rb +1 -1
  24. data/lib/sunspot.rb +9 -1
  25. data/spec/api/binding_spec.rb +15 -0
  26. data/spec/api/indexer/attributes_spec.rb +1 -1
  27. data/spec/api/indexer/removal_spec.rb +87 -0
  28. data/spec/api/query/connective_boost_examples.rb +85 -0
  29. data/spec/api/query/geo_examples.rb +1 -1
  30. data/spec/api/query/join_spec.rb +2 -2
  31. data/spec/api/query/standard_spec.rb +10 -0
  32. data/spec/api/query/types_spec.rb +2 -2
  33. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +1 -1
  34. data/spec/api/session_proxy/retry_5xx_session_proxy_spec.rb +9 -5
  35. data/spec/api/session_spec.rb +1 -1
  36. data/spec/api/setup_spec.rb +99 -0
  37. data/spec/helpers/indexer_helper.rb +22 -0
  38. data/spec/integration/atomic_updates_spec.rb +169 -5
  39. data/spec/integration/faceting_spec.rb +68 -34
  40. data/spec/integration/join_spec.rb +22 -3
  41. data/spec/integration/scoped_search_spec.rb +78 -0
  42. data/spec/mocks/comment.rb +1 -1
  43. data/spec/mocks/connection.rb +6 -0
  44. data/spec/mocks/photo.rb +11 -7
  45. data/spec/mocks/post.rb +35 -1
  46. data/sunspot.gemspec +4 -6
  47. metadata +33 -15
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
@@ -255,7 +248,7 @@ module Sunspot
255
248
  Coordinates = Struct.new(:lat, :lng)
256
249
 
257
250
  class ContextBoundDelegate
258
- class <<self
251
+ class << self
259
252
  def instance_eval_with_context(receiver, &block)
260
253
  calling_context = eval('self', block.binding)
261
254
  if parent_calling_context = calling_context.instance_eval{@__calling_context__ if defined?(@__calling_context__)}
@@ -296,21 +289,31 @@ module Sunspot
296
289
  __proxy_method__(:sub, *args, &block)
297
290
  end
298
291
 
299
- def method_missing(method, *args, &block)
300
- __proxy_method__(method, *args, &block)
292
+ def method_missing(method, *args, **kwargs, &block)
293
+ __proxy_method__(method, *args, **kwargs, &block)
301
294
  end
302
295
 
303
- def __proxy_method__(method, *args, &block)
304
- begin
296
+ def respond_to_missing?(method, _)
297
+ @__receiver__.respond_to?(method, true) || super
298
+ end
299
+
300
+ def __proxy_method__(method, *args, **kwargs, &block)
301
+ if kwargs.empty?
305
302
  @__receiver__.__send__(method.to_sym, *args, &block)
306
- rescue ::NoMethodError => e
307
- begin
303
+ else
304
+ @__receiver__.__send__(method.to_sym, *args, **kwargs, &block)
305
+ end
306
+ rescue ::NoMethodError => e
307
+ begin
308
+ if kwargs.empty?
308
309
  @__calling_context__.__send__(method.to_sym, *args, &block)
309
- rescue ::NoMethodError
310
- raise(e)
310
+ else
311
+ @__calling_context__.__send__(method.to_sym, *args, **kwargs, &block)
311
312
  end
313
+ rescue ::NoMethodError
314
+ raise(e)
312
315
  end
313
- end
316
+ end
314
317
  end
315
318
  end
316
319
  end
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '2.5.0'
2
+ VERSION = '2.7.1'
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 = {})
@@ -36,6 +36,17 @@ describe "DSL bindings" do
36
36
  end
37
37
  end
38
38
  end
39
+ expect(value).to eq('value')
40
+ end
41
+
42
+ it 'should give access to calling context\'s methods with keyword arguments' do
43
+ value = nil
44
+ session.search(Post) do
45
+ any_of do
46
+ value = kwargs_method(a: 10, b: 20)
47
+ end
48
+ end
49
+ expect(value).to eq({ a: 10, b: 20 })
39
50
  end
40
51
 
41
52
  private
@@ -47,4 +58,8 @@ describe "DSL bindings" do
47
58
  def id
48
59
  16
49
60
  end
61
+
62
+ def kwargs_method(a:, b:)
63
+ { a: a, b: b }
64
+ end
50
65
  end
@@ -98,7 +98,7 @@ describe 'indexing attribute fields', :type => :indexer do
98
98
 
99
99
  it 'should index latitude and longitude passed as non-Floats' do
100
100
  coordinates = Sunspot::Util::Coordinates.new(
101
- BigDecimal.new('40.7'), BigDecimal.new('-73.5'))
101
+ BigDecimal('40.7'), BigDecimal('-73.5'))
102
102
  session.index(post(:coordinates => coordinates))
103
103
  expect(connection).to have_add_with(:coordinates_s => 'dr5xx3nytvgs')
104
104
  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
@@ -10,7 +10,7 @@ shared_examples_for 'geohash query' do
10
10
 
11
11
  it 'searches for nearby points with non-Float arguments' do
12
12
  search do
13
- with(:coordinates).near(BigDecimal.new('40.7'), BigDecimal.new('-73.5'))
13
+ with(:coordinates).near(BigDecimal('40.7'), BigDecimal('-73.5'))
14
14
  end
15
15
  expect(connection).to have_last_search_including(:q, build_geo_query)
16
16
  end
@@ -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)
@@ -1,12 +1,12 @@
1
1
  describe 'typed query' do
2
2
  it 'properly escapes namespaced type names' do
3
3
  session.search(Namespaced::Comment)
4
- expect(connection).to have_last_search_with(:fq => ['type:Namespaced\:\:Comment'])
4
+ expect(connection).to have_last_search_with(:fq => ['type:"Namespaced\:\:Comment"'])
5
5
  end
6
6
 
7
7
  it 'builds search for multiple types' do
8
8
  session.search(Post, Namespaced::Comment)
9
- expect(connection).to have_last_search_with(:fq => ['type:(Post OR Namespaced\:\:Comment)'])
9
+ expect(connection).to have_last_search_with(:fq => ['type:(Post OR "Namespaced\:\:Comment")'])
10
10
  end
11
11
 
12
12
  it 'searches type of subclass when superclass is configured' do
@@ -38,7 +38,7 @@ describe Sunspot::SessionProxy::MasterSlaveSessionProxy do
38
38
  end
39
39
 
40
40
  it 'should raise ArgumentError when bogus config specified' do
41
- expect { @proxy.config(:bogus) }.to raise_error
41
+ expect { @proxy.config(:bogus) }.to raise_error(ArgumentError)
42
42
  end
43
43
 
44
44
  it_should_behave_like 'session proxy'
@@ -1,7 +1,8 @@
1
1
  require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+ require 'uri'
2
3
 
3
4
  describe Sunspot::SessionProxy::Retry5xxSessionProxy do
4
-
5
+
5
6
  before :each do
6
7
  Sunspot::Session.connection_class = Mock::ConnectionFactory.new
7
8
  @sunspot_session = Sunspot.session
@@ -21,11 +22,14 @@ describe Sunspot::SessionProxy::Retry5xxSessionProxy do
21
22
  end
22
23
 
23
24
  let :fake_rsolr_request do
24
- {:uri => 'http://solr.test/uri'}
25
+ {:uri => URI('http://solr.test/uri')}
25
26
  end
26
27
 
27
28
  def fake_rsolr_response(status)
28
- {:status => status.to_s}
29
+ {
30
+ :status => status.to_s,
31
+ :body => ''
32
+ }
29
33
  end
30
34
 
31
35
  let :post do
@@ -67,12 +71,12 @@ describe Sunspot::SessionProxy::Retry5xxSessionProxy do
67
71
  it "should not retry a 4xx" do
68
72
  e = FakeRSolrErrorHttp.new(fake_rsolr_request, fake_rsolr_response(400))
69
73
  expect(@sunspot_session).to receive(:index).and_raise(e)
70
- expect { Sunspot.index(post) }.to raise_error
74
+ expect { Sunspot.index(post) }.to raise_error(FakeRSolrErrorHttp)
71
75
  end
72
76
 
73
77
  # TODO: try against more than just Sunspot.index? but that's just testing the
74
78
  # invocation of delegate, so probably not important. -nz 11Apr12
75
79
 
76
80
  it_should_behave_like 'session proxy'
77
-
81
+
78
82
  end
@@ -109,7 +109,7 @@ describe 'Session' do
109
109
  it 'should open a connection with custom read timeout' do
110
110
  Sunspot.config.solr.read_timeout = 0.5
111
111
  Sunspot.commit
112
- expect(connection.opts[:read_timeout]).to eq(0.5)
112
+ expect(connection.opts[:timeout]).to eq(0.5)
113
113
  end
114
114
 
115
115
  it 'should open a connection with custom open timeout' do
@@ -0,0 +1,99 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ describe Sunspot::Setup do
4
+ context '#id_prefix_for_class' do
5
+ subject { Sunspot::Setup.for(clazz).id_prefix_for_class }
6
+
7
+ context 'when `id_prefix` is defined on model' do
8
+ context 'as Proc' do
9
+ let(:clazz) { PostWithProcPrefixId }
10
+
11
+ it 'returns nil' do
12
+ is_expected.to be_nil
13
+ end
14
+ end
15
+
16
+ context 'as Symbol' do
17
+ let(:clazz) { PostWithSymbolPrefixId }
18
+
19
+ it 'returns nil' do
20
+ is_expected.to be_nil
21
+ end
22
+ end
23
+
24
+ context 'as String' do
25
+ let(:clazz) { PostWithStringPrefixId }
26
+
27
+ it 'returns `id_prefix` value' do
28
+ is_expected.to eq('USERDATA!')
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'when `id_prefix` is not defined on model' do
34
+ let(:clazz) { PostWithoutPrefixId }
35
+
36
+ it 'returns nil' do
37
+ is_expected.to be_nil
38
+ end
39
+ end
40
+ end
41
+
42
+ context '#id_prefix_defined?' do
43
+ subject { Sunspot::Setup.for(clazz).id_prefix_defined? }
44
+
45
+ context 'when `id_prefix` is defined on model' do
46
+ let(:clazz) { PostWithProcPrefixId }
47
+
48
+ it 'returns true' do
49
+ is_expected.to be_truthy
50
+ end
51
+ end
52
+
53
+ context 'when `id_prefix` is not defined on model' do
54
+ let(:clazz) { PostWithoutPrefixId }
55
+
56
+ it 'returns false' do
57
+ is_expected.to be_falsey
58
+ end
59
+ end
60
+ end
61
+
62
+ context '#id_prefix_requires_instance?' do
63
+ subject { Sunspot::Setup.for(clazz).id_prefix_requires_instance? }
64
+
65
+ context 'when `id_prefix` is defined on model' do
66
+ context 'as Proc' do
67
+ let(:clazz) { PostWithProcPrefixId }
68
+
69
+ it 'returns true' do
70
+ is_expected.to be_truthy
71
+ end
72
+ end
73
+
74
+ context 'as Symbol' do
75
+ let(:clazz) { PostWithSymbolPrefixId }
76
+
77
+ it 'returns true' do
78
+ is_expected.to be_truthy
79
+ end
80
+ end
81
+
82
+ context 'as String' do
83
+ let(:clazz) { PostWithStringPrefixId }
84
+
85
+ it 'returns false' do
86
+ is_expected.to be_falsey
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'when `id_prefix` is not defined on model' do
92
+ let(:clazz) { PostWithoutPrefixId }
93
+
94
+ it 'returns false' do
95
+ is_expected.to be_falsey
96
+ end
97
+ end
98
+ end
99
+ end
@@ -14,4 +14,26 @@ module IndexerHelper
14
14
  def values_in_last_document_for(field_name)
15
15
  @connection.adds.last.last.fields_by_name(field_name).map { |field| field.value }
16
16
  end
17
+
18
+ def index_post(post)
19
+ Sunspot.index!(post)
20
+ hit = find_post(post)
21
+ expect(hit).not_to be_nil
22
+ hit
23
+ end
24
+
25
+ def find_post(post)
26
+ Sunspot.search(clazz).hits.find { |h| h.primary_key.to_i == post.id && h.id_prefix == id_prefix_value(post, id_prefix) }
27
+ end
28
+
29
+ def id_prefix_value(post, id_prefix)
30
+ return unless id_prefix
31
+ return id_prefix if id_prefix.is_a?(String)
32
+
33
+ id_prefix.call(post)
34
+ end
35
+
36
+ def post_solr_id
37
+ "#{id_prefix_value(post, id_prefix)}#{clazz} #{post.id}"
38
+ end
17
39
  end