chewy 7.2.1 → 7.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) 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 +28 -26
  5. data/.rubocop.yml +4 -1
  6. data/CHANGELOG.md +196 -0
  7. data/Gemfile +4 -3
  8. data/README.md +203 -20
  9. data/chewy.gemspec +4 -18
  10. data/gemfiles/base.gemfile +12 -0
  11. data/gemfiles/rails.6.1.activerecord.gemfile +2 -1
  12. data/gemfiles/{rails.5.2.activerecord.gemfile → rails.7.0.activerecord.gemfile} +6 -3
  13. data/gemfiles/{rails.6.0.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 +11 -2
  17. data/lib/chewy/fields/base.rb +69 -13
  18. data/lib/chewy/fields/root.rb +2 -10
  19. data/lib/chewy/index/actions.rb +11 -16
  20. data/lib/chewy/index/adapter/active_record.rb +18 -3
  21. data/lib/chewy/index/adapter/object.rb +0 -10
  22. data/lib/chewy/index/adapter/orm.rb +4 -14
  23. data/lib/chewy/index/crutch.rb +15 -7
  24. data/lib/chewy/index/import/bulk_builder.rb +219 -32
  25. data/lib/chewy/index/import/bulk_request.rb +1 -1
  26. data/lib/chewy/index/import/routine.rb +3 -3
  27. data/lib/chewy/index/import.rb +45 -31
  28. data/lib/chewy/index/mapping.rb +2 -2
  29. data/lib/chewy/index/observe/active_record_methods.rb +87 -0
  30. data/lib/chewy/index/observe/callback.rb +34 -0
  31. data/lib/chewy/index/observe.rb +3 -58
  32. data/lib/chewy/index/syncer.rb +1 -1
  33. data/lib/chewy/index.rb +25 -0
  34. data/lib/chewy/journal.rb +17 -6
  35. data/lib/chewy/log_subscriber.rb +5 -1
  36. data/lib/chewy/minitest/helpers.rb +77 -0
  37. data/lib/chewy/minitest/search_index_receiver.rb +3 -1
  38. data/lib/chewy/rake_helper.rb +92 -11
  39. data/lib/chewy/rspec/build_query.rb +12 -0
  40. data/lib/chewy/rspec/helpers.rb +55 -0
  41. data/lib/chewy/rspec/update_index.rb +14 -7
  42. data/lib/chewy/rspec.rb +2 -0
  43. data/lib/chewy/runtime/version.rb +1 -1
  44. data/lib/chewy/runtime.rb +1 -1
  45. data/lib/chewy/search/parameters/collapse.rb +16 -0
  46. data/lib/chewy/search/parameters/ignore_unavailable.rb +27 -0
  47. data/lib/chewy/search/parameters/indices.rb +1 -1
  48. data/lib/chewy/search/parameters/knn.rb +16 -0
  49. data/lib/chewy/search/parameters/order.rb +6 -19
  50. data/lib/chewy/search/parameters/storage.rb +1 -1
  51. data/lib/chewy/search/parameters/track_total_hits.rb +16 -0
  52. data/lib/chewy/search/parameters.rb +4 -4
  53. data/lib/chewy/search/request.rb +74 -16
  54. data/lib/chewy/search/scoping.rb +1 -1
  55. data/lib/chewy/search.rb +5 -2
  56. data/lib/chewy/stash.rb +3 -3
  57. data/lib/chewy/strategy/active_job.rb +1 -1
  58. data/lib/chewy/strategy/atomic_no_refresh.rb +18 -0
  59. data/lib/chewy/strategy/base.rb +10 -0
  60. data/lib/chewy/strategy/delayed_sidekiq/scheduler.rb +168 -0
  61. data/lib/chewy/strategy/delayed_sidekiq/worker.rb +76 -0
  62. data/lib/chewy/strategy/delayed_sidekiq.rb +30 -0
  63. data/lib/chewy/strategy/lazy_sidekiq.rb +64 -0
  64. data/lib/chewy/strategy/sidekiq.rb +1 -1
  65. data/lib/chewy/strategy.rb +3 -0
  66. data/lib/chewy/version.rb +1 -1
  67. data/lib/chewy.rb +21 -14
  68. data/lib/tasks/chewy.rake +18 -2
  69. data/migration_guide.md +1 -1
  70. data/spec/chewy/config_spec.rb +2 -2
  71. data/spec/chewy/elastic_client_spec.rb +26 -0
  72. data/spec/chewy/fields/base_spec.rb +39 -18
  73. data/spec/chewy/index/actions_spec.rb +10 -10
  74. data/spec/chewy/index/adapter/active_record_spec.rb +88 -0
  75. data/spec/chewy/index/import/bulk_builder_spec.rb +309 -1
  76. data/spec/chewy/index/import/routine_spec.rb +5 -5
  77. data/spec/chewy/index/import_spec.rb +48 -26
  78. data/spec/chewy/index/observe/active_record_methods_spec.rb +68 -0
  79. data/spec/chewy/index/observe/callback_spec.rb +139 -0
  80. data/spec/chewy/index/observe_spec.rb +27 -0
  81. data/spec/chewy/journal_spec.rb +13 -49
  82. data/spec/chewy/minitest/helpers_spec.rb +111 -1
  83. data/spec/chewy/minitest/search_index_receiver_spec.rb +6 -4
  84. data/spec/chewy/rake_helper_spec.rb +170 -0
  85. data/spec/chewy/rspec/build_query_spec.rb +34 -0
  86. data/spec/chewy/rspec/helpers_spec.rb +61 -0
  87. data/spec/chewy/search/pagination/kaminari_examples.rb +1 -1
  88. data/spec/chewy/search/pagination/kaminari_spec.rb +1 -1
  89. data/spec/chewy/search/parameters/collapse_spec.rb +5 -0
  90. data/spec/chewy/search/parameters/ignore_unavailable_spec.rb +67 -0
  91. data/spec/chewy/search/parameters/knn_spec.rb +5 -0
  92. data/spec/chewy/search/parameters/order_spec.rb +18 -11
  93. data/spec/chewy/search/parameters/track_total_hits_spec.rb +5 -0
  94. data/spec/chewy/search/parameters_spec.rb +6 -1
  95. data/spec/chewy/search/request_spec.rb +58 -9
  96. data/spec/chewy/search_spec.rb +9 -0
  97. data/spec/chewy/strategy/active_job_spec.rb +8 -8
  98. data/spec/chewy/strategy/atomic_no_refresh_spec.rb +60 -0
  99. data/spec/chewy/strategy/delayed_sidekiq_spec.rb +208 -0
  100. data/spec/chewy/strategy/lazy_sidekiq_spec.rb +214 -0
  101. data/spec/chewy/strategy/sidekiq_spec.rb +4 -4
  102. data/spec/chewy_spec.rb +10 -7
  103. data/spec/spec_helper.rb +1 -2
  104. data/spec/support/active_record.rb +8 -1
  105. metadata +45 -264
  106. data/lib/chewy/backports/deep_dup.rb +0 -46
  107. data/lib/chewy/backports/duplicable.rb +0 -91
  108. data/lib/chewy/index/import/thread_safe_progress_bar.rb +0 -40
@@ -44,24 +44,9 @@ describe Chewy::Fields::Base do
44
44
  end
45
45
 
46
46
  context 'parent objects' do
47
- let!(:country) do
48
- described_class.new(:name, value: lambda { |country, crutches|
49
- country.cities.map do |city|
50
- double(districts: city.districts, name: crutches.city_name)
51
- end
52
- })
53
- end
54
- let!(:city) do
55
- described_class.new(:name, value: lambda { |city, country, crutches|
56
- city.districts.map do |district|
57
- [district, country.name, crutches.suffix]
58
- end
59
- })
60
- end
61
- let(:district_value) { ->(district, city, country, crutches) { [district, city.name, country.name, crutches] } }
62
- let!(:district) do
63
- described_class.new(:name, value: district_value)
64
- end
47
+ let!(:country) { described_class.new(:name, value: ->(country, crutches) { country.cities.map { |city| double(districts: city.districts, name: crutches.city_name) } }) }
48
+ let!(:city) { described_class.new(:name, value: ->(city, country, crutches) { city.districts.map { |district| [district, country.name, crutches.suffix] } }) }
49
+ let!(:district) { described_class.new(:name, value: ->(district, city, country, crutches) { [district, city.name, country.name, crutches] }) }
65
50
  let(:crutches) { double(suffix: 'suffix', city_name: 'Bangkok') }
66
51
 
67
52
  before do
@@ -439,6 +424,7 @@ describe Chewy::Fields::Base do
439
424
  ]
440
425
  )
441
426
  end
427
+ specify { expect(CountriesIndex.reset).to eq(true) }
442
428
  end
443
429
 
444
430
  context 'nested fields' do
@@ -556,6 +542,41 @@ describe Chewy::Fields::Base do
556
542
  end
557
543
  end
558
544
 
545
+ context 'join field type' do
546
+ before do
547
+ stub_model(:comment)
548
+ stub_index(:comments) do
549
+ index_scope Comment
550
+ field :id
551
+ field :hierarchy, type: :join, relations: {question: %i[answer comment], answer: :vote, vote: :subvote}, join: {type: :comment_type, id: :commented_id}
552
+ end
553
+ end
554
+
555
+ specify do
556
+ expect(
557
+ CommentsIndex.root.compose(
558
+ {'id' => 1, 'comment_type' => 'question'}
559
+ )
560
+ ).to eq(
561
+ {'id' => 1, 'hierarchy' => 'question'}
562
+ )
563
+
564
+ expect(
565
+ CommentsIndex.root.compose(
566
+ {'id' => 2, 'comment_type' => 'answer', 'commented_id' => 1}
567
+ )
568
+ ).to eq(
569
+ {'id' => 2, 'hierarchy' => {'name' => 'answer', 'parent' => 1}}
570
+ )
571
+
572
+ expect do
573
+ CommentsIndex.root.compose(
574
+ {'id' => 2, 'comment_type' => 'asd', 'commented_id' => 1}
575
+ )
576
+ end.to raise_error Chewy::InvalidJoinFieldType
577
+ end
578
+ end
579
+
559
580
  context 'without ignore_blank option' do
560
581
  before do
561
582
  stub_index(:countries) do
@@ -502,7 +502,7 @@ describe Chewy::Index::Actions do
502
502
  specify do
503
503
  expect(CitiesIndex.client.indices).to receive(:put_settings).with(index: name, body: before_import_body).once
504
504
  expect(CitiesIndex.client.indices).to receive(:put_settings).with(index: name, body: after_import_body).once
505
- expect(CitiesIndex).to receive(:import).with(suffix: suffix, progressbar: false, journal: false, refresh: false).and_call_original
505
+ expect(CitiesIndex).to receive(:import).with(suffix: suffix, journal: false, refresh: false).and_call_original
506
506
  expect(CitiesIndex.reset!(suffix)).to eq(true)
507
507
  end
508
508
 
@@ -525,7 +525,7 @@ describe Chewy::Index::Actions do
525
525
  .to receive(:put_settings).with(index: name, body: before_import_body).once
526
526
  expect(CitiesIndex.client.indices).to receive(:put_settings).with(index: name, body: after_import_body).once
527
527
  expect(CitiesIndex)
528
- .to receive(:import).with(suffix: suffix, progressbar: false, journal: false, refresh: false).and_call_original
528
+ .to receive(:import).with(suffix: suffix, journal: false, refresh: false).and_call_original
529
529
  expect(CitiesIndex.reset!(suffix)).to eq(true)
530
530
  end
531
531
 
@@ -541,7 +541,7 @@ describe Chewy::Index::Actions do
541
541
  let(:reset_disable_refresh_interval) { false }
542
542
  specify do
543
543
  expect(CitiesIndex.client.indices).not_to receive(:put_settings)
544
- expect(CitiesIndex).to receive(:import).with(suffix: suffix, progressbar: false, journal: false, refresh: true).and_call_original
544
+ expect(CitiesIndex).to receive(:import).with(suffix: suffix, journal: false, refresh: true).and_call_original
545
545
  expect(CitiesIndex.reset!(suffix)).to eq(true)
546
546
  end
547
547
  end
@@ -568,7 +568,7 @@ describe Chewy::Index::Actions do
568
568
  specify do
569
569
  expect(CitiesIndex.client.indices).to receive(:put_settings).with(index: name, body: before_import_body).once
570
570
  expect(CitiesIndex.client.indices).to receive(:put_settings).with(index: name, body: after_import_body).once
571
- expect(CitiesIndex).to receive(:import).with(suffix: suffix, progressbar: false, journal: false, refresh: true).and_call_original
571
+ expect(CitiesIndex).to receive(:import).with(suffix: suffix, journal: false, refresh: true).and_call_original
572
572
  expect(CitiesIndex.reset!(suffix)).to eq(true)
573
573
  end
574
574
  end
@@ -577,7 +577,7 @@ describe Chewy::Index::Actions do
577
577
  let(:reset_no_replicas) { false }
578
578
  specify do
579
579
  expect(CitiesIndex.client.indices).not_to receive(:put_settings)
580
- expect(CitiesIndex).to receive(:import).with(suffix: suffix, progressbar: false, journal: false, refresh: true).and_call_original
580
+ expect(CitiesIndex).to receive(:import).with(suffix: suffix, journal: false, refresh: true).and_call_original
581
581
  expect(CitiesIndex.reset!(suffix)).to eq(true)
582
582
  end
583
583
  end
@@ -610,7 +610,7 @@ describe Chewy::Index::Actions do
610
610
  specify 'with journal application' do
611
611
  cities
612
612
  p 'cities created1'
613
- ::ActiveRecord::Base.connection.close if defined?(::ActiveRecord::Base)
613
+ ActiveRecord::Base.connection.close if defined?(ActiveRecord::Base)
614
614
  [
615
615
  parallel_update,
616
616
  Thread.new do
@@ -619,7 +619,7 @@ describe Chewy::Index::Actions do
619
619
  p 'end reset1'
620
620
  end
621
621
  ].map(&:join)
622
- ::ActiveRecord::Base.connection.reconnect! if defined?(::ActiveRecord::Base)
622
+ ActiveRecord::Base.connection.reconnect! if defined?(ActiveRecord::Base)
623
623
  p 'expect1'
624
624
  expect(CitiesIndex::City.pluck(:_id, :name)).to contain_exactly(%w[1 NewName1], %w[2 Name2], %w[3 NewName3])
625
625
  p 'end expect1'
@@ -628,7 +628,7 @@ describe Chewy::Index::Actions do
628
628
  specify 'without journal application' do
629
629
  cities
630
630
  p 'cities created2'
631
- ::ActiveRecord::Base.connection.close if defined?(::ActiveRecord::Base)
631
+ ActiveRecord::Base.connection.close if defined?(ActiveRecord::Base)
632
632
  [
633
633
  parallel_update,
634
634
  Thread.new do
@@ -637,7 +637,7 @@ describe Chewy::Index::Actions do
637
637
  p 'end reset2'
638
638
  end
639
639
  ].map(&:join)
640
- ::ActiveRecord::Base.connection.reconnect! if defined?(::ActiveRecord::Base)
640
+ ActiveRecord::Base.connection.reconnect! if defined?(ActiveRecord::Base)
641
641
  p 'expect2'
642
642
  expect(CitiesIndex::City.pluck(:_id, :name)).to contain_exactly(%w[1 Name1], %w[2 Name2], %w[3 Name3])
643
643
  p 'end expect2'
@@ -667,7 +667,7 @@ describe Chewy::Index::Actions do
667
667
  specify do
668
668
  expect(CitiesIndex)
669
669
  .to receive(:import)
670
- .with(suffix: 'suffix', progressbar: false, parallel: true, journal: false, refresh: true)
670
+ .with(suffix: 'suffix', parallel: true, journal: false, refresh: true)
671
671
  .once.and_return(true)
672
672
  expect(CitiesIndex.reset!('suffix', parallel: true)).to eq(true)
673
673
  end
@@ -1,5 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
+ RawCity = Struct.new(:id) do
4
+ def rating
5
+ id * 10
6
+ end
7
+ end
8
+
3
9
  describe Chewy::Index::Adapter::ActiveRecord, :active_record do
4
10
  before do
5
11
  stub_model(:city)
@@ -29,6 +35,68 @@ describe Chewy::Index::Adapter::ActiveRecord, :active_record do
29
35
  specify { expect(described_class.new(City.where(rating: 10)).default_scope).to eq(City.where(rating: 10)) }
30
36
  end
31
37
 
38
+ describe '.new' do
39
+ context 'with logger' do
40
+ let(:test_logger) { Logger.new('/dev/null') }
41
+ let(:default_scope_behavior) { :warn }
42
+
43
+ around do |example|
44
+ previous_logger = Chewy.logger
45
+ Chewy.logger = test_logger
46
+
47
+ previous_default_scope_behavior = Chewy.config.import_scope_cleanup_behavior
48
+ Chewy.config.import_scope_cleanup_behavior = default_scope_behavior
49
+
50
+ example.run
51
+ ensure
52
+ Chewy.logger = previous_logger
53
+ Chewy.config.import_scope_cleanup_behavior = previous_default_scope_behavior
54
+ end
55
+
56
+ specify do
57
+ expect(test_logger).to receive(:warn)
58
+ described_class.new(City.order(:id))
59
+ end
60
+
61
+ specify do
62
+ expect(test_logger).to receive(:warn)
63
+ described_class.new(City.offset(10))
64
+ end
65
+
66
+ specify do
67
+ expect(test_logger).to receive(:warn)
68
+ described_class.new(City.limit(10))
69
+ end
70
+
71
+ context 'ignore import scope warning' do
72
+ let(:default_scope_behavior) { :ignore }
73
+
74
+ specify do
75
+ expect(test_logger).not_to receive(:warn)
76
+ described_class.new(City.order(:id))
77
+ end
78
+
79
+ specify do
80
+ expect(test_logger).not_to receive(:warn)
81
+ described_class.new(City.offset(10))
82
+ end
83
+
84
+ specify do
85
+ expect(test_logger).not_to receive(:warn)
86
+ described_class.new(City.limit(10))
87
+ end
88
+ end
89
+
90
+ context 'raise exception on import scope with order/limit/offset' do
91
+ let(:default_scope_behavior) { :raise }
92
+
93
+ specify { expect { described_class.new(City.order(:id)) }.to raise_error(Chewy::ImportScopeCleanupError) }
94
+ specify { expect { described_class.new(City.limit(10)) }.to raise_error(Chewy::ImportScopeCleanupError) }
95
+ specify { expect { described_class.new(City.offset(10)) }.to raise_error(Chewy::ImportScopeCleanupError) }
96
+ end
97
+ end
98
+ end
99
+
32
100
  describe '#type_name' do
33
101
  specify { expect(described_class.new(City).type_name).to eq('city') }
34
102
  specify { expect(described_class.new(City.order(:id)).type_name).to eq('city') }
@@ -571,5 +639,25 @@ describe Chewy::Index::Adapter::ActiveRecord, :active_record do
571
639
  ).to eq(cities.first(2) + [nil])
572
640
  end
573
641
  end
642
+
643
+ context 'with raw_import option' do
644
+ subject { described_class.new(City) }
645
+
646
+ let!(:cities) { Array.new(3) { |i| City.create!(rating: i / 2) } }
647
+ let(:city_ids) { cities.map(&:id) }
648
+
649
+ let(:raw_import) { ->(hash) { RawCity.new(hash['id']) } }
650
+ it 'uses the custom loader' do
651
+ raw_cities = subject.load(city_ids, _index: 'cities', raw_import: raw_import).map do |c|
652
+ {id: c.id, rating: c.rating}
653
+ end
654
+
655
+ expect(raw_cities).to eq([
656
+ {id: 1, rating: 10},
657
+ {id: 2, rating: 20},
658
+ {id: 3, rating: 30}
659
+ ])
660
+ end
661
+ end
574
662
  end
575
663
  end
@@ -1,5 +1,21 @@
1
1
  require 'spec_helper'
2
2
 
3
+ SimpleComment = Class.new do
4
+ attr_reader :content, :comment_type, :commented_id, :updated_at, :id
5
+
6
+ def initialize(hash)
7
+ @id = hash['id']
8
+ @content = hash['content']
9
+ @comment_type = hash['comment_type']
10
+ @commented_id = hash['commented_id']
11
+ @updated_at = hash['updated_at']
12
+ end
13
+
14
+ def derived
15
+ "[derived] #{content}"
16
+ end
17
+ end
18
+
3
19
  describe Chewy::Index::Import::BulkBuilder do
4
20
  before { Chewy.massacre }
5
21
 
@@ -46,6 +62,8 @@ describe Chewy::Index::Import::BulkBuilder do
46
62
  let(:to_index) { cities.first(2) }
47
63
  let(:delete) { [cities.last] }
48
64
  specify do
65
+ expect(subject).to receive(:data_for).with(cities.first).and_call_original
66
+ expect(subject).to receive(:data_for).with(cities.second).and_call_original
49
67
  expect(subject.bulk_body).to eq([
50
68
  {index: {_id: 1, data: {'name' => 'City17', 'rating' => 42}}},
51
69
  {index: {_id: 2, data: {'name' => 'City18', 'rating' => 42}}},
@@ -56,6 +74,8 @@ describe Chewy::Index::Import::BulkBuilder do
56
74
  context ':fields' do
57
75
  let(:fields) { %w[name] }
58
76
  specify do
77
+ expect(subject).to receive(:data_for).with(cities.first, fields: [:name]).and_call_original
78
+ expect(subject).to receive(:data_for).with(cities.second, fields: [:name]).and_call_original
59
79
  expect(subject.bulk_body).to eq([
60
80
  {update: {_id: 1, data: {doc: {'name' => 'City17'}}}},
61
81
  {update: {_id: 2, data: {doc: {'name' => 'City18'}}}},
@@ -112,7 +132,7 @@ describe Chewy::Index::Import::BulkBuilder do
112
132
  before do
113
133
  stub_index(:cities) do
114
134
  crutch :names do |collection|
115
- collection.map { |item| [item.id, "Name#{item.id}"] }.to_h
135
+ collection.to_h { |item| [item.id, "Name#{item.id}"] }
116
136
  end
117
137
 
118
138
  field :name, value: ->(o, c) { c.names[o.id] }
@@ -169,6 +189,294 @@ describe Chewy::Index::Import::BulkBuilder do
169
189
  end
170
190
  end
171
191
  end
192
+
193
+ context 'with parents' do
194
+ let(:index) { CommentsIndex }
195
+ before do
196
+ stub_model(:comment)
197
+ stub_index(:comments) do
198
+ index_scope Comment
199
+
200
+ crutch :content_with_crutches do |collection| # collection here is a current batch of products
201
+ collection.to_h { |comment| [comment.id, "[crutches] #{comment.content}"] }
202
+ end
203
+
204
+ field :content
205
+ field :content_with_crutches, value: ->(comment, crutches) { crutches.content_with_crutches[comment.id] }
206
+ field :comment_type, type: :join, relations: {question: %i[answer comment], answer: :vote, vote: :subvote}, join: {type: :comment_type, id: :commented_id}
207
+ end
208
+ end
209
+
210
+ let!(:existing_comments) do
211
+ [
212
+ Comment.create!(id: 1, content: 'Where is Nemo?', comment_type: :question),
213
+ Comment.create!(id: 2, content: 'Here.', comment_type: :answer, commented_id: 1),
214
+ Comment.create!(id: 31, content: 'What is the best programming language?', comment_type: :question)
215
+ ]
216
+ end
217
+
218
+ def do_raw_index_comment(options:, data:)
219
+ CommentsIndex.client.index(options.merge(index: 'comments', type: '_doc', refresh: true, body: data))
220
+ end
221
+
222
+ def raw_index_comment(comment)
223
+ options = {id: comment.id, routing: root(comment).id}
224
+ comment_type = comment.commented_id.present? ? {name: comment.comment_type, parent: comment.commented_id} : comment.comment_type
225
+ do_raw_index_comment(
226
+ options: options,
227
+ data: {content: comment.content, comment_type: comment_type}
228
+ )
229
+ end
230
+
231
+ def root(comment)
232
+ current = comment
233
+ # slow, but it's OK, as we don't have too deep trees
234
+ current = Comment.find(current.commented_id) while current.commented_id
235
+ current
236
+ end
237
+
238
+ before do
239
+ CommentsIndex.reset! # initialize index
240
+ end
241
+
242
+ let(:comments) do
243
+ [
244
+ Comment.create!(id: 3, content: 'There!', comment_type: :answer, commented_id: 1),
245
+ Comment.create!(id: 4, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
246
+
247
+ Comment.create!(id: 11, content: 'What is the sense of the universe?', comment_type: :question),
248
+ Comment.create!(id: 12, content: 'I don\'t know.', comment_type: :answer, commented_id: 11),
249
+ Comment.create!(id: 13, content: '42', comment_type: :answer, commented_id: 11),
250
+ Comment.create!(id: 14, content: 'I think that 42 is a correct answer', comment_type: :vote, commented_id: 13),
251
+
252
+ Comment.create!(id: 21, content: 'How are you?', comment_type: :question),
253
+
254
+ Comment.create!(id: 32, content: 'Ruby', comment_type: :answer, commented_id: 31)
255
+ ]
256
+ end
257
+
258
+ context 'when indexing a single object' do
259
+ let(:to_index) { [comments[0]] }
260
+
261
+ specify do
262
+ expect(subject.bulk_body).to eq([
263
+ {index: {_id: 3, routing: '1', data: {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}}}}
264
+ ])
265
+ end
266
+ end
267
+
268
+ context 'with raw import' do
269
+ before do
270
+ stub_index(:comments) do
271
+ index_scope Comment
272
+ default_import_options raw_import: ->(hash) { SimpleComment.new(hash) }
273
+
274
+ crutch :content_with_crutches do |collection| # collection here is a current batch of products
275
+ collection.to_h { |comment| [comment.id, "[crutches] #{comment.content}"] }
276
+ end
277
+
278
+ field :content
279
+ field :content_with_crutches, value: ->(comment, crutches) { crutches.content_with_crutches[comment.id] }
280
+ field :derived
281
+ field :comment_type, type: :join, relations: {question: %i[answer comment], answer: :vote, vote: :subvote}, join: {type: :comment_type, id: :commented_id}
282
+ end
283
+ end
284
+
285
+ let(:to_index) { [comments[0]].map { |c| SimpleComment.new(c.attributes) } } # id: 3
286
+ let(:delete) { [existing_comments[0]].map { |c| c } } # id: 1
287
+
288
+ specify do
289
+ expected_data = {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'derived' => '[derived] There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}}
290
+ expect(subject.bulk_body).to eq([
291
+ {index: {_id: 3, routing: '1', data: expected_data}},
292
+ {delete: {_id: 1, routing: '1'}}
293
+ ])
294
+ end
295
+ end
296
+
297
+ context 'when switching parents' do
298
+ let(:switching_parent_comment) { comments[0].tap { |c| c.update!(commented_id: 31) } } # id: 3
299
+ let(:removing_parent_comment) { comments[1].tap { |c| c.update!(commented_id: nil, comment_type: nil) } } # id: 4
300
+ let(:converting_to_parent_comment) { comments[3].tap { |c| c.update!(commented_id: nil, comment_type: :question) } } # id: 12
301
+ let(:converting_to_child_comment) { comments[6].tap { |c| c.update!(commented_id: 1, comment_type: :answer) } } # id: 21
302
+ let(:fields) { %w[commented_id comment_type] }
303
+
304
+ let(:to_index) { [switching_parent_comment, removing_parent_comment, converting_to_parent_comment, converting_to_child_comment] }
305
+
306
+ before do
307
+ existing_comments.each { |c| raw_index_comment(c) }
308
+ comments.each { |c| raw_index_comment(c) }
309
+ end
310
+
311
+ specify do
312
+ expect(subject.bulk_body).to eq([
313
+ {delete: {_id: 3, routing: '1', parent: 1}},
314
+ {index: {_id: 3, routing: '31', data: {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'comment_type' => {'name' => 'answer', 'parent' => 31}}}},
315
+ {delete: {_id: 4, routing: '1', parent: 2}},
316
+ {index: {_id: 4, routing: '4', data: {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => nil}}},
317
+ {delete: {_id: 12, routing: '11', parent: 11}},
318
+ {index: {_id: 12, routing: '12', data: {'content' => 'I don\'t know.', 'content_with_crutches' => '[crutches] I don\'t know.', 'comment_type' => 'question'}}},
319
+ {delete: {_id: 21, routing: '21'}},
320
+ {index: {_id: 21, routing: '1', data: {'content' => 'How are you?', 'content_with_crutches' => '[crutches] How are you?', 'comment_type' => {'name' => 'answer', 'parent' => 1}}}}
321
+ ])
322
+ end
323
+ end
324
+
325
+ context 'when indexing with grandparents' do
326
+ let(:comments) do
327
+ [
328
+ Comment.create!(id: 3, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
329
+ Comment.create!(id: 4, content: 'What?', comment_type: :subvote, commented_id: 3)
330
+ ]
331
+ end
332
+ let(:to_index) { comments }
333
+
334
+ before do
335
+ existing_comments.each { |c| raw_index_comment(c) }
336
+ end
337
+
338
+ specify do
339
+ expected_data3 = {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
340
+ expected_data4 = {'content' => 'What?', 'content_with_crutches' => '[crutches] What?', 'comment_type' => {'name' => 'subvote', 'parent' => 3}}
341
+ expect(subject.bulk_body).to eq([
342
+ {index: {_id: 3, routing: '1', data: expected_data3}},
343
+ {index: {_id: 4, routing: '1', data: expected_data4}}
344
+ ])
345
+ end
346
+ end
347
+
348
+ context 'when switching grandparents' do
349
+ let(:comments) do
350
+ [
351
+ Comment.create!(id: 3, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
352
+ Comment.create!(id: 4, content: 'What?', comment_type: :subvote, commented_id: 3)
353
+ ]
354
+ end
355
+ let(:switching_parent_comment) { existing_comments[1].tap { |c| c.update!(commented_id: 31) } } # id: 2
356
+ let(:fields) { %w[commented_id comment_type] }
357
+ let(:to_index) { [switching_parent_comment] }
358
+
359
+ before do
360
+ existing_comments.each { |c| raw_index_comment(c) }
361
+ comments.each { |c| raw_index_comment(c) }
362
+ end
363
+
364
+ it 'reindexes children and grandchildren' do
365
+ expected_data2 = {'content' => 'Here.', 'content_with_crutches' => '[crutches] Here.', 'comment_type' => {'name' => 'answer', 'parent' => 31}}
366
+ expected_data3 = {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
367
+ expected_data4 = {'content' => 'What?', 'content_with_crutches' => '[crutches] What?', 'comment_type' => {'name' => 'subvote', 'parent' => 3}}
368
+ expect(subject.bulk_body).to eq([
369
+ {delete: {_id: 2, routing: '1', parent: 1}},
370
+ {index: {_id: 2, routing: '31', data: expected_data2}},
371
+ {delete: {_id: 3, routing: '1', parent: 2}},
372
+ {index: {_id: 3, routing: '31', data: expected_data3}},
373
+ {delete: {_id: 4, routing: '1', parent: 3}},
374
+ {index: {_id: 4, routing: '31', data: expected_data4}}
375
+ ])
376
+ end
377
+ end
378
+
379
+ describe 'when removing parents or grandparents' do
380
+ let(:comments) do
381
+ [
382
+ Comment.create!(id: 3, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
383
+ Comment.create!(id: 4, content: 'What?', comment_type: :subvote, commented_id: 3)
384
+ ]
385
+ end
386
+ let(:delete) { [existing_comments[0]] } # id: 1
387
+
388
+ before do
389
+ existing_comments.each { |c| raw_index_comment(c) }
390
+ comments.each { |c| raw_index_comment(c) }
391
+ end
392
+
393
+ it 'does not remove all descendants' do
394
+ expect(subject.bulk_body).to eq([
395
+ {delete: {_id: 1, routing: '1'}}
396
+ ])
397
+ end
398
+ end
399
+
400
+ context 'when indexing' do
401
+ let(:to_index) { comments }
402
+
403
+ specify do
404
+ expected_data3 = {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}}
405
+ expected_data4 = {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
406
+
407
+ expected_data11 = {'content' => 'What is the sense of the universe?', 'content_with_crutches' => '[crutches] What is the sense of the universe?', 'comment_type' => 'question'}
408
+ expected_data12 = {'content' => 'I don\'t know.', 'content_with_crutches' => '[crutches] I don\'t know.', 'comment_type' => {'name' => 'answer', 'parent' => 11}}
409
+ expected_data13 = {'content' => '42', 'content_with_crutches' => '[crutches] 42', 'comment_type' => {'name' => 'answer', 'parent' => 11}}
410
+ expected_data14 = {'content' => 'I think that 42 is a correct answer', 'content_with_crutches' => '[crutches] I think that 42 is a correct answer',
411
+ 'comment_type' => {'name' => 'vote', 'parent' => 13}}
412
+
413
+ expected_data21 = {'content' => 'How are you?', 'content_with_crutches' => '[crutches] How are you?', 'comment_type' => 'question'}
414
+
415
+ expected_data32 = {'content' => 'Ruby', 'content_with_crutches' => '[crutches] Ruby', 'comment_type' => {'name' => 'answer', 'parent' => 31}}
416
+
417
+ expect(subject.bulk_body).to eq([
418
+ {index: {_id: 3, routing: '1', data: expected_data3}},
419
+ {index: {_id: 4, routing: '1', data: expected_data4}},
420
+
421
+ {index: {_id: 11, routing: '11', data: expected_data11}},
422
+ {index: {_id: 12, routing: '11', data: expected_data12}},
423
+ {index: {_id: 13, routing: '11', data: expected_data13}},
424
+ {index: {_id: 14, routing: '11', data: expected_data14}},
425
+
426
+ {index: {_id: 21, routing: '21', data: expected_data21}},
427
+
428
+ {index: {_id: 32, routing: '31', data: expected_data32}}
429
+ ])
430
+ end
431
+ end
432
+
433
+ context 'when deleting' do
434
+ before do
435
+ existing_comments.each { |c| raw_index_comment(c) }
436
+ comments.each { |c| raw_index_comment(c) }
437
+ end
438
+
439
+ let(:delete) { comments }
440
+ specify do
441
+ expect(subject.bulk_body).to eq([
442
+ {delete: {_id: 3, routing: '1', parent: 1}},
443
+ {delete: {_id: 4, routing: '1', parent: 2}},
444
+
445
+ {delete: {_id: 11, routing: '11'}},
446
+ {delete: {_id: 12, routing: '11', parent: 11}},
447
+ {delete: {_id: 13, routing: '11', parent: 11}},
448
+ {delete: {_id: 14, routing: '11', parent: 13}},
449
+
450
+ {delete: {_id: 21, routing: '21'}},
451
+
452
+ {delete: {_id: 32, routing: '31', parent: 31}}
453
+ ])
454
+ end
455
+ end
456
+
457
+ context 'when updating' do
458
+ before do
459
+ comments.each { |c| raw_index_comment(c) }
460
+ end
461
+ let(:fields) { %w[content] }
462
+ let(:to_index) { comments }
463
+ specify do
464
+ expect(subject.bulk_body).to eq([
465
+ {update: {_id: 3, routing: '1', data: {doc: {'content' => comments[0].content}}}},
466
+ {update: {_id: 4, routing: '1', data: {doc: {'content' => comments[1].content}}}},
467
+
468
+ {update: {_id: 11, routing: '11', data: {doc: {'content' => comments[2].content}}}},
469
+ {update: {_id: 12, routing: '11', data: {doc: {'content' => comments[3].content}}}},
470
+ {update: {_id: 13, routing: '11', data: {doc: {'content' => comments[4].content}}}},
471
+ {update: {_id: 14, routing: '11', data: {doc: {'content' => comments[5].content}}}},
472
+
473
+ {update: {_id: 21, routing: '21', data: {doc: {'content' => comments[6].content}}}},
474
+
475
+ {update: {_id: 32, routing: '31', data: {doc: {'content' => comments[7].content}}}}
476
+ ])
477
+ end
478
+ end
479
+ end
172
480
  end
173
481
 
174
482
  describe '#index_objects_by_id' do
@@ -11,8 +11,8 @@ describe Chewy::Index::Import::Routine do
11
11
  CitiesIndex.create!
12
12
  end
13
13
 
14
- let(:index) { [double(id: 1, name: 'Name', object: {}), double(id: 2, name: 'Name', object: {})] }
15
- let(:delete) { [double(id: 3, name: 'Name')] }
14
+ let(:index) { [double('city_1', id: 1, name: 'Name', object: {}), double('city_2', id: 2, name: 'Name', object: {})] }
15
+ let(:delete) { [double('city_3', id: 3, name: 'Name', object: {})] }
16
16
 
17
17
  describe '#options' do
18
18
  specify do
@@ -64,8 +64,8 @@ describe Chewy::Index::Import::Routine do
64
64
 
65
65
  describe '#parallel_options' do
66
66
  specify { expect(described_class.new(CitiesIndex).parallel_options).to be_nil }
67
- specify { expect(described_class.new(CitiesIndex, parallel: true).parallel_options).to eq({in_threads: [::Parallel.processor_count, ActiveRecord::Base.connection_pool.size].min}) }
68
- specify { expect(described_class.new(CitiesIndex, parallel: 3).parallel_options).to eq(in_threads: 3) }
67
+ specify { expect(described_class.new(CitiesIndex, parallel: true).parallel_options).to eq({}) }
68
+ specify { expect(described_class.new(CitiesIndex, parallel: 3).parallel_options).to eq(in_processes: 3) }
69
69
  specify do
70
70
  expect(described_class.new(CitiesIndex, parallel: {in_threads: 2}).parallel_options).to eq(in_threads: 2)
71
71
  end
@@ -93,7 +93,7 @@ describe Chewy::Index::Import::Routine do
93
93
 
94
94
  describe '#errors' do
95
95
  subject { described_class.new(CitiesIndex) }
96
- let(:index) { [double(id: 1, name: 'Name', object: ''), double(id: 2, name: 'Name', object: {})] }
96
+ let(:index) { [double('city_1', id: 1, name: 'Name', object: ''), double('city_2', id: 2, name: 'Name', object: {})] }
97
97
 
98
98
  specify { expect(subject.errors).to eq([]) }
99
99
  specify do