chewy 8.2.1 → 8.3.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: ec704929088d1e1f785d46b74dd62197c2a173ff5b405d500ab4068b6025d407
4
- data.tar.gz: ed17897341f8c43713c41d6e0b22a71afa13899a3c553baf6d034072ad40712a
3
+ metadata.gz: 34d773f10bf67d60eca51ff91a6d668186eea74515bb5b31a5fd5ce814dac3c7
4
+ data.tar.gz: 334c6e92185369170b9f4d534029b69c218bb3c0cabfe6e4c11fd79268c19587
5
5
  SHA512:
6
- metadata.gz: e957fc1353e97b268ead1e99ac9665eac7bd8d0d4eceaff205c07763c7690f8ea173844aeac1be7a9f1af6138b875f49508ed68a21d92814660916aa057566ad
7
- data.tar.gz: d3ece465fadfae3486f28303eadd1919e5b760e797e41ab231d797363c9ed5143fa616b270f342172ef63bd128780f21ca54f1675d6507749ae5c7aca1278025
6
+ metadata.gz: 24bde735bcfc4f1bfec8b146cdcc5024d049e1405ccfbbae5e6f57f53a89aa9a6951063b00251a2f87fafac3632e1640fddc64038c7337e9147a0b9d6740f984
7
+ data.tar.gz: d2d84b1d2d3b07bde52699dfc54ed3b9ac8b9b71c5d79ca204851abab06bf53a41f0591ded6cdab59be5bf87522374dc10ff01d180891bcf81d73d33b009e180
data/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## master (unreleased
3
+ ## master (unreleased)
4
4
 
5
5
  ### New Features
6
6
 
@@ -8,6 +8,20 @@
8
8
 
9
9
  ### Changes
10
10
 
11
+ ## 8.3.0 (2026-06-03)
12
+
13
+ ### New Features
14
+
15
+ * New compiled compose path is now the default for every index. Chewy generates one `__chewy_compose__` method per index from the field tree on first import and reuses it for every object, replacing the iterative `Fields::Root#compose` loop. Typical document composition is 3-4× faster than the iterative path and matches `witchcraft!` performance without any extra dependencies. `update_fields:` partial imports are also covered via per-fields-set memoized methods.
16
+
17
+ ### Bug Fixes
18
+
19
+ ### Changes
20
+
21
+ * **Deprecation:** `Chewy::Index.witchcraft!` is deprecated and will be removed in a future major release. It now prints a deprecation warning and is no longer required for fast composition — the compiled compose path described above is the default and delivers equivalent throughput without `method_source` / `parser` / `prism` / `unparser`. The `parser` / `prism` / `unparser` requires are now lazy and only loaded when an index actually calls `witchcraft!`, eliminating ~24 MiB of boot-time allocations and ~1 MiB of retained memory for apps that don't use it ([#644](https://github.com/toptal/chewy/issues/644)).
22
+
23
+ * [#1028](https://github.com/toptal/chewy/pull/1028): Remove the obsolete `_type` field from mock response helpers (`Chewy::Minitest::Helpers`, `Chewy::Rspec::Helpers`), `EVERFIELDS`, and `Chewy::Index::Wrapper` accessors. Elasticsearch removed `_type` from search responses in 8.0 (ES 7 already returned only the placeholder `_doc`), so these references no longer matched real responses. Also removes the long-obsolete `_parent` entry from `EVERFIELDS`. ([@mattmenefee][])
24
+
11
25
  ## 8.2.1 (2026-06-01)
12
26
 
13
27
  ### New Features
data/README.md CHANGED
@@ -218,7 +218,6 @@ end
218
218
  },
219
219
  "_data":{
220
220
  "_index":"users",
221
- "_type":"_doc",
222
221
  "_id":"1",
223
222
  "_score":0.9808291,
224
223
  "_source":{
@@ -236,7 +235,7 @@ end
236
235
 
237
236
  - [Getting Started](docs/getting_started.md) — end-to-end tutorial building search for a media library
238
237
  - [Configuration](docs/configuration.md) — client settings, update strategies, notifications, integrations
239
- - [Indexing](docs/indexing.md) — index definition, field types, crutches, witchcraft, index manipulation
238
+ - [Indexing](docs/indexing.md) — index definition, field types, crutches, compiled compose path, index manipulation
240
239
  - [Import](docs/import.md) — import options, raw import, journaling
241
240
  - [Querying](docs/querying.md) — search requests, pagination, scopes, scroll, loading
242
241
  - [Rake Tasks](docs/rake_tasks.md) — all rake tasks and parallelization
@@ -3,10 +3,12 @@ module Chewy
3
3
  class Root < Chewy::Fields::Base
4
4
  attr_reader :dynamic_templates, :id
5
5
 
6
+ DEFAULT_VALUE = -> { self }
7
+
6
8
  def initialize(name, **options)
7
9
  super
8
10
 
9
- @value ||= -> { self }
11
+ @value ||= DEFAULT_VALUE
10
12
  @dynamic_templates = []
11
13
  end
12
14
 
@@ -0,0 +1,235 @@
1
+ module Chewy
2
+ class Index
3
+ # Compiled compose path. Default for all indexes.
4
+ #
5
+ # Generates a single `__chewy_compose__` method per index from the
6
+ # field tree (no Ruby source parsing of user procs). Bakes the
7
+ # per-field arity into the call site and inlines hash construction,
8
+ # removing the per-field iteration + dispatch overhead of the legacy
9
+ # plain compose path. Proc bodies are NOT inlined — procs are called
10
+ # via `.call`. In exchange there are no parser/unparser/method_source
11
+ # deps and no Ruby/Prism version coupling.
12
+ #
13
+ # `fields:` selection is supported: a dedicated method is generated
14
+ # and memoized per unique fields set.
15
+ #
16
+ # Falls back to the plain `Chewy::Fields::Root#compose` path when the
17
+ # field tree uses features the compiler does not handle:
18
+ # ignore_blank, geo_point, custom root value.
19
+ module Compiled
20
+ extend ActiveSupport::Concern
21
+
22
+ class UNSUPPORTED < StandardError
23
+ end
24
+
25
+ COMPILE_MUTEX = Mutex.new
26
+
27
+ module ClassMethods
28
+ # Returns true when this index can use the compiled path for the
29
+ # given compose call. Builds the per-fields method if needed.
30
+ def compiled_compose_available?(fields)
31
+ return false if witchcraft?
32
+ return false unless root&.children&.present?
33
+
34
+ ensure_compiled_compose_for!(fields)
35
+ rescue UNSUPPORTED
36
+ false
37
+ end
38
+
39
+ def compiled_compose(object, crutches, context, fields)
40
+ send(compiled_method_name(fields), object, crutches, context)
41
+ end
42
+
43
+ def compiled_procs
44
+ @compiled_procs ||= []
45
+ end
46
+
47
+ # Build (and cache) the compiled compose method for the given
48
+ # fields restriction. Returns true on success, false / raises on
49
+ # unsupported. Thread-safe: compilation under a process-wide
50
+ # mutex prevents two threads from interleaving appends into
51
+ # @compiled_procs and baking duplicate indices into class_eval'd
52
+ # method bodies.
53
+ def ensure_compiled_compose_for!(fields)
54
+ key = fields_key(fields)
55
+ built = (@compiled_compose_built ||= {})
56
+ return built[key] if built.key?(key)
57
+
58
+ COMPILE_MUTEX.synchronize do
59
+ return built[key] if built.key?(key)
60
+
61
+ method_name = compiled_method_name(fields)
62
+ source = Compiler.new(self, fields: fields, method_name: method_name).build
63
+ class_eval(source, __FILE__, __LINE__)
64
+ built[key] = true
65
+ end
66
+ rescue UNSUPPORTED
67
+ (@compiled_compose_built ||= {})[fields_key(fields)] = false
68
+ raise
69
+ end
70
+
71
+ # Clears compiled-method cache and rebuilds-on-next-call. Hooked
72
+ # from `Mapping.field` so a `field` added after the first compose
73
+ # is picked up rather than absent from a stale generated method.
74
+ # Existing singleton methods remain defined (they shadow until
75
+ # rebuilt) but the cache flag is dropped so the next compose
76
+ # rebuilds and reassigns them.
77
+ def invalidate_compiled_compose!
78
+ @compiled_compose_built = nil
79
+ @compiled_procs = nil
80
+ @compiled_default_ready = false
81
+ end
82
+
83
+ def compiled_method_name(fields)
84
+ key = fields_key(fields)
85
+ return :__chewy_compose__ if key.empty?
86
+
87
+ :"__chewy_compose__#{key}"
88
+ end
89
+
90
+ private
91
+
92
+ # Distinct field sets are kept distinct by hashing the joined,
93
+ # sorted symbol names rather than joining with a separator that
94
+ # collides with `_` inside field names (e.g. [:full_name, :id]
95
+ # vs [:full, :name_id]). Hex-encoding the digest keeps the
96
+ # method name a valid Ruby identifier.
97
+ def fields_key(fields)
98
+ return ''.freeze if fields.nil? || fields.empty?
99
+
100
+ require 'digest'
101
+ Digest::SHA1.hexdigest(fields.map(&:to_sym).sort.join("\0"))[0, 12]
102
+ end
103
+ end
104
+
105
+ class Compiler
106
+ IDENTIFIER_RE = /\A[A-Za-z_][A-Za-z0-9_]*[!?=]?\z/
107
+
108
+ def initialize(index, fields: [], method_name: :__chewy_compose__)
109
+ @index = index
110
+ @procs = index.compiled_procs
111
+ @counter = 0
112
+ @fields = fields.map(&:to_sym)
113
+ @method_name = method_name
114
+ end
115
+
116
+ def build
117
+ check_root_value!(@index.root)
118
+ body = compose_children_src(@index.root, 'o0', restrict: @fields)
119
+ <<~RUBY
120
+ def self.#{@method_name}(o0, crutches, context)
121
+ #{body}.as_json
122
+ end
123
+ RUBY
124
+ end
125
+
126
+ private
127
+
128
+ def check_root_value!(root)
129
+ v = root.instance_variable_get(:@value)
130
+ return if v.equal?(Chewy::Fields::Root::DEFAULT_VALUE)
131
+
132
+ raise UNSUPPORTED, 'custom root value proc not supported by compiled path'
133
+ end
134
+
135
+ def compose_children_src(parent_field, obj_var, restrict: [])
136
+ children = parent_field.children
137
+ children = children.select { |f| restrict.include?(f.name) } if restrict.any?
138
+ # Plain Base#compose_children merges per-field hashes, so a field
139
+ # redeclared with the same name is silently last-wins. Match that
140
+ # here to avoid Ruby's "duplicate key" warning on the generated
141
+ # literal hash.
142
+ children = children.reverse.uniq(&:name).reverse
143
+
144
+ pairs = children.map do |f|
145
+ raise UNSUPPORTED, "field #{f.name}" if unsupported?(f)
146
+
147
+ # Use Ruby's literal-inspect for the key so names with quotes,
148
+ # backslashes, `#{}`, or other special characters are safely
149
+ # serialised — never raw-interpolated into source.
150
+ "#{f.name.to_s.inspect} => #{field_value_src(f, obj_var)}"
151
+ end
152
+ "{ #{pairs.join(', ')} }"
153
+ end
154
+
155
+ # Features the compiler refuses; caller falls back to plain.
156
+ # Join fields are supported via Field#value returning a proc.
157
+ def unsupported?(field)
158
+ return true if field.options.key?(:ignore_blank)
159
+ return true if field.options[:type].to_s == 'geo_point'
160
+
161
+ false
162
+ end
163
+
164
+ def field_value_src(field, parent_obj_var)
165
+ fetch = fetch_src(field, parent_obj_var)
166
+ return fetch if !field.children.present? || field.multi_field?
167
+
168
+ child_var = "o#{next_counter}"
169
+ raw_var = "#{child_var}_raw"
170
+ children_src = compose_children_src(field, child_var)
171
+ <<~RUBY.chomp
172
+ (#{raw_var} = #{fetch}
173
+ if #{raw_var}.nil?
174
+ nil
175
+ elsif #{raw_var}.respond_to?(:to_ary)
176
+ #{raw_var}.to_ary.map { |#{child_var}| #{children_src} }
177
+ else
178
+ #{child_var} = #{raw_var}
179
+ #{children_src}
180
+ end)
181
+ RUBY
182
+ end
183
+
184
+ def fetch_src(field, obj_var)
185
+ v = field.value
186
+ if v.is_a?(Proc)
187
+ idx = @procs.size
188
+ @procs << v
189
+ procs_ref = "compiled_procs[#{idx}]"
190
+ param_count = positional_param_count(v)
191
+ case v.arity
192
+ when 0
193
+ "#{obj_var}.instance_exec(&#{procs_ref})"
194
+ else
195
+ # Pass only as many of (object, crutches, context) as the
196
+ # proc actually declares. This keeps lambdas with optional
197
+ # args (negative arity like `->(o, c=nil)`) from being
198
+ # called with too many arguments. Anything beyond context
199
+ # truncates to all three.
200
+ args = [obj_var, 'crutches', 'context'].first(param_count)
201
+ "#{procs_ref}.call(#{args.join(', ')})"
202
+ end
203
+ else
204
+ method_name = v.is_a?(Symbol) || v.is_a?(String) ? v.to_s : field.name.to_s
205
+ raise UNSUPPORTED, "non-identifier field accessor: #{method_name.inspect}" unless safe_identifier?(method_name)
206
+
207
+ # Mirror Base#value_by_name_proc: hash key-or-string fallback,
208
+ # method send otherwise.
209
+ "(#{obj_var}.is_a?(Hash) ? " \
210
+ "(#{obj_var}.key?(:#{method_name}) ? #{obj_var}[:#{method_name}] : #{obj_var}['#{method_name}']) : " \
211
+ "#{obj_var}.#{method_name})"
212
+ end
213
+ end
214
+
215
+ # Number of positional parameters declared by `proc`, counting
216
+ # required + optional. Splats and keyword args contribute one
217
+ # bucket each so the caller still passes all three context args.
218
+ def positional_param_count(proc)
219
+ params = proc.parameters
220
+ required = params.count { |type, _| %i[req opt].include?(type) }
221
+ has_splat = params.any? { |type, _| type == :rest }
222
+ has_splat ? 3 : [required, 3].min
223
+ end
224
+
225
+ def safe_identifier?(name)
226
+ IDENTIFIER_RE.match?(name.to_s)
227
+ end
228
+
229
+ def next_counter
230
+ @counter += 1
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -127,7 +127,20 @@ module Chewy
127
127
  def compose(object, crutches = nil, fields: [], context: {})
128
128
  crutches ||= Chewy::Index::Crutch::Crutches.new self, [object], context
129
129
 
130
- if witchcraft? && root.children.present?
130
+ # Hot path: the default-fields compiled method has already
131
+ # been built, so skip the per-call availability check and
132
+ # dispatch directly. Bulk reindexing hits this branch for
133
+ # every object after the first.
134
+ return __chewy_compose__(object, crutches, context) if fields.empty? && @compiled_default_ready
135
+
136
+ if compiled_compose_available?(fields)
137
+ if fields.empty?
138
+ @compiled_default_ready = true
139
+ __chewy_compose__(object, crutches, context)
140
+ else
141
+ compiled_compose(object, crutches, context, fields)
142
+ end
143
+ elsif witchcraft? && root.children.present?
131
144
  cauldron(fields: fields).brew(object, crutches, context)
132
145
  else
133
146
  root.compose(object, crutches, fields: fields, context: context)
@@ -122,6 +122,7 @@ module Chewy
122
122
  # end
123
123
  #
124
124
  def field(*args, **options, &block)
125
+ invalidate_compiled_compose! if respond_to?(:invalidate_compiled_compose!)
125
126
  if args.size > 1
126
127
  args.map { |name| field(name, **options) }
127
128
  else
@@ -1,15 +1,3 @@
1
- begin
2
- require 'method_source'
3
- begin
4
- require 'prism'
5
- rescue LoadError
6
- require 'parser/current'
7
- end
8
- require 'unparser'
9
- rescue LoadError
10
- nil
11
- end
12
-
13
1
  module Chewy
14
2
  class Index
15
3
  module Witchcraft
@@ -19,8 +7,30 @@ module Chewy
19
7
  class_attribute :_witchcraft, instance_reader: false, instance_writer: false
20
8
  end
21
9
 
10
+ def self.load_dependencies!
11
+ return if @dependencies_loaded
12
+
13
+ require 'method_source'
14
+ begin
15
+ require 'prism'
16
+ rescue LoadError
17
+ require 'parser/current'
18
+ end
19
+ require 'unparser'
20
+ @dependencies_loaded = true
21
+ rescue LoadError
22
+ nil
23
+ end
24
+
22
25
  module ClassMethods
23
26
  def witchcraft!
27
+ warn(
28
+ '[DEPRECATION] Chewy::Index.witchcraft! is deprecated and will be removed in a future release. ' \
29
+ 'The compiled compose path is now the default and delivers equivalent performance without ' \
30
+ "method_source/parser/unparser dependencies. Remove the `witchcraft!` call from #{name || self}.",
31
+ uplevel: 1
32
+ )
33
+ Witchcraft.load_dependencies!
24
34
  self._witchcraft = true
25
35
  check_requirements!
26
36
  end
@@ -172,7 +182,7 @@ module Chewy
172
182
  end
173
183
 
174
184
  def source_for(proc, nesting) # rubocop:disable Metrics/AbcSize
175
- lambdas = exctract_lambdas(ast_from_proc(proc))
185
+ lambdas = extract_lambdas(ast_from_proc(proc))
176
186
 
177
187
  raise "No lambdas found, try to reformat your code:\n`#{proc.source}`" unless lambdas
178
188
 
@@ -208,13 +218,13 @@ module Chewy
208
218
  end
209
219
  end
210
220
 
211
- def exctract_lambdas(node)
221
+ def extract_lambdas(node)
212
222
  return unless node.is_a?(Parser::AST::Node)
213
223
 
214
224
  if node.type == :block && node.children[0].type == :send && node.children[0].to_a == [nil, :lambda]
215
225
  [node.children[2]]
216
226
  else
217
- node.children.map { |child| exctract_lambdas(child) }.flatten.compact
227
+ node.children.map { |child| extract_lambdas(child) }.flatten.compact
218
228
  end
219
229
  end
220
230
 
@@ -39,7 +39,7 @@ module Chewy
39
39
  end
40
40
  end
41
41
 
42
- %w[_id _type _index].each do |name|
42
+ %w[_id _index].each do |name|
43
43
  define_method name do
44
44
  _data[name]
45
45
  end
data/lib/chewy/index.rb CHANGED
@@ -11,6 +11,7 @@ require 'chewy/index/settings'
11
11
  require 'chewy/index/specification'
12
12
  require 'chewy/index/syncer'
13
13
  require 'chewy/index/witchcraft'
14
+ require 'chewy/index/compiled'
14
15
  require 'chewy/index/wrapper'
15
16
 
16
17
  module Chewy
@@ -32,6 +33,7 @@ module Chewy
32
33
  include Observe
33
34
  include Crutch
34
35
  include Witchcraft
36
+ include Compiled
35
37
  include Wrapper
36
38
 
37
39
  singleton_class.delegate :client, to: 'Chewy'
@@ -96,7 +96,6 @@ module Chewy
96
96
  'hits' => hits.each_with_index.map do |hit, i|
97
97
  {
98
98
  '_index' => index.index_name,
99
- '_type' => '_doc',
100
99
  '_id' => hit[:id] || (i + 1).to_s,
101
100
  '_score' => 3.14,
102
101
  '_source' => hit
@@ -39,7 +39,6 @@ module Chewy
39
39
  'hits' => hits.each_with_index.map do |hit, i|
40
40
  {
41
41
  '_index' => index.index_name,
42
- '_type' => '_doc',
43
42
  '_id' => (i + 1).to_s,
44
43
  '_score' => 3.14,
45
44
  '_source' => hit
@@ -18,7 +18,7 @@ module Chewy
18
18
  include Scoping
19
19
  include Scrolling
20
20
  UNDEFINED = Class.new.freeze
21
- EVERFIELDS = %w[_index _type _id _parent _routing].freeze
21
+ EVERFIELDS = %w[_index _id _routing].freeze
22
22
  DELEGATED_METHODS = %i[
23
23
  query filter post_filter knn order reorder docvalue_fields
24
24
  track_scores track_total_hits request_cache explain version profile
@@ -952,7 +952,7 @@ module Chewy
952
952
 
953
953
  # Returns and array of values for specified fields.
954
954
  # Uses `source` to restrict the list of returned fields.
955
- # Fields `_id`, `_type`, `_routing` and `_index` are also supported.
955
+ # Fields `_id`, `_routing` and `_index` are also supported.
956
956
  #
957
957
  # @overload pluck(field)
958
958
  # If single field is passed - it returns and array of values.
data/lib/chewy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '8.2.1'.freeze
2
+ VERSION = '8.3.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chewy
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.2.1
4
+ version: 8.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal, LLC
@@ -83,6 +83,7 @@ files:
83
83
  - lib/chewy/index/adapter/object.rb
84
84
  - lib/chewy/index/adapter/orm.rb
85
85
  - lib/chewy/index/aliases.rb
86
+ - lib/chewy/index/compiled.rb
86
87
  - lib/chewy/index/crutch.rb
87
88
  - lib/chewy/index/import.rb
88
89
  - lib/chewy/index/import/bulk_builder.rb