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
@@ -2,19 +2,17 @@ require 'chewy/index/import/journal_builder'
2
2
  require 'chewy/index/import/bulk_builder'
3
3
  require 'chewy/index/import/bulk_request'
4
4
  require 'chewy/index/import/routine'
5
- require 'chewy/index/import/thread_safe_progress_bar'
6
5
 
7
6
  module Chewy
8
7
  class Index
9
8
  module Import
10
9
  extend ActiveSupport::Concern
11
10
 
12
- IMPORT_WORKER = lambda do |index, options, total, progress_bar, ids, iteration|
11
+ IMPORT_WORKER = lambda do |index, options, total, ids, iteration|
13
12
  ::Process.setproctitle("chewy [#{index}]: import data (#{iteration + 1}/#{total})")
14
13
  routine = Routine.new(index, **options)
15
14
  index.adapter.import(*ids, routine.options) do |action_objects|
16
15
  routine.process(**action_objects)
17
- progress_bar.increment(action_objects.map { |_, v| v.size }.sum) if routine.options[:progressbar]
18
16
  end
19
17
  {errors: routine.errors, import: routine.stats, leftovers: routine.leftovers}
20
18
  end
@@ -38,8 +36,7 @@ module Chewy
38
36
  # passed objects from the index if they are not in the default scope
39
37
  # or marked for destruction.
40
38
  #
41
- # It handles parent-child relationships: if the object parent_id has been
42
- # changed it destroys the object and recreates it from scratch.
39
+ # It handles parent-child relationships with a join field reindexing children when the parent is reindexed.
43
40
  #
44
41
  # Performs journaling if enabled: it stores all the ids of the imported
45
42
  # objects to a specialized index. It is possible to replay particular import
@@ -76,7 +73,7 @@ module Chewy
76
73
  # @option options [true, Integer, Hash] parallel enables parallel import processing with the Parallel gem, accepts the number of workers or any Parallel gem acceptable options
77
74
  # @return [true, false] false in case of errors
78
75
  def import(*args)
79
- import_routine(*args).blank?
76
+ intercept_import_using_strategy(*args).blank?
80
77
  end
81
78
 
82
79
  # @!method import!(*collection, **options)
@@ -87,7 +84,8 @@ module Chewy
87
84
  #
88
85
  # @raise [Chewy::ImportFailed] in case of errors
89
86
  def import!(*args)
90
- errors = import_routine(*args)
87
+ errors = intercept_import_using_strategy(*args)
88
+
91
89
  raise Chewy::ImportFailed.new(self, errors) if errors.present?
92
90
 
93
91
  true
@@ -129,6 +127,32 @@ module Chewy
129
127
 
130
128
  private
131
129
 
130
+ def intercept_import_using_strategy(*args)
131
+ args_clone = args.deep_dup
132
+ options = args_clone.extract_options!
133
+ strategy = options.delete(:strategy)
134
+
135
+ return import_routine(*args) if strategy.blank?
136
+
137
+ ids = args_clone.flatten
138
+ return {} if ids.blank?
139
+ return {argument: {"#{strategy} supports ids only!" => ids}} unless ids.all? do |id|
140
+ id.respond_to?(:to_i)
141
+ end
142
+
143
+ case strategy
144
+ when :delayed_sidekiq
145
+ begin
146
+ Chewy::Strategy::DelayedSidekiq::Scheduler.new(self, ids, options).postpone
147
+ {} # success. errors handling convention
148
+ rescue StandardError => e
149
+ {scheduler: {e.message => ids}}
150
+ end
151
+ else
152
+ {argument: {"unsupported strategy: '#{strategy}'" => ids}}
153
+ end
154
+ end
155
+
132
156
  def import_routine(*args)
133
157
  return if !args.first.nil? && empty_objects_or_scope?(args.first)
134
158
 
@@ -152,10 +176,8 @@ module Chewy
152
176
 
153
177
  def import_linear(objects, routine)
154
178
  ActiveSupport::Notifications.instrument 'import_objects.chewy', index: self do |payload|
155
- progress_bar = ThreadSafeProgressBar.new(routine.options[:progressbar]) { adapter.import_count(objects) }
156
179
  adapter.import(*objects, routine.options) do |action_objects|
157
180
  routine.process(**action_objects)
158
- progress_bar.increment(action_objects.map { |_, v| v.size }.sum) if routine.options[:progressbar]
159
181
  end
160
182
  routine.perform_bulk(routine.leftovers)
161
183
  payload[:import] = routine.stats
@@ -170,20 +192,24 @@ module Chewy
170
192
  ActiveSupport::Notifications.instrument 'import_objects.chewy', index: self do |payload|
171
193
  batches = adapter.import_references(*objects, routine.options.slice(:batch_size)).to_a
172
194
 
173
- progress_bar = ThreadSafeProgressBar.new(routine.options[:progressbar]) { adapter.import_count(objects) }
174
195
  ::ActiveRecord::Base.connection.close if defined?(::ActiveRecord::Base)
175
-
176
- results = ::Parallel.map_with_index(batches, routine.parallel_options) do |ids, index|
177
- progress_bar.wait_until_ready
178
- ActiveRecord::Base.connection_pool.with_connection do
179
- IMPORT_WORKER.call(self, routine.options, total, progress_bar, ids, index)
180
- end
181
- end
182
-
196
+ results = ::Parallel.map_with_index(
197
+ batches,
198
+ routine.parallel_options,
199
+ &IMPORT_WORKER.curry[self, routine.options, batches.size]
200
+ )
183
201
  ::ActiveRecord::Base.connection.reconnect! if defined?(::ActiveRecord::Base)
184
202
  errors, import, leftovers = process_parallel_import_results(results)
185
203
 
186
- execute_leftovers(leftovers, routine, self, errors)
204
+ if leftovers.present?
205
+ batches = leftovers.each_slice(routine.options[:batch_size])
206
+ results = ::Parallel.map_with_index(
207
+ batches,
208
+ routine.parallel_options,
209
+ &LEFTOVERS_WORKER.curry[self, routine.options, batches.size]
210
+ )
211
+ errors.concat(results.flatten(1))
212
+ end
187
213
 
188
214
  payload[:import] = import
189
215
  payload[:errors] = payload_errors(errors) if errors.present?
@@ -191,18 +217,6 @@ module Chewy
191
217
  end
192
218
  end
193
219
 
194
- def execute_leftovers(leftovers, routine, self_object, errors)
195
- return unless leftovers.present?
196
-
197
- batches = leftovers.each_slice(routine.options[:batch_size])
198
- results = ::Parallel.map_with_index(
199
- batches,
200
- routine.parallel_options,
201
- &LEFTOVERS_WORKER.curry[self_object, routine.options, batches.size]
202
- )
203
- errors.concat(results.flatten(1))
204
- end
205
-
206
220
  def process_parallel_import_results(results)
207
221
  results.each_with_object([[], {}, []]) do |r, (e, i, l)|
208
222
  e.concat(r[:errors])
@@ -170,8 +170,8 @@ module Chewy
170
170
  # template /tit.+/, type: 'text', mapping_hash # "match_mapping_type" as an optional second argument
171
171
  # template template42: {match: 'hello*', mapping: {type: 'object'}} # or even pass a template as is
172
172
  #
173
- def template(*args)
174
- root.dynamic_template(*args)
173
+ def template(*args, **options)
174
+ root.dynamic_template(*args, **options)
175
175
  end
176
176
  alias_method :dynamic_template, :template
177
177
 
@@ -0,0 +1,87 @@
1
+ module Chewy
2
+ class Index
3
+ module Observe
4
+ module Helpers
5
+ def update_proc(index_name, *args, &block)
6
+ options = args.extract_options!
7
+ method = args.first
8
+
9
+ proc do
10
+ reference = if index_name.is_a?(Proc)
11
+ if index_name.arity.zero?
12
+ instance_exec(&index_name)
13
+ else
14
+ index_name.call(self)
15
+ end
16
+ else
17
+ index_name
18
+ end
19
+
20
+ index = Chewy.derive_name(reference)
21
+
22
+ next if Chewy.strategy.current.name == :bypass
23
+
24
+ backreference = if method && method.to_s == 'self'
25
+ self
26
+ elsif method
27
+ send(method)
28
+ else
29
+ instance_eval(&block)
30
+ end
31
+
32
+ index.update_index(backreference, options)
33
+ end
34
+ end
35
+
36
+ def extract_callback_options!(args)
37
+ options = args.extract_options!
38
+ result = options.each_key.with_object({}) do |key, hash|
39
+ hash[key] = options.delete(key) if %i[if unless].include?(key)
40
+ end
41
+ args.push(options) unless options.empty?
42
+ result
43
+ end
44
+ end
45
+
46
+ extend Helpers
47
+
48
+ module ActiveRecordMethods
49
+ extend ActiveSupport::Concern
50
+
51
+ def run_chewy_callbacks
52
+ chewy_callbacks.each { |callback| callback.call(self) }
53
+ end
54
+
55
+ def update_chewy_indices
56
+ Chewy.strategy.current.update_chewy_indices(self)
57
+ end
58
+
59
+ included do
60
+ class_attribute :chewy_callbacks, default: []
61
+ end
62
+
63
+ class_methods do
64
+ def initialize_chewy_callbacks
65
+ if Chewy.use_after_commit_callbacks
66
+ after_commit :update_chewy_indices, on: %i[create update]
67
+ after_commit :run_chewy_callbacks, on: :destroy
68
+ else
69
+ after_save :update_chewy_indices
70
+ after_destroy :run_chewy_callbacks
71
+ end
72
+ end
73
+
74
+ def update_index(type_name, *args, &block)
75
+ callback_options = Observe.extract_callback_options!(args)
76
+ update_proc = Observe.update_proc(type_name, *args, &block)
77
+ callback = Chewy::Index::Observe::Callback.new(update_proc, callback_options)
78
+
79
+ initialize_chewy_callbacks if chewy_callbacks.empty?
80
+
81
+ self.chewy_callbacks = chewy_callbacks.dup << callback
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ module Chewy
2
+ class Index
3
+ module Observe
4
+ class Callback
5
+ def initialize(executable, filters = {})
6
+ @executable = executable
7
+ @if_filter = filters[:if]
8
+ @unless_filter = filters[:unless]
9
+ end
10
+
11
+ def call(context)
12
+ return if !@if_filter.nil? && !eval_filter(@if_filter, context)
13
+ return if !@unless_filter.nil? && eval_filter(@unless_filter, context)
14
+
15
+ eval_proc(@executable, context)
16
+ end
17
+
18
+ private
19
+
20
+ def eval_filter(filter, context)
21
+ case filter
22
+ when Symbol then context.send(filter)
23
+ when Proc then eval_proc(filter, context)
24
+ else filter
25
+ end
26
+ end
27
+
28
+ def eval_proc(executable, context)
29
+ executable.arity.zero? ? context.instance_exec(&executable) : executable.call(context)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,66 +1,11 @@
1
+ require 'chewy/index/observe/callback'
2
+ require 'chewy/index/observe/active_record_methods'
3
+
1
4
  module Chewy
2
5
  class Index
3
6
  module Observe
4
7
  extend ActiveSupport::Concern
5
8
 
6
- module Helpers
7
- def update_proc(index_name, *args, &block)
8
- options = args.extract_options!
9
- method = args.first
10
-
11
- proc do
12
- reference = if index_name.is_a?(Proc)
13
- if index_name.arity.zero?
14
- instance_exec(&index_name)
15
- else
16
- index_name.call(self)
17
- end
18
- else
19
- index_name
20
- end
21
-
22
- index = Chewy.derive_name(reference)
23
-
24
- next if Chewy.strategy.current.name == :bypass
25
-
26
- backreference = if method && method.to_s == 'self'
27
- self
28
- elsif method
29
- send(method)
30
- else
31
- instance_eval(&block)
32
- end
33
-
34
- index.update_index(backreference, options)
35
- end
36
- end
37
-
38
- def extract_callback_options!(args)
39
- options = args.extract_options!
40
- result = options.each_key.with_object({}) do |key, hash|
41
- hash[key] = options.delete(key) if %i[if unless].include?(key)
42
- end
43
- args.push(options) unless options.empty?
44
- result
45
- end
46
- end
47
-
48
- extend Helpers
49
-
50
- module ActiveRecordMethods
51
- def update_index(type_name, *args, &block)
52
- callback_options = Observe.extract_callback_options!(args)
53
- update_proc = Observe.update_proc(type_name, *args, &block)
54
-
55
- if Chewy.use_after_commit_callbacks
56
- after_commit(**callback_options, &update_proc)
57
- else
58
- after_save(**callback_options, &update_proc)
59
- after_destroy(**callback_options, &update_proc)
60
- end
61
- end
62
- end
63
-
64
9
  module ClassMethods
65
10
  def update_index(objects, options = {})
66
11
  Chewy.strategy.current.update(self, objects, options)
@@ -27,7 +27,7 @@ module Chewy
27
27
  # @see Chewy::Index::Actions::ClassMethods#sync
28
28
  class Syncer
29
29
  DEFAULT_SYNC_BATCH_SIZE = 20_000
30
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/.freeze
30
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
31
31
  OUTDATED_IDS_WORKER = lambda do |outdated_sync_field_type, source_data_hash, index, total, index_data|
32
32
  ::Process.setproctitle("chewy [#{index}]: sync outdated calculation (#{::Parallel.worker_number + 1}/#{total})") if index
33
33
  index_data.each_with_object([]) do |(id, index_sync_value), result|
data/lib/chewy/index.rb CHANGED
@@ -20,6 +20,10 @@ module Chewy
20
20
  pipeline raw_import refresh replication
21
21
  ].freeze
22
22
 
23
+ STRATEGY_OPTIONS = {
24
+ delayed_sidekiq: %i[latency margin ttl reindex_wrapper]
25
+ }.freeze
26
+
23
27
  include Search
24
28
  include Actions
25
29
  include Aliases
@@ -221,6 +225,27 @@ module Chewy
221
225
  params.assert_valid_keys(IMPORT_OPTIONS_KEYS)
222
226
  self._default_import_options = _default_import_options.merge(params)
223
227
  end
228
+
229
+ def strategy_config(params = {})
230
+ @strategy_config ||= begin
231
+ config_struct = Struct.new(*STRATEGY_OPTIONS.keys).new
232
+
233
+ STRATEGY_OPTIONS.each_with_object(config_struct) do |(strategy, options), res|
234
+ res[strategy] = case strategy
235
+ when :delayed_sidekiq
236
+ Struct.new(*STRATEGY_OPTIONS[strategy]).new.tap do |config|
237
+ options.each do |option|
238
+ config[option] = params.dig(strategy, option) || Chewy.configuration.dig(:strategy_config, strategy, option)
239
+ end
240
+
241
+ config[:reindex_wrapper] ||= ->(&reindex) { reindex.call } # default wrapper
242
+ end
243
+ else
244
+ raise NotImplementedError, "Unsupported strategy: '#{strategy}'"
245
+ end
246
+ end
247
+ end
248
+ end
224
249
  end
225
250
  end
226
251
  end
data/lib/chewy/journal.rb CHANGED
@@ -16,14 +16,17 @@ module Chewy
16
16
  # specified indexes.
17
17
  #
18
18
  # @param since_time [Time, DateTime] timestamp from which changes will be applied
19
- # @param retries [Integer] maximum number of attempts to make journal empty, 10 by default
19
+ # @param fetch_limit [Int] amount of entries to be fetched on each cycle
20
20
  # @return [Integer] the amount of journal entries found
21
- def apply(since_time, retries: 10, **import_options)
21
+ def apply(since_time, fetch_limit: 10, **import_options)
22
22
  stage = 1
23
23
  since_time -= 1
24
24
  count = 0
25
- while stage <= retries
26
- entries = Chewy::Stash::Journal.entries(since_time, only: @only).to_a.presence or break
25
+
26
+ total_count = entries(since_time, fetch_limit).total_count
27
+
28
+ while count < total_count
29
+ entries = entries(since_time, fetch_limit).to_a.presence or break
27
30
  count += entries.size
28
31
  groups = reference_groups(entries)
29
32
  ActiveSupport::Notifications.instrument 'apply_journal.chewy', stage: stage, groups: groups
@@ -40,12 +43,20 @@ module Chewy
40
43
  #
41
44
  # @param until_time [Time, DateTime] time to clean up until it
42
45
  # @return [Hash] delete_by_query ES API call result
43
- def clean(until_time = nil)
44
- Chewy::Stash::Journal.clean(until_time, only: @only)
46
+ def clean(until_time = nil, delete_by_query_options: {})
47
+ Chewy::Stash::Journal.clean(
48
+ until_time,
49
+ only: @only,
50
+ delete_by_query_options: delete_by_query_options.merge(refresh: false)
51
+ )
45
52
  end
46
53
 
47
54
  private
48
55
 
56
+ def entries(since_time, fetch_limit)
57
+ Chewy::Stash::Journal.entries(since_time, only: @only).order(:created_at).limit(fetch_limit)
58
+ end
59
+
49
60
  def reference_groups(entries)
50
61
  entries.group_by(&:index_name)
51
62
  .transform_keys { |index_name| Chewy.derive_name(index_name) }
@@ -24,7 +24,11 @@ module Chewy
24
24
 
25
25
  subject = payload[:type].presence || payload[:index]
26
26
  action = "#{subject} #{action} (#{event.duration.round(1)}ms)"
27
- action = color(action, GREEN, true)
27
+ action = if ActiveSupport.version >= Gem::Version.new('7.1')
28
+ color(action, GREEN, bold: true)
29
+ else
30
+ color(action, GREEN, true)
31
+ end
28
32
 
29
33
  debug(" #{action} #{description}")
30
34
  end
@@ -42,10 +42,87 @@ module Chewy
42
42
  # Run indexing for the database changes during the block provided.
43
43
  # By default, indexing is run at the end of the block.
44
44
  # @param strategy [Symbol] the Chewy index update strategy see Chewy docs.
45
+ #
45
46
  def run_indexing(strategy: :atomic, &block)
46
47
  Chewy.strategy strategy, &block
47
48
  end
48
49
 
50
+ # Mock Elasticsearch response
51
+ # Simple usage - just pass index, expected raw response
52
+ # and block with the query.
53
+ #
54
+ # @param index [Chewy::Index] the index to watch, eg EntitiesIndex.
55
+ # @param raw_response [Hash] hash with response.
56
+ #
57
+ def mock_elasticsearch_response(index, raw_response)
58
+ mocked_request = Chewy::Search::Request.new(index)
59
+
60
+ original_new = Chewy::Search::Request.method(:new)
61
+
62
+ Chewy::Search::Request.define_singleton_method(:new) { |*_args| mocked_request }
63
+
64
+ original_perform = mocked_request.method(:perform)
65
+ mocked_request.define_singleton_method(:perform) { raw_response }
66
+
67
+ yield
68
+ ensure
69
+ mocked_request.define_singleton_method(:perform, original_perform)
70
+ Chewy::Search::Request.define_singleton_method(:new, original_new)
71
+ end
72
+
73
+ # Mock Elasticsearch response with defined sources
74
+ # Simple usage - just pass index, expected sources
75
+ # and block with the query.
76
+ #
77
+ # @param index [Chewy::Index] the index to watch, eg EntitiesIndex.
78
+ # @param hits [Hash] hash with sources.
79
+ #
80
+ def mock_elasticsearch_response_sources(index, hits, &block)
81
+ raw_response = {
82
+ 'took' => 4,
83
+ 'timed_out' => false,
84
+ '_shards' => {
85
+ 'total' => 1,
86
+ 'successful' => 1,
87
+ 'skipped' => 0,
88
+ 'failed' => 0
89
+ },
90
+ 'hits' => {
91
+ 'total' => {
92
+ 'value' => hits.count,
93
+ 'relation' => 'eq'
94
+ },
95
+ 'max_score' => 1.0,
96
+ 'hits' => hits.each_with_index.map do |hit, i|
97
+ {
98
+ '_index' => index.index_name,
99
+ '_type' => '_doc',
100
+ '_id' => hit[:id] || (i + 1).to_s,
101
+ '_score' => 3.14,
102
+ '_source' => hit
103
+ }
104
+ end
105
+ }
106
+ }
107
+
108
+ mock_elasticsearch_response(index, raw_response, &block)
109
+ end
110
+
111
+ # Check the assertion that actual Elasticsearch query is rendered
112
+ # to the expected query
113
+ #
114
+ # @param query [::Query] the actual Elasticsearch query.
115
+ # @param expected_query [Hash] expected query.
116
+ #
117
+ # @return [Boolean]
118
+ # True - in the case when actual Elasticsearch query is rendered to the expected query.
119
+ # False - in the opposite case.
120
+ #
121
+ def assert_elasticsearch_query(query, expected_query)
122
+ actual_query = query.render
123
+ assert_equal expected_query, actual_query, "got #{actual_query.inspect} instead of expected query."
124
+ end
125
+
49
126
  module ClassMethods
50
127
  # Declare that all tests in this file require real indexing, always.
51
128
  # In my completely unscientific experiments, this roughly doubled test runtime.
@@ -6,6 +6,8 @@
6
6
  # The class will capture the data from the *param on the Chewy::Index.bulk method and
7
7
  # aggregate the data for test analysis.
8
8
  class SearchIndexReceiver
9
+ MUTATION_FOR_CLASS = Struct.new(:indexes, :deletes, keyword_init: true)
10
+
9
11
  def initialize
10
12
  @mutations = {}
11
13
  end
@@ -71,6 +73,6 @@ private
71
73
  # @param index [Chewy::Index] the index to fetch.
72
74
  # @return [#indexes, #deletes] an object with a list of indexes and a list of deletes.
73
75
  def mutation_for(index)
74
- @mutations[index] ||= OpenStruct.new(indexes: [], deletes: [])
76
+ @mutations[index] ||= MUTATION_FOR_CLASS.new(indexes: [], deletes: [])
75
77
  end
76
78
  end