chewy 8.0.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 +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +3 -18
- data/lib/chewy/errors.rb +3 -0
- data/lib/chewy/fields/root.rb +6 -4
- data/lib/chewy/index/actions.rb +13 -11
- data/lib/chewy/index/adapter/object.rb +17 -0
- data/lib/chewy/index/adapter/orm.rb +20 -0
- data/lib/chewy/index/compiled.rb +235 -0
- data/lib/chewy/index/crutch.rb +12 -2
- data/lib/chewy/index/import/bulk_builder.rb +8 -5
- data/lib/chewy/index/import/progressbar.rb +79 -0
- data/lib/chewy/index/import/routine.rb +3 -2
- data/lib/chewy/index/import.rb +87 -20
- data/lib/chewy/index/mapping.rb +1 -0
- data/lib/chewy/index/witchcraft.rb +30 -19
- data/lib/chewy/index/wrapper.rb +1 -1
- data/lib/chewy/index.rb +2 -0
- data/lib/chewy/minitest/helpers.rb +0 -1
- data/lib/chewy/multi_search.rb +1 -1
- data/lib/chewy/rake_helper.rb +19 -2
- data/lib/chewy/rspec/helpers.rb +0 -1
- data/lib/chewy/search/parameters/runtime_mappings.rb +14 -0
- data/lib/chewy/search/request.rb +20 -4
- data/lib/chewy/search/scrolling.rb +14 -6
- data/lib/chewy/stash.rb +10 -6
- data/lib/chewy/strategy/delayed_sidekiq/worker.rb +1 -1
- data/lib/chewy/version.rb +1 -1
- data/lib/chewy.rb +1 -0
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34d773f10bf67d60eca51ff91a6d668186eea74515bb5b31a5fd5ce814dac3c7
|
|
4
|
+
data.tar.gz: 334c6e92185369170b9f4d534029b69c218bb3c0cabfe6e4c11fd79268c19587
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24bde735bcfc4f1bfec8b146cdcc5024d049e1405ccfbbae5e6f57f53a89aa9a6951063b00251a2f87fafac3632e1640fddc64038c7337e9147a0b9d6740f984
|
|
7
|
+
data.tar.gz: d2d84b1d2d3b07bde52699dfc54ed3b9ac8b9b71c5d79ca204851abab06bf53a41f0591ded6cdab59be5bf87522374dc10ff01d180891bcf81d73d33b009e180
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## master (unreleased)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
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
|
+
|
|
25
|
+
## 8.2.1 (2026-06-01)
|
|
26
|
+
|
|
27
|
+
### New Features
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* Fix `import_count` on relations with multiple `.select(...)` clauses so the bounded progressbar is used instead of silently falling back to the unbounded spinner. Bare `.count` produced `SELECT COUNT(col1, col2, ...)` (invalid SQL); `count(:all)` is used for relations. ([#1026](https://github.com/toptal/chewy/pull/1026))
|
|
32
|
+
|
|
33
|
+
### Changes
|
|
34
|
+
|
|
35
|
+
## 8.2.0 (2026-05-29)
|
|
36
|
+
|
|
37
|
+
### New Features
|
|
38
|
+
|
|
39
|
+
* 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.
|
|
40
|
+
|
|
41
|
+
### Bug Fixes
|
|
42
|
+
|
|
43
|
+
* 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.
|
|
44
|
+
The delete + alias-add are now performed in a single atomic `_aliases` cluster state update via the `remove_index` action.
|
|
45
|
+
No public API or layout change.
|
|
46
|
+
* [#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.
|
|
47
|
+
|
|
48
|
+
### Changes
|
|
49
|
+
|
|
50
|
+
* [#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.
|
|
51
|
+
|
|
52
|
+
## 8.1.0 (2026-05-28)
|
|
53
|
+
|
|
54
|
+
### New Features
|
|
55
|
+
|
|
56
|
+
* [#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))
|
|
57
|
+
* [#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.
|
|
58
|
+
|
|
59
|
+
### Bug Fixes
|
|
60
|
+
|
|
61
|
+
* [#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.
|
|
62
|
+
|
|
63
|
+
### Changes
|
|
64
|
+
|
|
65
|
+
* [#916](https://github.com/toptal/chewy/pull/916): Raise error in #scroll_batches when search backend returns a failure. ([@tomdev][])
|
|
66
|
+
* [#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][])
|
|
67
|
+
* [#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][])
|
|
68
|
+
* [#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][])
|
|
69
|
+
* [#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][])
|
|
70
|
+
* [#1014](https://github.com/toptal/chewy/pull/1014): Improve contributing documentation with development setup instructions, PR workflow, and grammar fixes. ([@mattmenefee][])
|
|
71
|
+
|
|
3
72
|
## 8.0.1 (2026-03-12)
|
|
4
73
|
|
|
5
74
|
### New Features
|
|
@@ -874,10 +943,12 @@
|
|
|
874
943
|
[@marshall]: https://github.com/marshall
|
|
875
944
|
[@matchbookmac]: https://github.com/matchbookmac
|
|
876
945
|
[@matthee]: https://github.com/matthee
|
|
946
|
+
[@mattmenefee]: https://github.com/mattmenefee
|
|
877
947
|
[@mattzollinhofer]: https://github.com/mattzollinhofer
|
|
878
948
|
[@menglewis]: https://github.com/menglewis
|
|
879
949
|
[@mikeyhogarth]: https://github.com/mikeyhogarth
|
|
880
950
|
[@milk1000cc]: https://github.com/milk1000cc
|
|
951
|
+
[@mjankowski]: https://github.com/mjankowski
|
|
881
952
|
[@mkcode]: https://github.com/mkcode
|
|
882
953
|
[@mpeychich]: https://github.com/mpeychich
|
|
883
954
|
[@mrbrdo]: https://github.com/mrbrdo
|
|
@@ -901,6 +972,7 @@
|
|
|
901
972
|
[@socialchorus]: https://github.com/socialchorus
|
|
902
973
|
[@taylor-au]: https://github.com/taylor-au
|
|
903
974
|
[@TikiTDO]: https://github.com/TikiTDO
|
|
975
|
+
[@tomdev]: https://github.com/tomdev
|
|
904
976
|
[@undr]: https://github.com/undr
|
|
905
977
|
[@Vitalina-Vakulchyk]: https://github.com/Vitalina-Vakulchyk
|
|
906
978
|
[@webgago]: https://github.com/webgago
|
data/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
[](http://badge.fury.io/rb/chewy)
|
|
2
2
|
[](https://github.com/toptal/chewy/actions/workflows/ruby.yml)
|
|
3
|
-
[](https://codeclimate.com/github/toptal/chewy)
|
|
4
|
-
[](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-
|
|
48
|
+
Chewy is compatible with MRI 3.2-4.0.
|
|
51
49
|
|
|
52
50
|
### Elasticsearch compatibility matrix
|
|
53
51
|
|
|
@@ -220,7 +218,6 @@ end
|
|
|
220
218
|
},
|
|
221
219
|
"_data":{
|
|
222
220
|
"_index":"users",
|
|
223
|
-
"_type":"_doc",
|
|
224
221
|
"_id":"1",
|
|
225
222
|
"_score":0.9808291,
|
|
226
223
|
"_source":{
|
|
@@ -238,7 +235,7 @@ end
|
|
|
238
235
|
|
|
239
236
|
- [Getting Started](docs/getting_started.md) — end-to-end tutorial building search for a media library
|
|
240
237
|
- [Configuration](docs/configuration.md) — client settings, update strategies, notifications, integrations
|
|
241
|
-
- [Indexing](docs/indexing.md) — index definition, field types, crutches,
|
|
238
|
+
- [Indexing](docs/indexing.md) — index definition, field types, crutches, compiled compose path, index manipulation
|
|
242
239
|
- [Import](docs/import.md) — import options, raw import, journaling
|
|
243
240
|
- [Querying](docs/querying.md) — search requests, pagination, scopes, scroll, loading
|
|
244
241
|
- [Rake Tasks](docs/rake_tasks.md) — all rake tasks and parallelization
|
|
@@ -274,19 +271,7 @@ Use the standard client settings with your Cloud credentials (API key or user/pa
|
|
|
274
271
|
|
|
275
272
|
## Contributing
|
|
276
273
|
|
|
277
|
-
|
|
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
|
-
```
|
|
274
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup instructions, how to run tests, and the pull request workflow.
|
|
290
275
|
|
|
291
276
|
## Copyright
|
|
292
277
|
|
data/lib/chewy/errors.rb
CHANGED
data/lib/chewy/fields/root.rb
CHANGED
|
@@ -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 ||=
|
|
11
|
+
@value ||= DEFAULT_VALUE
|
|
10
12
|
@dynamic_templates = []
|
|
11
13
|
end
|
|
12
14
|
|
|
@@ -61,8 +63,8 @@ module Chewy
|
|
|
61
63
|
# @param fields [Array<Symbol>] a list of fields to compose, every field will be composed if empty
|
|
62
64
|
# @return [Hash] JSON-ready hash with stringified keys
|
|
63
65
|
#
|
|
64
|
-
def compose(object, crutches = nil, fields: [])
|
|
65
|
-
result = evaluate([object, crutches])
|
|
66
|
+
def compose(object, crutches = nil, fields: [], context: {})
|
|
67
|
+
result = evaluate([object, crutches, context])
|
|
66
68
|
|
|
67
69
|
if children.present?
|
|
68
70
|
child_fields = if fields.present?
|
|
@@ -72,7 +74,7 @@ module Chewy
|
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
child_fields.each_with_object({}) do |field, memo|
|
|
75
|
-
memo.merge!(field.compose(result, crutches) || {})
|
|
77
|
+
memo.merge!(field.compose(result, crutches, context) || {})
|
|
76
78
|
end.as_json
|
|
77
79
|
elsif fields.present?
|
|
78
80
|
result.as_json(only: fields, root: false)
|
data/lib/chewy/index/actions.rb
CHANGED
|
@@ -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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.is_a?(relation_class) ? collection.count(:all) : 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]
|
|
@@ -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
|
data/lib/chewy/index/crutch.rb
CHANGED
|
@@ -9,9 +9,12 @@ module Chewy
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
class Crutches
|
|
12
|
-
|
|
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] ||=
|
|
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
|
|
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
|
|
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.:
|