chewy 8.0.1 → 8.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36ec23aabab2b6b62d9425ad6505f5589958400f21411c90d9808113336aac35
4
- data.tar.gz: 0b082e6327fa4092477c7888714662f791a940b9a1045047ce7a183d41656f2c
3
+ metadata.gz: 4797e925ff275cb6b742ef6dd7be3a03d6033f73c93453305d08383c11252c00
4
+ data.tar.gz: 46b165cfeda3df1558221bc668a7749b1c98ec8a937e4956f710dfbe93934cb1
5
5
  SHA512:
6
- metadata.gz: ad839b902eeb12a5f94547e21f0ed9b2d078be1869b0070be7698fd357edde8d6d759502301dd823eb53d8a3fa42bd6d5584cee53dd482df6c05d1b46e2d110a
7
- data.tar.gz: 3fdef90ea2c73f41274556d5f001e7931bcbbb9b2bdd02df6204fbd75c5b8139ed2cbff00cd448922e8215d2f35f6f3695bd52505db2f30cd20968b54732c401
6
+ metadata.gz: 6451084c18ae23ee908f0ba6c00cffa2cc5abcec29ecc4c911f7e8340081f4b0b89c66d91f2c9dda988c412bcd5a995705ff219d00147c4d52209749f375ff84
7
+ data.tar.gz: b2dd7dda7bae223e450165ff35be42ea814b57d8f06cd0546970a0aed3bed113f290ccc233724248a0cdce3af7b9ef8ac60d2e0e155b1dc6f19376375ccb1990
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## master (unreleased)
4
+
5
+ ### New Features
6
+
7
+ ### Bug Fixes
8
+
9
+ ### Changes
10
+
11
+ ## 8.2.0 (2026-05-29)
12
+
13
+ ### New Features
14
+
15
+ * Add `progressbar:` option to `import`/`import!` and a `PROGRESS=1` rake env toggle for `chewy:reset` / `chewy:update`. The bar is opt-in (default `false`), supports a `:unbounded` spinner mode that skips the `import_count` query, and is safe in parallel mode — workers stay process-based, the bar is incremented in the parent via `Parallel`'s `finish:` callback. Reintroduces the feature originally added in [#787](https://github.com/toptal/chewy/pull/787) and reverted in [#800](https://github.com/toptal/chewy/pull/800) without the GVL regression.
16
+
17
+ ### Bug Fixes
18
+
19
+ * Fix race condition during `reset!` where the unsuffixed concrete index could be recreated by a concurrent process between the delete and the alias creation, causing the alias creation to fail with an index/alias name collision.
20
+ The delete + alias-add are now performed in a single atomic `_aliases` cluster state update via the `remove_index` action.
21
+ No public API or layout change.
22
+ * [#992](https://github.com/toptal/chewy/issues/992): `import(update_fields: [])` is now a no-op (zero fields to update) instead of triggering a full document reindex. The default of `update_fields` is now `nil` (still performs a full reindex). Behavior change: any caller passing an explicit empty array previously got a silent full reimport; they will now skip the update.
23
+
24
+ ### Changes
25
+
26
+ * [#1024](https://github.com/toptal/chewy/pull/1024): Replace deprecated `ZRANGEBYSCORE` with `ZRANGE ... BYSCORE` in the `delayed_sidekiq` worker Lua script. `ZRANGEBYSCORE` has been deprecated in Redis since 6.2.0.
27
+
28
+ ## 8.1.0 (2026-05-28)
29
+
30
+ ### New Features
31
+
32
+ * [#887](https://github.com/toptal/chewy/pull/887): Add support [runtime_mappings](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html). ([@TakuyaKurimoto](https://github.com/TakuyaKurimoto))
33
+ * [#996](https://github.com/toptal/chewy/pull/996): Add `context:` option to `import`/`import!` for passing custom data to crutch blocks and field value procs without redundant DB queries.
34
+
35
+ ### Bug Fixes
36
+
37
+ * [#878](https://github.com/toptal/chewy/issues/878): Flatten `Chewy::Stash::Journal.for` to use a single `terms` filter instead of a chain of `bool.should` clauses. Fixes Elasticsearch `indices.query.bool.max_nested_depth` errors when applying or cleaning the journal across many indices.
38
+
39
+ ### Changes
40
+
41
+ * [#916](https://github.com/toptal/chewy/pull/916): Raise error in #scroll_batches when search backend returns a failure. ([@tomdev][])
42
+ * [#1008](https://github.com/toptal/chewy/pull/1008): Promote Elasticsearch to a native GitHub Actions service with a health-check gate, replacing the fragile `docker compose` + `sleep 15` approach. ([@mattmenefee][])
43
+ * [#1010](https://github.com/toptal/chewy/pull/1010): Add Chewy 7/ES 7 to Chewy 8/ES 8 migration guide and fix stale `Elasticsearch::Transport` namespace references in docs. ([@mattmenefee][])
44
+ * [#1011](https://github.com/toptal/chewy/pull/1011): Replace deprecated `Sidekiq::Testing` API with new `Sidekiq 8.1+` testing API and silence Sidekiq logger during spec runs. ([@mattmenefee][], [@mjankowski][])
45
+ * [#1013](https://github.com/toptal/chewy/pull/1013): Fix `drop_indices` test helper to use `format: 'json'` for ES version portability. If you define a custom `drop_indices` helper in your test suite, update it to use `Chewy.client.cat.indices(format: 'json')` instead of parsing the text-format response. ([@mattmenefee][])
46
+ * [#1014](https://github.com/toptal/chewy/pull/1014): Improve contributing documentation with development setup instructions, PR workflow, and grammar fixes. ([@mattmenefee][])
47
+
3
48
  ## 8.0.1 (2026-03-12)
4
49
 
5
50
  ### New Features
@@ -874,10 +919,12 @@
874
919
  [@marshall]: https://github.com/marshall
875
920
  [@matchbookmac]: https://github.com/matchbookmac
876
921
  [@matthee]: https://github.com/matthee
922
+ [@mattmenefee]: https://github.com/mattmenefee
877
923
  [@mattzollinhofer]: https://github.com/mattzollinhofer
878
924
  [@menglewis]: https://github.com/menglewis
879
925
  [@mikeyhogarth]: https://github.com/mikeyhogarth
880
926
  [@milk1000cc]: https://github.com/milk1000cc
927
+ [@mjankowski]: https://github.com/mjankowski
881
928
  [@mkcode]: https://github.com/mkcode
882
929
  [@mpeychich]: https://github.com/mpeychich
883
930
  [@mrbrdo]: https://github.com/mrbrdo
@@ -901,6 +948,7 @@
901
948
  [@socialchorus]: https://github.com/socialchorus
902
949
  [@taylor-au]: https://github.com/taylor-au
903
950
  [@TikiTDO]: https://github.com/TikiTDO
951
+ [@tomdev]: https://github.com/tomdev
904
952
  [@undr]: https://github.com/undr
905
953
  [@Vitalina-Vakulchyk]: https://github.com/Vitalina-Vakulchyk
906
954
  [@webgago]: https://github.com/webgago
data/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/chewy.svg)](http://badge.fury.io/rb/chewy)
2
2
  [![GitHub Actions](https://github.com/toptal/chewy/actions/workflows/ruby.yml/badge.svg)](https://github.com/toptal/chewy/actions/workflows/ruby.yml)
3
- [![Code Climate](https://codeclimate.com/github/toptal/chewy.svg)](https://codeclimate.com/github/toptal/chewy)
4
- [![Inline docs](http://inch-ci.org/github/toptal/chewy.svg?branch=master)](http://inch-ci.org/github/toptal/chewy)
5
3
 
6
4
  # Chewy
7
5
 
@@ -47,7 +45,7 @@ Chewy aims to support all Ruby and Rails versions that are currently maintained
47
45
 
48
46
  ### Ruby
49
47
 
50
- Chewy is compatible with MRI 3.2-3.4.
48
+ Chewy is compatible with MRI 3.2-4.0.
51
49
 
52
50
  ### Elasticsearch compatibility matrix
53
51
 
@@ -274,19 +272,7 @@ Use the standard client settings with your Cloud credentials (API key or user/pa
274
272
 
275
273
  ## Contributing
276
274
 
277
- 1. Fork it (http://github.com/toptal/chewy/fork)
278
- 2. Create your feature branch (`git checkout -b my-new-feature`)
279
- 3. Implement your changes, cover it with specs and make sure old specs are passing
280
- 4. Commit your changes (`git commit -am 'Add some feature'`)
281
- 5. Push to the branch (`git push origin my-new-feature`)
282
- 6. Create new Pull Request
283
-
284
- Use the following Rake tasks to control the Elasticsearch cluster while developing, if you prefer native Elasticsearch installation over the dockerized one:
285
-
286
- ```bash
287
- rake elasticsearch:start # start Elasticsearch cluster on 9250 port for tests
288
- rake elasticsearch:stop # stop Elasticsearch
289
- ```
275
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup instructions, how to run tests, and the pull request workflow.
290
276
 
291
277
  ## Copyright
292
278
 
data/lib/chewy/errors.rb CHANGED
@@ -37,6 +37,9 @@ module Chewy
37
37
  end
38
38
  end
39
39
 
40
+ class MissingHitsInScrollError < Error
41
+ end
42
+
40
43
  class ImportScopeCleanupError < Error
41
44
  end
42
45
 
@@ -61,8 +61,8 @@ module Chewy
61
61
  # @param fields [Array<Symbol>] a list of fields to compose, every field will be composed if empty
62
62
  # @return [Hash] JSON-ready hash with stringified keys
63
63
  #
64
- def compose(object, crutches = nil, fields: [])
65
- result = evaluate([object, crutches])
64
+ def compose(object, crutches = nil, fields: [], context: {})
65
+ result = evaluate([object, crutches, context])
66
66
 
67
67
  if children.present?
68
68
  child_fields = if fields.present?
@@ -72,7 +72,7 @@ module Chewy
72
72
  end
73
73
 
74
74
  child_fields.each_with_object({}) do |field, memo|
75
- memo.merge!(field.compose(result, crutches) || {})
75
+ memo.merge!(field.compose(result, crutches, context) || {})
76
76
  end.as_json
77
77
  elsif fields.present?
78
78
  result.as_json(only: fields, root: false)
@@ -7,12 +7,13 @@ module Chewy
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  module ClassMethods
10
- # Checks index existance. Returns true or false
10
+ # Checks index existance. Supports suffixes. Returns true or false
11
11
  #
12
12
  # UsersIndex.exists? #=> true
13
+ # UsersIndex.exists?('11-2024') #=> false
13
14
  #
14
- def exists?
15
- client.indices.exists(index: index_name)
15
+ def exists?(suffix = nil)
16
+ client.indices.exists(index: index_name(suffix: suffix))
16
17
  end
17
18
 
18
19
  # Creates index and applies mappings and settings.
@@ -163,16 +164,17 @@ module Chewy
163
164
  ))
164
165
  original_index_settings suffixed_name
165
166
 
166
- delete if indexes.blank?
167
- client.indices.update_aliases body: {actions: [
168
- *indexes.map do |index|
169
- {remove: {index: index, alias: general_name}}
170
- end,
171
- {add: {index: suffixed_name, alias: general_name}}
172
- ]}
167
+ actions = indexes.map { |index| {remove: {index: index, alias: general_name}} }
168
+ actions << {add: {index: suffixed_name, alias: general_name}}
169
+ if indexes.blank? && exists?
170
+ index_names = client.indices.get_alias(index: general_name).keys
171
+ actions << {remove_index: {index: index_names.join(',')}}
172
+ end
173
+
174
+ client.indices.update_aliases body: {actions: actions}
173
175
  client.indices.delete index: indexes if indexes.present?
174
176
 
175
- self.journal.apply(start_time, **import_options) if apply_journal
177
+ self.journal.apply(start_time, **import_options.except(:progressbar)) if apply_journal
176
178
  result
177
179
  else
178
180
  purge!
@@ -146,6 +146,23 @@ module Chewy
146
146
  collection.each_slice(options[:batch_size], &block)
147
147
  end
148
148
 
149
+ # Returns the count of objects that would be imported. Used by the
150
+ # progressbar feature to set the total. Mirrors {#import_args} input
151
+ # handling but does not enumerate batches.
152
+ #
153
+ # @return [Integer]
154
+ def import_count(*args)
155
+ args = args.dup
156
+ args.extract_options!
157
+ collection = if args.empty? && @target.respond_to?(import_all_method)
158
+ @target.send(import_all_method)
159
+ else
160
+ args.flatten(1).compact
161
+ end
162
+
163
+ collection.count
164
+ end
165
+
149
166
  # This method is used internally by the request DSL when the
150
167
  # collection of ORM/ODM objects is requested.
151
168
  #
@@ -96,6 +96,26 @@ module Chewy
96
96
  end
97
97
  alias_method :import_references, :import_fields
98
98
 
99
+ # Returns the count of records that would be imported. Used by the
100
+ # progressbar feature to set the total. Accepts the same shapes as
101
+ # {#import}: nothing (uses default scope), a relation, or an array
102
+ # of ids/objects.
103
+ #
104
+ # @return [Integer]
105
+ def import_count(*args)
106
+ args = args.dup
107
+ args.extract_options!
108
+ collection = if args.empty?
109
+ default_scope
110
+ elsif args.first.is_a?(relation_class)
111
+ args.first
112
+ else
113
+ args.flatten.compact
114
+ end
115
+
116
+ collection.count
117
+ end
118
+
99
119
  def load(ids, **options)
100
120
  scope = all_scope_where_ids_in(ids)
101
121
  additional_scope = options[options[:_index].to_sym].try(:[], :scope) || options[:scope]
@@ -9,9 +9,12 @@ module Chewy
9
9
  end
10
10
 
11
11
  class Crutches
12
- def initialize(index, collection)
12
+ attr_reader :context
13
+
14
+ def initialize(index, collection, context = {})
13
15
  @index = index
14
16
  @collection = collection
17
+ @context = context
15
18
  @crutches_instances = {}
16
19
  end
17
20
 
@@ -26,7 +29,14 @@ module Chewy
26
29
  end
27
30
 
28
31
  def [](name)
29
- @crutches_instances[name] ||= @index._crutches[:"#{name}"].call(@collection)
32
+ @crutches_instances[name] ||= begin
33
+ block = @index._crutches[:"#{name}"]
34
+ if block.arity > 1 || block.arity < -1
35
+ block.call(@collection, @context)
36
+ else
37
+ block.call(@collection)
38
+ end
39
+ end
30
40
  end
31
41
  end
32
42
 
@@ -13,11 +13,12 @@ module Chewy
13
13
  # @param to_index [Array<Object>] objects to index
14
14
  # @param delete [Array<Object>] objects or ids to delete
15
15
  # @param fields [Array<Symbol, String>] and array of fields for documents update
16
- def initialize(index, to_index: [], delete: [], fields: [])
16
+ def initialize(index, to_index: [], delete: [], fields: nil, context: {})
17
17
  @index = index
18
18
  @to_index = to_index
19
19
  @delete = delete
20
- @fields = fields.map!(&:to_sym)
20
+ @fields = fields&.map(&:to_sym)
21
+ @context = context
21
22
  end
22
23
 
23
24
  # Returns ES API-ready bulk requiest body.
@@ -42,10 +43,12 @@ module Chewy
42
43
  private
43
44
 
44
45
  def crutches_for_index
45
- @crutches_for_index ||= Chewy::Index::Crutch::Crutches.new @index, @to_index
46
+ @crutches_for_index ||= Chewy::Index::Crutch::Crutches.new @index, @to_index, @context
46
47
  end
47
48
 
48
49
  def index_entry(object)
50
+ return [] if @fields&.empty?
51
+
49
52
  entry = {}
50
53
  entry[:_id] = index_object_ids[object] if index_object_ids[object]
51
54
  entry[:routing] = routing(object) if join_field?
@@ -257,13 +260,13 @@ module Chewy
257
260
  end
258
261
 
259
262
  def data_for(object, fields: [], crutches: crutches_for_index)
260
- @index.compose(object, crutches, fields: fields)
263
+ @index.compose(object, crutches, fields: fields, context: @context)
261
264
  end
262
265
 
263
266
  def parent_changed?(data, old_parent)
264
267
  return false unless old_parent
265
268
  return false unless join_field?
266
- return false unless @fields.include?(join_field.to_sym)
269
+ return false unless @fields&.include?(join_field.to_sym)
267
270
  return false unless data.key?(join_field)
268
271
 
269
272
  # The join field value can be a hash, e.g.:
@@ -0,0 +1,79 @@
1
+ module Chewy
2
+ class Index
3
+ module Import
4
+ # Thin wrapper around `ruby-progressbar` for import feedback.
5
+ #
6
+ # Unlike the original PR #787 implementation, this wrapper is only
7
+ # touched from the parent process: serial imports increment it directly,
8
+ # and parallel imports increment it via `Parallel`'s `finish:` callback
9
+ # (which runs in the parent under an internal mutex). The workers stay
10
+ # process-based, so there is no GVL contention as in PR #787 / #800.
11
+ #
12
+ # `Progressbar.build` returns a NULL object when the feature is disabled,
13
+ # so call sites do not need feature guards.
14
+ class Progressbar
15
+ NULL = Object.new
16
+ class << NULL
17
+ def increment(_); end
18
+ def total=(_); end
19
+ def finish; end
20
+ end
21
+
22
+ BOUNDED_FORMAT = '%t |%B| %p%% %c/%C %e'.freeze
23
+ UNBOUNDED_FORMAT = '%t %c (%a)'.freeze
24
+ TITLE = 'Importing'.freeze
25
+
26
+ # @param enabled [Boolean, :unbounded] feature flag. `:unbounded` shows
27
+ # a spinner with no total (skip `import_count`).
28
+ # @param total [Integer, nil] expected total; ignored when `:unbounded`.
29
+ # @return [Progressbar, NULL]
30
+ def self.build(enabled, total)
31
+ return NULL unless enabled
32
+
33
+ unless '::ProgressBar'.safe_constantize
34
+ raise 'The `ruby-progressbar` gem is required for import progress, ' \
35
+ "please add `gem 'ruby-progressbar'` to your Gemfile"
36
+ end
37
+
38
+ return new if enabled == :unbounded
39
+
40
+ new(normalize_total(total))
41
+ end
42
+
43
+ # Some ActiveRecord scopes (e.g., `.group(...)`) make `.count` return a
44
+ # Hash rather than an Integer. Coerce so we still get a usable total.
45
+ def self.normalize_total(total)
46
+ case total
47
+ when Hash then total.values.sum
48
+ when Integer then total
49
+ end
50
+ end
51
+
52
+ attr_reader :bar
53
+
54
+ def initialize(total = nil)
55
+ format = total ? BOUNDED_FORMAT : UNBOUNDED_FORMAT
56
+ @bar = ::ProgressBar.create(title: TITLE, total: total, format: format)
57
+ end
58
+
59
+ # Clamps to total when bounded — action_objects may include :delete
60
+ # entries (parent-child re-indexing, delete_if scope) that aren't
61
+ # counted by `adapter.import_count`, which would otherwise raise
62
+ # ProgressBar::InvalidProgressError.
63
+ def increment(by)
64
+ target = bar.progress + by
65
+ target = [bar.total, target].min if bar.total
66
+ bar.progress = target
67
+ end
68
+
69
+ def total=(value)
70
+ bar.total = value
71
+ end
72
+
73
+ def finish
74
+ bar.finish unless bar.finished?
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -31,7 +31,7 @@ module Chewy
31
31
 
32
32
  DEFAULT_OPTIONS = {
33
33
  refresh: true,
34
- update_fields: [],
34
+ update_fields: nil,
35
35
  update_failover: true,
36
36
  batch_size: Chewy::Index::Adapter::Base::BATCH_SIZE
37
37
  }.freeze
@@ -56,6 +56,7 @@ module Chewy
56
56
  {}
57
57
  end
58
58
  end
59
+ @context = @options[:context] || {}
59
60
  @errors = []
60
61
  @stats = {}
61
62
  @leftovers = []
@@ -78,7 +79,7 @@ module Chewy
78
79
  # @param delete [Array<Object>] any acceptable objects for deleting
79
80
  # @return [true, false] the result of the request, true if no errors
80
81
  def process(index: [], delete: [])
81
- bulk_builder = BulkBuilder.new(@index, to_index: index, delete: delete, fields: @options[:update_fields])
82
+ bulk_builder = BulkBuilder.new(@index, to_index: index, delete: delete, fields: @options[:update_fields], context: @context)
82
83
  bulk_body = bulk_builder.bulk_body
83
84
 
84
85
  if @options[:journal]
@@ -2,6 +2,7 @@ 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/progressbar'
5
6
 
6
7
  module Chewy
7
8
  class Index
@@ -11,20 +12,22 @@ module Chewy
11
12
  IMPORT_WORKER = lambda do |index, options, total, ids, iteration|
12
13
  ::Process.setproctitle("chewy [#{index}]: import data (#{iteration + 1}/#{total})")
13
14
  routine = Routine.new(index, **options)
15
+ processed = 0
14
16
  index.adapter.import(*ids, routine.options) do |action_objects|
15
17
  routine.process(**action_objects)
18
+ processed += action_objects.sum { |_, v| v.size }
16
19
  end
17
- {errors: routine.errors, import: routine.stats, leftovers: routine.leftovers}
20
+ {errors: routine.errors, import: routine.stats, leftovers: routine.leftovers, processed: processed}
18
21
  end
19
22
 
20
23
  LEFTOVERS_WORKER = lambda do |index, options, total, body, iteration|
21
24
  ::Process.setproctitle("chewy [#{index}]: import leftovers (#{iteration + 1}/#{total})")
22
25
  routine = Routine.new(index, **options)
23
26
  routine.perform_bulk(body)
24
- routine.errors
27
+ {errors: routine.errors}
25
28
  end
26
29
 
27
- module ClassMethods
30
+ module ClassMethods # rubocop:disable Metrics/ModuleLength
28
31
  # @!method import(*collection, **options)
29
32
  # Basically, one of the main methods for an index. Performs any objects import
30
33
  # to the index. Does all the objects handling routines.
@@ -68,9 +71,15 @@ module Chewy
68
71
  # @option options [Integer] batch_size passed to the adapter import method, used to split imported objects in chunks, 1000 by default
69
72
  # @option options [Boolean] direct_import skips object reloading in ORM adapter, `false` by default
70
73
  # @option options [true, false] journal enables imported objects journaling, false by default
71
- # @option options [Array<Symbol, String>] update_fields list of fields for the partial import, empty by default
74
+ # @option options [Array<Symbol, String>] update_fields list of fields for partial import. `nil` (default) triggers full document reindex;
75
+ # an empty array (`[]`) is an explicit no-op (no fields updated).
72
76
  # @option options [true, false] update_failover enables full objects reimport in cases of partial update errors, `true` by default
73
77
  # @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
78
+ # @option options [true, false, :unbounded] progressbar shows an import progressbar
79
+ # on stderr. `true` precomputes the total via `adapter.import_count` (one extra
80
+ # count query); `:unbounded` shows a spinner without computing the total. Default
81
+ # `false`. Safe in parallel mode: the bar is incremented in the parent process via
82
+ # `Parallel`'s `finish:` callback, workers stay process-based.
74
83
  # @return [true, false] false in case of errors
75
84
  def import(*args)
76
85
  intercept_import_using_strategy(*args).blank?
@@ -115,13 +124,13 @@ module Chewy
115
124
  # @param crutches [Object] optional crutches object; if omitted - a crutch for the single passed object is created as a fallback
116
125
  # @param fields [Array<Symbol>] and array of fields to restrict the generated document
117
126
  # @return [Hash] a JSON-ready hash
118
- def compose(object, crutches = nil, fields: [])
119
- crutches ||= Chewy::Index::Crutch::Crutches.new self, [object]
127
+ def compose(object, crutches = nil, fields: [], context: {})
128
+ crutches ||= Chewy::Index::Crutch::Crutches.new self, [object], context
120
129
 
121
130
  if witchcraft? && root.children.present?
122
- cauldron(fields: fields).brew(object, crutches)
131
+ cauldron(fields: fields).brew(object, crutches, context)
123
132
  else
124
- root.compose(object, crutches, fields: fields)
133
+ root.compose(object, crutches, fields: fields, context: context)
125
134
  end
126
135
  end
127
136
 
@@ -175,46 +184,91 @@ module Chewy
175
184
  end
176
185
 
177
186
  def import_linear(objects, routine)
187
+ bar = build_progressbar(routine, objects)
178
188
  ActiveSupport::Notifications.instrument 'import_objects.chewy', index: self do |payload|
179
189
  adapter.import(*objects, routine.options) do |action_objects|
180
190
  routine.process(**action_objects)
191
+ bar.increment(action_objects.sum { |_, v| v.size })
181
192
  end
182
193
  routine.perform_bulk(routine.leftovers)
183
194
  payload[:import] = routine.stats
184
195
  payload[:errors] = payload_errors(routine.errors) if routine.errors.present?
185
196
  payload[:errors]
186
197
  end
198
+ ensure
199
+ bar&.finish
187
200
  end
188
201
 
189
202
  def import_parallel(objects, routine)
190
203
  raise "The `parallel` gem is required for parallel import, please add `gem 'parallel'` to your Gemfile" unless '::Parallel'.safe_constantize
191
204
 
205
+ bar = build_progressbar(routine, objects)
192
206
  ActiveSupport::Notifications.instrument 'import_objects.chewy', index: self do |payload|
193
207
  batches = adapter.import_references(*objects, routine.options.slice(:batch_size)).to_a
194
208
 
195
209
  ::ActiveRecord::Base.connection.close if defined?(::ActiveRecord::Base)
196
210
  results = ::Parallel.map_with_index(
197
211
  batches,
198
- routine.parallel_options,
212
+ parallel_options_with_progress(routine.parallel_options, bar),
199
213
  &IMPORT_WORKER.curry[self, routine.options, batches.size]
200
214
  )
201
215
  ::ActiveRecord::Base.connection.reconnect! if defined?(::ActiveRecord::Base)
202
216
  errors, import, leftovers = process_parallel_import_results(results)
203
-
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
217
+ errors.concat(process_parallel_leftovers(leftovers, routine)) if leftovers.present?
213
218
 
214
219
  payload[:import] = import
215
220
  payload[:errors] = payload_errors(errors) if errors.present?
216
221
  payload[:errors]
217
222
  end
223
+ ensure
224
+ bar&.finish
225
+ end
226
+
227
+ def process_parallel_leftovers(leftovers, routine)
228
+ batches = leftovers.each_slice(routine.options[:batch_size]).to_a
229
+ results = ::Parallel.map_with_index(
230
+ batches,
231
+ routine.parallel_options,
232
+ &LEFTOVERS_WORKER.curry[self, routine.options, batches.size]
233
+ )
234
+ results.flat_map { |r| r[:errors] }
235
+ end
236
+
237
+ # Builds Parallel options with a `finish:` callback that increments the
238
+ # progressbar after each worker batch returns. The callback runs in the
239
+ # parent (main) thread under Parallel's internal mutex (parallel-1.x),
240
+ # so workers stay process-based and there is no worker-side
241
+ # synchronization — the regression that triggered the PR #800 revert.
242
+ #
243
+ # If the caller already supplied a `finish:` callback in
244
+ # `parallel_options`, both run; user callback first, then the bar.
245
+ def parallel_options_with_progress(parallel_options, bar)
246
+ user_finish = parallel_options[:finish]
247
+ progress = lambda do |item, i, result|
248
+ user_finish&.call(item, i, result)
249
+ bar.increment(result[:processed]) if result.is_a?(Hash) && result[:processed]
250
+ end
251
+ parallel_options.merge(finish: progress)
252
+ end
253
+
254
+ def build_progressbar(routine, objects)
255
+ enabled = routine.options[:progressbar]
256
+ total = enabled == true ? safe_import_count(objects) : nil
257
+ Progressbar.build(enabled, total)
258
+ end
259
+
260
+ # Returns nil when the adapter cannot or should not be counted:
261
+ # missing `import_count` on a custom adapter, a grouped scope that
262
+ # raises, or any unexpected count failure. A nil total makes
263
+ # `Progressbar.new` render a spinner instead of a bounded bar —
264
+ # avoids aborting the import just because the progressbar can't
265
+ # size itself.
266
+ def safe_import_count(objects)
267
+ return nil unless adapter.respond_to?(:import_count)
268
+
269
+ adapter.import_count(*objects)
270
+ rescue StandardError
271
+ nil
218
272
  end
219
273
 
220
274
  def process_parallel_import_results(results)
@@ -59,15 +59,15 @@ module Chewy
59
59
  @fields = fields
60
60
  end
61
61
 
62
- def brew(object, crutches = nil)
63
- alicorn.call(locals, object, crutches).as_json
62
+ def brew(object, crutches = nil, context = {})
63
+ alicorn.call(locals, object, crutches, context).as_json
64
64
  end
65
65
 
66
66
  private
67
67
 
68
68
  def alicorn
69
69
  @alicorn ||= singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
70
- -> (locals, object0, crutches) do
70
+ -> (locals, object0, crutches, context) do
71
71
  #{composed_values(@index.root, 0)}
72
72
  end
73
73
  RUBY
@@ -171,7 +171,7 @@ module Chewy
171
171
  end
172
172
  end
173
173
 
174
- def source_for(proc, nesting)
174
+ def source_for(proc, nesting) # rubocop:disable Metrics/AbcSize
175
175
  lambdas = exctract_lambdas(ast_from_proc(proc))
176
176
 
177
177
  raise "No lambdas found, try to reformat your code:\n`#{proc.source}`" unless lambdas
@@ -189,6 +189,7 @@ module Chewy
189
189
  source = replace_lvar(source, proc_params[n], :"object#{n}") if proc_params[n]
190
190
  end
191
191
  source = replace_lvar(source, proc_params[nesting + 1], :crutches) if proc_params[nesting + 1]
192
+ source = replace_lvar(source, proc_params[nesting + 2], :context) if proc_params[nesting + 2]
192
193
 
193
194
  binding_variable_list(source).each do |variable|
194
195
  locals.push(proc.binding.eval(variable.to_s))
@@ -11,7 +11,7 @@ module Chewy
11
11
  # Instantiate a new MultiSearch instance.
12
12
  #
13
13
  # @param queries [Array<Chewy::Search::Request>]
14
- # @option [Elasticsearch::Transport::Client] :client (Chewy.client)
14
+ # @option [Elasticsearch::Client] :client (Chewy.client)
15
15
  # The Elasticsearch client that should be used for issuing requests.
16
16
  def initialize(queries, client: Chewy.client)
17
17
  @client = client
@@ -21,6 +21,8 @@ module Chewy
21
21
 
22
22
  DELETE_BY_QUERY_OPTIONS = %w[WAIT_FOR_COMPLETION REQUESTS_PER_SECOND SCROLL_SIZE].freeze
23
23
  FALSE_VALUES = %w[0 f false off].freeze
24
+ TRUE_VALUES = %w[1 t true on yes].freeze
25
+ UNBOUNDED_VALUES = %w[unbounded].freeze
24
26
 
25
27
  class << self
26
28
  # Performs zero-downtime reindexing of all documents for the specified indexes
@@ -105,7 +107,7 @@ module Chewy
105
107
  indexes_from(only: only, except: except).each_with_object([]) do |index, updated_indexes|
106
108
  if index.exists?
107
109
  output.puts "Updating #{index}"
108
- index.import(parallel: parallel)
110
+ index.import(parallel: parallel, progressbar: progressbar_option)
109
111
  updated_indexes.push(index)
110
112
  else
111
113
  output.puts "Skipping #{index}, it does not exists (use rake chewy:reset[#{index.derivable_name}] to create and update it)"
@@ -336,7 +338,22 @@ module Chewy
336
338
 
337
339
  def reset_one(index, output, parallel: false)
338
340
  output.puts "Resetting #{index}"
339
- index.reset!((Time.now.to_f * 1000).round, parallel: parallel, apply_journal: journal_exists?)
341
+ index.reset!((Time.now.to_f * 1000).round, parallel: parallel, apply_journal: journal_exists?, progressbar: progressbar_option)
342
+ end
343
+
344
+ def progressbar_option
345
+ value = ENV.fetch('PROGRESS', nil)
346
+ return false if value.nil? || value.empty?
347
+
348
+ case value.downcase
349
+ when *FALSE_VALUES then false
350
+ when *UNBOUNDED_VALUES then :unbounded
351
+ when *TRUE_VALUES then true
352
+ else
353
+ warn "PROGRESS=#{value.inspect} not recognized; treating as enabled. " \
354
+ "Use #{TRUE_VALUES.join('/')}, #{UNBOUNDED_VALUES.join('/')}, or #{FALSE_VALUES.join('/')}."
355
+ true
356
+ end
340
357
  end
341
358
 
342
359
  def warn_missing_index(output)
@@ -0,0 +1,14 @@
1
+ module Chewy
2
+ module Search
3
+ class Parameters
4
+ # Just a standard hash storage. Nothing to see here.
5
+ #
6
+ # @see Chewy::Search::Parameters::HashStorage
7
+ # @see Chewy::Search::Request#runtime_mappings
8
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html
9
+ class RuntimeMappings < Storage
10
+ include HashStorage
11
+ end
12
+ end
13
+ end
14
+ end
@@ -25,7 +25,7 @@ module Chewy
25
25
  search_type preference limit offset terminate_after
26
26
  timeout min_score source stored_fields search_after
27
27
  load script_fields suggest aggs aggregations collapse none
28
- indices_boost rescore highlight total total_count
28
+ indices_boost rescore highlight runtime_mappings total total_count
29
29
  total_entries indices types delete_all count exists?
30
30
  exist? find pluck scroll_batches scroll_hits
31
31
  scroll_results scroll_wrappers ignore_unavailable
@@ -656,7 +656,23 @@ module Chewy
656
656
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html
657
657
  # @param value [Hash]
658
658
  # @return [Chewy::Search::Request]
659
- %i[script_fields indices_boost rescore highlight].each do |name|
659
+ #
660
+ # @!method runtime_mappings(value)
661
+ # Add a `runtime_mappings` part to the request. Further
662
+ # call values are merged to the storage hash.
663
+ #
664
+ # @example
665
+ # PlacesIndex
666
+ # .runtime_mappings(field1: {type: "keyword", script: {lang: "painless", source: "emit('some script here')"}})
667
+ # .runtime_mappings(field2: {type: "keyword", script: {lang: "painless", source: "emit('some script here')"}})
668
+ # # => <PlacesIndex::Query {..., :body=>{:runtime_mappings=>{
669
+ # # "field1"=>{:type=>"keyword", :script=>{:lang=>"painless", :source=>"emit('some script here')"}},
670
+ # # "field2"=>{:type=>"keyword", :script=>{:lang=>"painless", :source=>"emit('some script here')"}}}}}>
671
+ # @see Chewy::Search::Parameters::RuntimeMappings
672
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html
673
+ # @param value [Hash]
674
+ # @return [Chewy::Search::Request]
675
+ %i[script_fields indices_boost rescore highlight runtime_mappings].each do |name|
660
676
  define_method name do |value|
661
677
  modify(name) { update!(value) }
662
678
  end
@@ -29,20 +29,28 @@ module Chewy
29
29
 
30
30
  result = perform(size: batch_size, scroll: scroll)
31
31
  total = [raw_limit_value, result.fetch('hits', {}).fetch('total', {}).fetch('value', 0)].compact.min
32
+
33
+ total_batches = total / batch_size
32
34
  last_batch_size = total % batch_size
33
- fetched = 0
35
+
36
+ total_batches += 1 if last_batch_size != 0
37
+
34
38
  scroll_id = nil
35
39
 
36
- loop do
40
+ total_batches.times do |batch_counter|
41
+ last_run = total_batches - 1 == batch_counter
42
+
37
43
  hits = result.fetch('hits', {}).fetch('hits', [])
38
- fetched += hits.size
39
- hits = hits.first(last_batch_size) if last_batch_size != 0 && fetched >= total
44
+ hits = hits.first(last_batch_size) if last_run && last_batch_size != 0
45
+
46
+ raise Chewy::MissingHitsInScrollError if hits.empty?
47
+
40
48
  yield(hits) if hits.present?
41
49
  scroll_id = result['_scroll_id']
42
50
 
43
- break if result['terminated_early'] || fetched >= total
51
+ break if result['terminated_early']
44
52
 
45
- result = perform_scroll(scroll: scroll, scroll_id: scroll_id)
53
+ result = perform_scroll(scroll: scroll, scroll_id: scroll_id) unless last_run
46
54
  end
47
55
  ensure
48
56
  Chewy.client.clear_scroll(body: {scroll_id: scroll_id}) if scroll_id
data/lib/chewy/stash.rb CHANGED
@@ -38,17 +38,21 @@ module Chewy
38
38
 
39
39
  # Selects all the journal entries for the specified indices.
40
40
  #
41
+ # Uses a single `terms` filter rather than a chain of `bool.should`
42
+ # clauses so the query depth stays constant regardless of how many
43
+ # indices are passed. Avoids hitting the Elasticsearch
44
+ # `indices.query.bool.max_nested_depth` limit (default 30) when
45
+ # cleaning or applying journals across many indices.
46
+ #
41
47
  # @param indices [Chewy::Index, Array<Chewy::Index>]
42
48
  def self.for(*something)
43
49
  something = something.flatten.compact
50
+ return all if something.empty?
51
+
44
52
  indexes = something.flat_map { |s| Chewy.derive_name(s) }
45
- return none if something.present? && indexes.blank?
53
+ return none if indexes.blank?
46
54
 
47
- scope = all
48
- indexes.each do |index|
49
- scope = scope.or(filter(term: {index_name: index.derivable_name}))
50
- end
51
- scope
55
+ filter(terms: {index_name: indexes.map(&:derivable_name).uniq})
52
56
  end
53
57
 
54
58
  default_import_options journal: false
@@ -13,7 +13,7 @@ module Chewy
13
13
  local timechunks_key = prefix .. ":" .. type .. ":timechunks"
14
14
 
15
15
  -- Get timechunk_keys with scores less than or equal to the specified score
16
- local timechunk_keys = redis.call('zrangebyscore', timechunks_key, '-inf', score)
16
+ local timechunk_keys = redis.call('zrange', timechunks_key, '-inf', score, 'byscore')
17
17
 
18
18
  -- Get all members from the sets associated with the timechunk_keys
19
19
  local members = {}
data/lib/chewy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '8.0.1'.freeze
2
+ VERSION = '8.2.0'.freeze
3
3
  end
data/lib/chewy.rb CHANGED
@@ -31,6 +31,7 @@ end
31
31
  try_require 'kaminari'
32
32
  try_require 'kaminari/core'
33
33
  try_require 'parallel'
34
+ try_require 'ruby-progressbar'
34
35
 
35
36
  ActiveSupport.on_load(:active_record) do
36
37
  try_require 'kaminari/activerecord'
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chewy
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.1
4
+ version: 8.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal, LLC
8
8
  - pyromaniac
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2026-03-12 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: activesupport
@@ -89,6 +88,7 @@ files:
89
88
  - lib/chewy/index/import/bulk_builder.rb
90
89
  - lib/chewy/index/import/bulk_request.rb
91
90
  - lib/chewy/index/import/journal_builder.rb
91
+ - lib/chewy/index/import/progressbar.rb
92
92
  - lib/chewy/index/import/routine.rb
93
93
  - lib/chewy/index/mapping.rb
94
94
  - lib/chewy/index/observe.rb
@@ -147,6 +147,7 @@ files:
147
147
  - lib/chewy/search/parameters/query.rb
148
148
  - lib/chewy/search/parameters/request_cache.rb
149
149
  - lib/chewy/search/parameters/rescore.rb
150
+ - lib/chewy/search/parameters/runtime_mappings.rb
150
151
  - lib/chewy/search/parameters/script_fields.rb
151
152
  - lib/chewy/search/parameters/search_after.rb
152
153
  - lib/chewy/search/parameters/search_type.rb
@@ -186,7 +187,6 @@ licenses:
186
187
  - MIT
187
188
  metadata:
188
189
  rubygems_mfa_required: 'true'
189
- post_install_message:
190
190
  rdoc_options: []
191
191
  require_paths:
192
192
  - lib
@@ -201,8 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
201
  - !ruby/object:Gem::Version
202
202
  version: '0'
203
203
  requirements: []
204
- rubygems_version: 3.4.19
205
- signing_key:
204
+ rubygems_version: 4.0.12
206
205
  specification_version: 4
207
206
  summary: Elasticsearch ODM client wrapper
208
207
  test_files: []