chewy 7.2.4 → 7.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/dependabot.yml +42 -0
  4. data/.github/workflows/ruby.yml +26 -32
  5. data/.rubocop.yml +4 -1
  6. data/CHANGELOG.md +144 -0
  7. data/Gemfile +4 -4
  8. data/README.md +165 -10
  9. data/chewy.gemspec +4 -17
  10. data/gemfiles/base.gemfile +12 -0
  11. data/gemfiles/rails.6.1.activerecord.gemfile +2 -1
  12. data/gemfiles/rails.7.0.activerecord.gemfile +2 -1
  13. data/gemfiles/{rails.5.2.activerecord.gemfile → rails.7.1.activerecord.gemfile} +6 -3
  14. data/lib/chewy/config.rb +22 -14
  15. data/lib/chewy/elastic_client.rb +31 -0
  16. data/lib/chewy/errors.rb +5 -2
  17. data/lib/chewy/fields/base.rb +1 -1
  18. data/lib/chewy/fields/root.rb +1 -1
  19. data/lib/chewy/index/adapter/active_record.rb +13 -3
  20. data/lib/chewy/index/adapter/object.rb +3 -3
  21. data/lib/chewy/index/adapter/orm.rb +2 -2
  22. data/lib/chewy/index/crutch.rb +15 -7
  23. data/lib/chewy/index/import/bulk_builder.rb +6 -7
  24. data/lib/chewy/index/import/routine.rb +1 -1
  25. data/lib/chewy/index/import.rb +31 -4
  26. data/lib/chewy/index/observe/active_record_methods.rb +87 -0
  27. data/lib/chewy/index/observe/callback.rb +34 -0
  28. data/lib/chewy/index/observe.rb +3 -58
  29. data/lib/chewy/index/syncer.rb +1 -1
  30. data/lib/chewy/index.rb +25 -0
  31. data/lib/chewy/journal.rb +17 -6
  32. data/lib/chewy/log_subscriber.rb +5 -1
  33. data/lib/chewy/minitest/helpers.rb +1 -1
  34. data/lib/chewy/minitest/search_index_receiver.rb +3 -1
  35. data/lib/chewy/rake_helper.rb +74 -13
  36. data/lib/chewy/rspec/update_index.rb +13 -6
  37. data/lib/chewy/runtime/version.rb +1 -1
  38. data/lib/chewy/search/parameters/collapse.rb +16 -0
  39. data/lib/chewy/search/parameters/indices.rb +1 -1
  40. data/lib/chewy/search/parameters/knn.rb +16 -0
  41. data/lib/chewy/search/parameters/storage.rb +1 -1
  42. data/lib/chewy/search/parameters.rb +3 -3
  43. data/lib/chewy/search/request.rb +45 -11
  44. data/lib/chewy/search.rb +6 -3
  45. data/lib/chewy/stash.rb +3 -3
  46. data/lib/chewy/strategy/atomic_no_refresh.rb +18 -0
  47. data/lib/chewy/strategy/base.rb +10 -0
  48. data/lib/chewy/strategy/delayed_sidekiq/scheduler.rb +168 -0
  49. data/lib/chewy/strategy/delayed_sidekiq/worker.rb +76 -0
  50. data/lib/chewy/strategy/delayed_sidekiq.rb +30 -0
  51. data/lib/chewy/strategy/lazy_sidekiq.rb +64 -0
  52. data/lib/chewy/strategy.rb +3 -0
  53. data/lib/chewy/version.rb +1 -1
  54. data/lib/chewy.rb +5 -8
  55. data/lib/tasks/chewy.rake +17 -1
  56. data/migration_guide.md +1 -1
  57. data/spec/chewy/config_spec.rb +2 -2
  58. data/spec/chewy/elastic_client_spec.rb +26 -0
  59. data/spec/chewy/fields/base_spec.rb +1 -0
  60. data/spec/chewy/index/actions_spec.rb +4 -4
  61. data/spec/chewy/index/adapter/active_record_spec.rb +62 -0
  62. data/spec/chewy/index/import/bulk_builder_spec.rb +7 -3
  63. data/spec/chewy/index/import_spec.rb +16 -3
  64. data/spec/chewy/index/observe/active_record_methods_spec.rb +68 -0
  65. data/spec/chewy/index/observe/callback_spec.rb +139 -0
  66. data/spec/chewy/index/observe_spec.rb +27 -0
  67. data/spec/chewy/journal_spec.rb +13 -49
  68. data/spec/chewy/minitest/helpers_spec.rb +3 -3
  69. data/spec/chewy/minitest/search_index_receiver_spec.rb +6 -4
  70. data/spec/chewy/rake_helper_spec.rb +155 -4
  71. data/spec/chewy/rspec/helpers_spec.rb +1 -1
  72. data/spec/chewy/search/pagination/kaminari_examples.rb +1 -1
  73. data/spec/chewy/search/pagination/kaminari_spec.rb +1 -1
  74. data/spec/chewy/search/parameters/collapse_spec.rb +5 -0
  75. data/spec/chewy/search/parameters/knn_spec.rb +5 -0
  76. data/spec/chewy/search/request_spec.rb +37 -0
  77. data/spec/chewy/search_spec.rb +9 -0
  78. data/spec/chewy/strategy/active_job_spec.rb +8 -8
  79. data/spec/chewy/strategy/atomic_no_refresh_spec.rb +60 -0
  80. data/spec/chewy/strategy/delayed_sidekiq_spec.rb +208 -0
  81. data/spec/chewy/strategy/lazy_sidekiq_spec.rb +214 -0
  82. data/spec/chewy/strategy/sidekiq_spec.rb +4 -4
  83. data/spec/chewy_spec.rb +7 -4
  84. data/spec/spec_helper.rb +1 -1
  85. metadata +32 -253
  86. data/gemfiles/rails.6.0.activerecord.gemfile +0 -11
@@ -314,6 +314,18 @@ describe Chewy::Search::Request do
314
314
  end
315
315
  end
316
316
 
317
+ %i[collapse knn].each do |name|
318
+ describe "##{name}" do
319
+ specify { expect(subject.send(name, foo: {bar: 42}).render[:body]).to include(name => {'foo' => {bar: 42}}) }
320
+ specify do
321
+ expect(subject.send(name, foo: {bar: 42}).send(name, moo: {baz: 43}).render[:body])
322
+ .to include(name => {'moo' => {baz: 43}})
323
+ end
324
+ specify { expect(subject.send(name, foo: {bar: 42}).send(name, nil).render[:body]).to be_blank }
325
+ specify { expect { subject.send(name, foo: {bar: 42}) }.not_to change { subject.render } }
326
+ end
327
+ end
328
+
317
329
  describe '#docvalue_fields' do
318
330
  specify { expect(subject.docvalue_fields(:foo).render[:body]).to include(docvalue_fields: ['foo']) }
319
331
  specify do
@@ -807,6 +819,31 @@ describe Chewy::Search::Request do
807
819
  request: {index: ['products'], body: {query: {match: {name: 'name3'}}}, refresh: false}
808
820
  )
809
821
  end
822
+
823
+ it 'delete records asynchronously' do
824
+ outer_payload = nil
825
+ ActiveSupport::Notifications.subscribe('delete_query.chewy') do |_name, _start, _finish, _id, payload|
826
+ outer_payload = payload
827
+ end
828
+ subject.query(match: {name: 'name3'}).delete_all(
829
+ refresh: false,
830
+ wait_for_completion: false,
831
+ requests_per_second: 10.0,
832
+ scroll_size: 2000
833
+ )
834
+ expect(outer_payload).to eq(
835
+ index: ProductsIndex,
836
+ indexes: [ProductsIndex],
837
+ request: {
838
+ index: ['products'],
839
+ body: {query: {match: {name: 'name3'}}},
840
+ refresh: false,
841
+ wait_for_completion: false,
842
+ requests_per_second: 10.0,
843
+ scroll_size: 2000
844
+ }
845
+ )
846
+ end
810
847
  end
811
848
 
812
849
  describe '#response=' do
@@ -48,6 +48,10 @@ describe Chewy::Search do
48
48
  filter { match name: "Name#{index}" }
49
49
  end
50
50
 
51
+ def self.by_rating_with_kwargs(value, options:) # rubocop:disable Lint/UnusedMethodArgument
52
+ filter { match rating: value }
53
+ end
54
+
51
55
  index_scope City
52
56
  field :name, type: 'keyword'
53
57
  field :rating, type: :integer
@@ -114,5 +118,10 @@ describe Chewy::Search do
114
118
  specify { expect(CountriesIndex.by_rating(3).by_name(5).map(&:class)).to eq([CountriesIndex]) }
115
119
  specify { expect(CountriesIndex.order(:name).by_rating(3).map(&:rating)).to eq([3]) }
116
120
  specify { expect(CountriesIndex.order(:name).by_rating(3).map(&:class)).to eq([CountriesIndex]) }
121
+
122
+ specify 'supports keyword arguments' do
123
+ expect(CitiesIndex.by_rating_with_kwargs(3, options: 'blah blah blah').map(&:rating)).to eq([3])
124
+ expect(CitiesIndex.order(:name).by_rating_with_kwargs(3, options: 'blah blah blah').map(&:rating)).to eq([3])
125
+ end
117
126
  end
118
127
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- if defined?(::ActiveJob)
3
+ if defined?(ActiveJob)
4
4
  describe Chewy::Strategy::ActiveJob do
5
5
  around do |example|
6
6
  active_job_settings = Chewy.settings[:active_job]
@@ -9,12 +9,12 @@ if defined?(::ActiveJob)
9
9
  Chewy.settings[:active_job] = active_job_settings
10
10
  end
11
11
  before(:all) do
12
- ::ActiveJob::Base.logger = Chewy.logger
12
+ ActiveJob::Base.logger = Chewy.logger
13
13
  end
14
14
  before do
15
- ::ActiveJob::Base.queue_adapter = :test
16
- ::ActiveJob::Base.queue_adapter.enqueued_jobs.clear
17
- ::ActiveJob::Base.queue_adapter.performed_jobs.clear
15
+ ActiveJob::Base.queue_adapter = :test
16
+ ActiveJob::Base.queue_adapter.enqueued_jobs.clear
17
+ ActiveJob::Base.queue_adapter.performed_jobs.clear
18
18
  end
19
19
 
20
20
  before do
@@ -39,7 +39,7 @@ if defined?(::ActiveJob)
39
39
  Chewy.strategy(:active_job) do
40
40
  [city, other_city].map(&:save!)
41
41
  end
42
- enqueued_job = ::ActiveJob::Base.queue_adapter.enqueued_jobs.first
42
+ enqueued_job = ActiveJob::Base.queue_adapter.enqueued_jobs.first
43
43
  expect(enqueued_job[:job]).to eq(Chewy::Strategy::ActiveJob::Worker)
44
44
  expect(enqueued_job[:queue]).to eq('low')
45
45
  end
@@ -48,12 +48,12 @@ if defined?(::ActiveJob)
48
48
  Chewy.strategy(:active_job) do
49
49
  [city, other_city].map(&:save!)
50
50
  end
51
- enqueued_job = ::ActiveJob::Base.queue_adapter.enqueued_jobs.first
51
+ enqueued_job = ActiveJob::Base.queue_adapter.enqueued_jobs.first
52
52
  expect(enqueued_job[:queue]).to eq('low')
53
53
  end
54
54
 
55
55
  specify do
56
- ::ActiveJob::Base.queue_adapter = :inline
56
+ ActiveJob::Base.queue_adapter = :inline
57
57
  expect { [city, other_city].map(&:save!) }
58
58
  .to update_index(CitiesIndex, strategy: :active_job)
59
59
  .and_reindex(city, other_city).only
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Strategy::AtomicNoRefresh, :orm do
4
+ around { |example| Chewy.strategy(:bypass) { example.run } }
5
+
6
+ before do
7
+ stub_model(:country) do
8
+ update_index('countries') { self }
9
+ end
10
+
11
+ stub_index(:countries) do
12
+ index_scope Country
13
+ end
14
+ end
15
+
16
+ let(:country) { Country.create!(name: 'hello', country_code: 'HL') }
17
+ let(:other_country) { Country.create!(name: 'world', country_code: 'WD') }
18
+
19
+ specify do
20
+ expect { [country, other_country].map(&:save!) }
21
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
22
+ .and_reindex(country, other_country).only.no_refresh
23
+ end
24
+
25
+ specify do
26
+ expect { [country, other_country].map(&:destroy) }
27
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
28
+ .and_delete(country, other_country).only.no_refresh
29
+ end
30
+
31
+ context do
32
+ before do
33
+ stub_index(:countries) do
34
+ index_scope Country
35
+ root id: -> { country_code }
36
+ end
37
+ end
38
+
39
+ specify do
40
+ expect { [country, other_country].map(&:save!) }
41
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
42
+ .and_reindex('HL', 'WD').only.no_refresh
43
+ end
44
+
45
+ specify do
46
+ expect { [country, other_country].map(&:destroy) }
47
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
48
+ .and_delete('HL', 'WD').only.no_refresh
49
+ end
50
+
51
+ specify do
52
+ expect do
53
+ country.save!
54
+ other_country.destroy
55
+ end
56
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
57
+ .and_reindex('HL').and_delete('WD').only.no_refresh
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,208 @@
1
+ require 'spec_helper'
2
+
3
+ if defined?(Sidekiq)
4
+ require 'sidekiq/testing'
5
+ require 'redis'
6
+
7
+ describe Chewy::Strategy::DelayedSidekiq do
8
+ around do |example|
9
+ Chewy.strategy(:bypass) { example.run }
10
+ end
11
+
12
+ before do
13
+ redis = Redis.new
14
+ allow(Sidekiq).to receive(:redis).and_yield(redis)
15
+ Sidekiq::Worker.clear_all
16
+ described_class.clear_timechunks!
17
+ end
18
+
19
+ before do
20
+ stub_model(:city) do
21
+ update_index('cities') { self }
22
+ end
23
+
24
+ stub_index(:cities) do
25
+ index_scope City
26
+ end
27
+ end
28
+
29
+ let(:city) { City.create!(name: 'hello') }
30
+ let(:other_city) { City.create!(name: 'world') }
31
+
32
+ it 'does not trigger immediate reindex due to it`s async nature' do
33
+ expect { [city, other_city].map(&:save!) }
34
+ .not_to update_index(CitiesIndex, strategy: :delayed_sidekiq)
35
+ end
36
+
37
+ it "respects 'refresh: false' options" do
38
+ allow(Chewy).to receive(:disable_refresh_async).and_return(true)
39
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id]), refresh: false)
40
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id, other_city.id])
41
+ scheduler.postpone
42
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
43
+ end
44
+
45
+ context 'with default config' do
46
+ it 'does schedule a job that triggers reindex with default options' do
47
+ Timecop.freeze do
48
+ expect(Sidekiq::Client).to receive(:push).with(
49
+ hash_including(
50
+ 'queue' => 'chewy',
51
+ 'at' => expected_at_time.to_i,
52
+ 'class' => Chewy::Strategy::DelayedSidekiq::Worker,
53
+ 'args' => ['CitiesIndex', an_instance_of(Integer)]
54
+ )
55
+ ).and_call_original
56
+
57
+ expect($stdout).not_to receive(:puts)
58
+
59
+ Sidekiq::Testing.inline! do
60
+ expect { [city, other_city].map(&:save!) }
61
+ .to update_index(CitiesIndex, strategy: :delayed_sidekiq)
62
+ .and_reindex(city, other_city).only
63
+ end
64
+ end
65
+ end
66
+
67
+ def expected_at_time
68
+ target = described_class::Scheduler::DEFAULT_LATENCY.seconds.from_now.to_i
69
+ target - (target % described_class::Scheduler::DEFAULT_LATENCY) + described_class::Scheduler::DEFAULT_MARGIN.seconds
70
+ end
71
+ end
72
+
73
+ context 'with custom config' do
74
+ before do
75
+ CitiesIndex.strategy_config(
76
+ delayed_sidekiq: {
77
+ reindex_wrapper: lambda { |&reindex|
78
+ puts 'hello'
79
+ reindex.call
80
+ },
81
+ margin: 5,
82
+ latency: 60
83
+ }
84
+ )
85
+ end
86
+
87
+ it 'respects :strategy_config options' do
88
+ Timecop.freeze do
89
+ expect(Sidekiq::Client).to receive(:push).with(
90
+ hash_including(
91
+ 'queue' => 'chewy',
92
+ 'at' => (60.seconds.from_now.change(sec: 0) + 5.seconds).to_i,
93
+ 'class' => Chewy::Strategy::DelayedSidekiq::Worker,
94
+ 'args' => ['CitiesIndex', an_instance_of(Integer)]
95
+ )
96
+ ).and_call_original
97
+
98
+ expect($stdout).to receive(:puts).with('hello') # check that reindex_wrapper works
99
+
100
+ Sidekiq::Testing.inline! do
101
+ expect { [city, other_city].map(&:save!) }
102
+ .to update_index(CitiesIndex, strategy: :delayed_sidekiq)
103
+ .and_reindex(city, other_city).only
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'two reindex call within the timewindow' do
110
+ it 'accumulates all ids does the reindex one time' do
111
+ Timecop.freeze do
112
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id])).once
113
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id])
114
+ scheduler.postpone
115
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id])
116
+ scheduler.postpone
117
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
118
+ end
119
+ end
120
+
121
+ context 'one call with update_fields another one without update_fields' do
122
+ it 'does reindex of all fields' do
123
+ Timecop.freeze do
124
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id])).once
125
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id], update_fields: ['name'])
126
+ scheduler.postpone
127
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id])
128
+ scheduler.postpone
129
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
130
+ end
131
+ end
132
+ end
133
+
134
+ context 'both calls with different update fields' do
135
+ it 'deos reindex with union of fields' do
136
+ Timecop.freeze do
137
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id]), update_fields: %w[name description]).once
138
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id], update_fields: ['name'])
139
+ scheduler.postpone
140
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id], update_fields: ['description'])
141
+ scheduler.postpone
142
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'two calls within different timewindows' do
149
+ it 'does two separate reindexes' do
150
+ Timecop.freeze do
151
+ expect(CitiesIndex).to receive(:import!).with([city.id]).once
152
+ expect(CitiesIndex).to receive(:import!).with([other_city.id]).once
153
+ Timecop.travel(20.seconds.ago) do
154
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id])
155
+ scheduler.postpone
156
+ end
157
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id])
158
+ scheduler.postpone
159
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
160
+ end
161
+ end
162
+ end
163
+
164
+ context 'first call has update_fields' do
165
+ it 'does first reindex with the expected update_fields and second without update_fields' do
166
+ Timecop.freeze do
167
+ expect(CitiesIndex).to receive(:import!).with([city.id], update_fields: ['name']).once
168
+ expect(CitiesIndex).to receive(:import!).with([other_city.id]).once
169
+ Timecop.travel(20.seconds.ago) do
170
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id], update_fields: ['name'])
171
+ scheduler.postpone
172
+ end
173
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id])
174
+ scheduler.postpone
175
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
176
+ end
177
+ end
178
+ end
179
+
180
+ context 'both calls have update_fields option' do
181
+ it 'does both reindexes with their expected update_fields option' do
182
+ Timecop.freeze do
183
+ expect(CitiesIndex).to receive(:import!).with([city.id], update_fields: ['name']).once
184
+ expect(CitiesIndex).to receive(:import!).with([other_city.id], update_fields: ['description']).once
185
+ Timecop.travel(20.seconds.ago) do
186
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id], update_fields: ['name'])
187
+ scheduler.postpone
188
+ end
189
+ scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id], update_fields: ['description'])
190
+ scheduler.postpone
191
+ Chewy::Strategy::DelayedSidekiq::Worker.drain
192
+ end
193
+ end
194
+ end
195
+
196
+ describe '#clear_delayed_sidekiq_timechunks test helper' do
197
+ it 'clears redis from the timechunk sorted sets to avoid leak between tests' do
198
+ timechunks_set = -> { Sidekiq.redis { |redis| redis.zrange('chewy:delayed_sidekiq:CitiesIndex:timechunks', 0, -1) } }
199
+
200
+ expect { CitiesIndex.import!([1], strategy: :delayed_sidekiq) }
201
+ .to change { timechunks_set.call.size }.by(1)
202
+
203
+ expect { Chewy::Strategy::DelayedSidekiq.clear_timechunks! }
204
+ .to change { timechunks_set.call.size }.to(0)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,214 @@
1
+ require 'spec_helper'
2
+
3
+ if defined?(Sidekiq)
4
+ require 'sidekiq/testing'
5
+
6
+ describe Chewy::Strategy::LazySidekiq do
7
+ around do |example|
8
+ sidekiq_settings = Chewy.settings[:sidekiq]
9
+ Chewy.settings[:sidekiq] = {queue: 'low'}
10
+ Chewy.strategy(:bypass) { example.run }
11
+ Chewy.settings[:sidekiq] = sidekiq_settings
12
+ end
13
+ before { Sidekiq::Worker.clear_all }
14
+
15
+ context 'strategy' do
16
+ before do
17
+ stub_model(:city) do
18
+ update_index('cities') { self }
19
+ end
20
+
21
+ stub_index(:cities) do
22
+ index_scope City
23
+ end
24
+ end
25
+
26
+ let(:city) { City.create!(name: 'hello') }
27
+ let(:other_city) { City.create!(name: 'world') }
28
+
29
+ it 'does not update indices synchronously' do
30
+ expect { [city, other_city].map(&:save!) }
31
+ .not_to update_index(CitiesIndex, strategy: :lazy_sidekiq)
32
+ end
33
+
34
+ it 'updates indices asynchronously on record save' do
35
+ expect(Sidekiq::Client).to receive(:push)
36
+ .with(hash_including(
37
+ 'class' => Chewy::Strategy::LazySidekiq::IndicesUpdateWorker,
38
+ 'queue' => 'low'
39
+ ))
40
+ .and_call_original
41
+ .once
42
+ Sidekiq::Testing.inline! do
43
+ expect { [city, other_city].map(&:save!) }
44
+ .to update_index(CitiesIndex, strategy: :lazy_sidekiq)
45
+ .and_reindex(city, other_city).only
46
+ end
47
+ end
48
+
49
+ it 'updates indices asynchronously with falling back to sidekiq strategy on record destroy' do
50
+ expect(Sidekiq::Client).not_to receive(:push)
51
+ .with(hash_including(
52
+ 'class' => Chewy::Strategy::LazySidekiq::IndicesUpdateWorker,
53
+ 'queue' => 'low'
54
+ ))
55
+ expect(Sidekiq::Client).to receive(:push)
56
+ .with(hash_including(
57
+ 'class' => Chewy::Strategy::Sidekiq::Worker,
58
+ 'queue' => 'low',
59
+ 'args' => ['CitiesIndex', [city.id, other_city.id]]
60
+ ))
61
+ .and_call_original
62
+ .once
63
+ Sidekiq::Testing.inline! do
64
+ expect { [city, other_city].map(&:destroy) }.to update_index(CitiesIndex, strategy: :sidekiq)
65
+ end
66
+ end
67
+
68
+ it 'calls Index#import!' do
69
+ allow(City).to receive(:where).with(id: [city.id, other_city.id]).and_return([city, other_city])
70
+ expect(city).to receive(:run_chewy_callbacks).and_call_original
71
+ expect(other_city).to receive(:run_chewy_callbacks).and_call_original
72
+
73
+ expect do
74
+ Sidekiq::Testing.inline! do
75
+ Chewy::Strategy::LazySidekiq::IndicesUpdateWorker.new.perform({'City' => [city.id, other_city.id]})
76
+ end
77
+ end.to update_index(CitiesIndex).and_reindex(city, other_city).only
78
+ end
79
+
80
+ context 'when Chewy.disable_refresh_async is true' do
81
+ before do
82
+ allow(Chewy).to receive(:disable_refresh_async).and_return(true)
83
+ end
84
+
85
+ it 'calls Index#import! with refresh false' do
86
+ allow(City).to receive(:where).with(id: [city.id, other_city.id]).and_return([city, other_city])
87
+ expect(city).to receive(:run_chewy_callbacks).and_call_original
88
+ expect(other_city).to receive(:run_chewy_callbacks).and_call_original
89
+
90
+ expect do
91
+ Sidekiq::Testing.inline! do
92
+ Chewy::Strategy::LazySidekiq::IndicesUpdateWorker.new.perform({'City' => [city.id, other_city.id]})
93
+ end
94
+ end.to update_index(CitiesIndex).and_reindex(city, other_city).only.no_refresh
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'integration' do
100
+ around { |example| Sidekiq::Testing.inline! { example.run } }
101
+
102
+ let(:update_condition) { true }
103
+
104
+ before do
105
+ city_model
106
+ country_model
107
+
108
+ City.belongs_to :country
109
+ Country.has_many :cities
110
+
111
+ stub_index(:cities) do
112
+ index_scope City
113
+ end
114
+
115
+ stub_index(:countries) do
116
+ index_scope Country
117
+ end
118
+ end
119
+
120
+ context 'state dependent' do
121
+ let(:city_model) do
122
+ stub_model(:city) do
123
+ update_index(-> { 'cities' }, :self)
124
+ update_index('countries') { changes['country_id'] || previous_changes['country_id'] || country }
125
+ end
126
+ end
127
+
128
+ let(:country_model) do
129
+ stub_model(:country) do
130
+ update_index('cities', if: -> { state_dependent_update_condition }) { cities }
131
+ update_index(-> { 'countries' }, :self)
132
+ attr_accessor :state_dependent_update_condition
133
+ end
134
+ end
135
+
136
+ context 'city updates' do
137
+ let!(:country1) { Country.create!(id: 1) }
138
+ let!(:country2) { Country.create!(id: 2) }
139
+ let!(:city) { City.create!(id: 1, country: country1) }
140
+
141
+ it 'does not update index of removed entity because model state on the moment of save cannot be fetched' do
142
+ expect { city.update!(country: nil) }.not_to update_index('countries', strategy: :lazy_sidekiq)
143
+ end
144
+ it 'does not update index of removed entity because model state on the moment of save cannot be fetched' do
145
+ expect { city.update!(country: country2) }.to update_index('countries', strategy: :lazy_sidekiq).and_reindex(country2).only
146
+ end
147
+ end
148
+
149
+ context 'country updates' do
150
+ let!(:country) do
151
+ cities = Array.new(2) { |i| City.create!(id: i) }
152
+ Country.create!(id: 1, cities: cities, state_dependent_update_condition: update_condition)
153
+ end
154
+
155
+ it 'does not update index because state of attribute cannot be fetched' do
156
+ expect { country.save! }.not_to update_index('cities', strategy: :lazy_sidekiq)
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'state independent' do
162
+ let(:city_model) do
163
+ stub_model(:city) do
164
+ update_index(-> { 'cities' }, :self)
165
+ update_index('countries') { country }
166
+ end
167
+ end
168
+
169
+ let(:country_model) do
170
+ stub_model(:country) do
171
+ update_index('cities', if: -> { state_independent_update_condition }) { cities }
172
+ update_index(-> { 'countries' }, :self)
173
+ end
174
+ end
175
+
176
+ before do
177
+ allow_any_instance_of(Country).to receive(:state_independent_update_condition).and_return(update_condition)
178
+ end
179
+
180
+ context 'when city updates' do
181
+ let!(:country1) { Country.create!(id: 1) }
182
+ let!(:country2) { Country.create!(id: 2) }
183
+ let!(:city) { City.create!(id: 1, country: country1) }
184
+
185
+ specify { expect { city.save! }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(city).only }
186
+ specify { expect { city.save! }.to update_index('countries', strategy: :lazy_sidekiq).and_reindex(country1).only }
187
+
188
+ specify { expect { city.destroy }.not_to update_index('cities').and_reindex(city).only }
189
+ specify { expect { city.destroy }.to update_index('countries', strategy: :sidekiq).and_reindex(country1).only }
190
+
191
+ specify { expect { city.update!(country: nil) }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(city).only }
192
+ specify { expect { city.update!(country: country2) }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(city).only }
193
+ end
194
+
195
+ context 'when country updates' do
196
+ let!(:country) do
197
+ cities = Array.new(2) { |i| City.create!(id: i) }
198
+ Country.create!(id: 1, cities: cities)
199
+ end
200
+ specify { expect { country.save! }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(country.cities).only }
201
+ specify { expect { country.save! }.to update_index('countries', strategy: :lazy_sidekiq).and_reindex(country).only }
202
+
203
+ specify { expect { country.destroy }.to update_index('cities', strategy: :sidekiq).and_reindex(country.cities).only }
204
+ specify { expect { country.destroy }.not_to update_index('countries').and_reindex(country).only }
205
+
206
+ context 'when update condition is false' do
207
+ let(:update_condition) { false }
208
+ specify { expect { country.save! }.not_to update_index('cities', strategy: :lazy_sidekiq) }
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- if defined?(::Sidekiq)
3
+ if defined?(Sidekiq)
4
4
  require 'sidekiq/testing'
5
5
 
6
6
  describe Chewy::Strategy::Sidekiq do
@@ -10,7 +10,7 @@ if defined?(::Sidekiq)
10
10
  Chewy.strategy(:bypass) { example.run }
11
11
  Chewy.settings[:sidekiq] = sidekiq_settings
12
12
  end
13
- before { ::Sidekiq::Worker.clear_all }
13
+ before { Sidekiq::Worker.clear_all }
14
14
  before do
15
15
  stub_model(:city) do
16
16
  update_index('cities') { self }
@@ -30,8 +30,8 @@ if defined?(::Sidekiq)
30
30
  end
31
31
 
32
32
  specify do
33
- expect(::Sidekiq::Client).to receive(:push).with(hash_including('queue' => 'low')).and_call_original
34
- ::Sidekiq::Testing.inline! do
33
+ expect(Sidekiq::Client).to receive(:push).with(hash_including('queue' => 'low')).and_call_original
34
+ Sidekiq::Testing.inline! do
35
35
  expect { [city, other_city].map(&:save!) }
36
36
  .to update_index(CitiesIndex, strategy: :sidekiq)
37
37
  .and_reindex(city, other_city).only
data/spec/chewy_spec.rb CHANGED
@@ -57,18 +57,21 @@ describe Chewy do
57
57
 
58
58
  before do
59
59
  Chewy.current[:chewy_client] = nil
60
- allow(Chewy).to receive_messages(configuration: {transport_options: {proc: faraday_block}})
60
+ end
61
+
62
+ specify do
63
+ expect(Chewy).to receive_messages(configuration: {transport_options: {proc: faraday_block}})
61
64
 
62
- allow(::Elasticsearch::Client).to receive(:new).with(expected_client_config) do |*_args, &passed_block|
65
+ expect(Elasticsearch::Client).to receive(:new).with(expected_client_config) do |*_args, &passed_block|
63
66
  # RSpec's `with(..., &block)` was used previously, but doesn't actually do
64
67
  # any verification of the passed block (even of its presence).
65
68
  expect(passed_block.source_location).to eq(faraday_block.source_location)
66
69
 
67
70
  mock_client
68
71
  end
69
- end
70
72
 
71
- its(:client) { is_expected.to eq(mock_client) }
73
+ expect(Chewy.client).to be_a(Chewy::ElasticClient)
74
+ end
72
75
 
73
76
  after { Chewy.current[:chewy_client] = initial_client }
74
77
  end
data/spec/spec_helper.rb CHANGED
@@ -9,7 +9,7 @@ require 'rspec/collection_matchers'
9
9
 
10
10
  require 'timecop'
11
11
 
12
- Kaminari::Hooks.init if defined?(::Kaminari::Hooks)
12
+ Kaminari::Hooks.init if defined?(Kaminari::Hooks)
13
13
 
14
14
  require 'support/fail_helpers'
15
15
  require 'support/class_helpers'