elasticsearch_record 2.0.0 → 4.0.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/README.md +112 -12
- data/docs/CHANGELOG.md +26 -0
- data/elasticsearch_record.gemspec +2 -2
- data/lib/active_record/connection_adapters/elasticsearch/column.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +3 -2
- data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +16 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/column_methods.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +0 -2
- data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +23 -13
- data/lib/arel/collectors/elasticsearch_query.rb +3 -0
- data/lib/elasticsearch_record/core.rb +1 -1
- data/lib/elasticsearch_record/gem_version.rb +1 -1
- data/lib/elasticsearch_record/internal_metadata.rb +19 -0
- data/lib/elasticsearch_record/patches/active_record/connection_pool_patch.rb +57 -0
- data/lib/elasticsearch_record/persistence.rb +4 -3
- data/lib/elasticsearch_record/querying.rb +38 -14
- data/lib/elasticsearch_record/relation/core_methods.rb +1 -1
- data/lib/elasticsearch_record/relation/query_clause_tree.rb +2 -1
- data/lib/elasticsearch_record/relation/query_methods.rb +1 -1
- data/lib/elasticsearch_record/relation/value_methods.rb +4 -4
- data/lib/elasticsearch_record/result.rb +10 -0
- data/lib/elasticsearch_record/schema_migration.rb +5 -2
- data/lib/elasticsearch_record/statement_cache.rb +0 -0
- data/lib/elasticsearch_record.rb +2 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3d4d1dd73be19114191e80cba0ec2844ab3049e380998f8ba48fed78f5c5837
|
|
4
|
+
data.tar.gz: 252d5abb68af3199c73850337aac6d6ce2c6e379a9fd5f2bea1656447da29995
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 635066f96fb6713a4719ac6fc2a4338dd967b7ca990c9d2c1e644ca05a749be4932f9a07277a42da5340530f0291d7a2bd06edcfdaf89cdcda844339ede6b4fa
|
|
7
|
+
data.tar.gz: 324d987c404a4dd0069d48c30866c6b854719eec91e3051321396ecf457b160dd16dfc29d47427c4e4a7193fc5d82e721f89013ae692ca9cc4642bb005503cb8
|
data/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
[](https://badge.fury.io/rb/elasticsearch_record)
|
|
7
7
|
[](docs/LICENSE)
|
|
8
8
|
|
|
9
|
+
[](https://coveralls.io/github/ruby-smart/elasticsearch_record?branch=main)
|
|
10
|
+
|
|
9
11
|
ActiveRecord adapter for Elasticsearch
|
|
10
12
|
|
|
11
13
|
_ElasticsearchRecord is a ActiveRecord adapter and provides similar functionality for Elasticsearch._
|
|
@@ -14,9 +16,9 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
|
|
|
14
16
|
|
|
15
17
|
**PLEASE NOTE:**
|
|
16
18
|
|
|
17
|
-
- This is the `main`-branch, which
|
|
18
|
-
- supports ActiveRecord ~> 7.
|
|
19
|
-
- added features up to Elasticsearch `8.17.1`
|
|
19
|
+
- This is the `main`-branch, which supports rails **8.0** _(see section 'Rails_Versions' for supported versions)_
|
|
20
|
+
- supports ActiveRecord ~> 7.2 + Elasticsearch >= 7.17
|
|
21
|
+
- added features up to Elasticsearch `8.17.1` _(tested against `8.19.14`)_
|
|
20
22
|
- _ES|QL_ queries _(`TYPE_ESQL` / the `esql.query` gate)_ require **Elasticsearch >= 8.11**, where the feature became
|
|
21
23
|
generally available. All other features remain available from Elasticsearch `7.17`.
|
|
22
24
|
|
|
@@ -26,8 +28,23 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
|
|
|
26
28
|
|
|
27
29
|
Supported rails versions:
|
|
28
30
|
|
|
31
|
+
### Rails 8.0:
|
|
32
|
+
_(since gem version 4.0)_
|
|
33
|
+
|
|
34
|
+
https://github.com/ruby-smart/elasticsearch_record/tree/rails-8-0-stable
|
|
35
|
+
|
|
36
|
+
[](https://github.com/ruby-smart/elasticsearch_record/tree/rails-8-0-stable)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Rails 7.2:
|
|
40
|
+
_(since gem version 3.0)_
|
|
41
|
+
|
|
42
|
+
https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-2-stable
|
|
43
|
+
|
|
44
|
+
[](https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-2-stable)
|
|
45
|
+
|
|
29
46
|
### Rails 7.1:
|
|
30
|
-
_(since gem version
|
|
47
|
+
_(since gem version 2.0)_
|
|
31
48
|
|
|
32
49
|
https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-1-stable
|
|
33
50
|
|
|
@@ -47,7 +64,7 @@ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-0-stable
|
|
|
47
64
|
Add this line to your application's Gemfile:
|
|
48
65
|
|
|
49
66
|
```ruby
|
|
50
|
-
gem 'elasticsearch_record'
|
|
67
|
+
gem 'elasticsearch_record'
|
|
51
68
|
|
|
52
69
|
# alternative
|
|
53
70
|
gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-1-stable'
|
|
@@ -63,6 +80,78 @@ Or install it yourself as:
|
|
|
63
80
|
|
|
64
81
|
$ gem install elasticsearch_record
|
|
65
82
|
|
|
83
|
+
-----
|
|
84
|
+
|
|
85
|
+
## Upgrading to 2.0
|
|
86
|
+
|
|
87
|
+
Version **2.0** contains breaking changes. Coming from **1.8.x**, check the following:
|
|
88
|
+
|
|
89
|
+
### 1. Table (index) names are resolved by default
|
|
90
|
+
|
|
91
|
+
Every table statement now resolves its name through `#_env_table_name`, so the
|
|
92
|
+
`table_name_prefix` / `table_name_suffix` no longer have to be applied by hand in migrations or database statements
|
|
93
|
+
_(which silently wrote into another environment's index)_.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# 1.8.x - the prefix / suffix had to be applied manually
|
|
97
|
+
drop_table _env_table_name("settings")
|
|
98
|
+
|
|
99
|
+
# 2.0 - resolved on its own
|
|
100
|
+
drop_table "settings"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Existing migrations keep working - `_env_table_name` is idempotent and still public, so a
|
|
104
|
+
hand-resolved name resolves to the very same index. A new `decorate:`-argument opts out per
|
|
105
|
+
call, and `ElasticsearchRecord.decorate_table_names` acts as a global kill-switch.
|
|
106
|
+
|
|
107
|
+
_see @ [environment-related-table-name](#environment-related-table-name), [opting out with `decorate: false`](#opting-out-with-decorate-false) & [global kill-switch](#global-kill-switch)_
|
|
108
|
+
|
|
109
|
+
### 2. `select` raises on metadata fields
|
|
110
|
+
|
|
111
|
+
Metadata fields _(`_id`, `_score`, `_index`, ...)_ are not part of the `_source` node, so they
|
|
112
|
+
could never be resolved through the `_source`-filter this method builds - providing them
|
|
113
|
+
silently created a filter that never matched.
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Search.select(:_id)
|
|
117
|
+
# => ActiveRecord::UnknownAttributeReference
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
They are returned anyway and accessible on each record _(`Search.first._id`)_. To resolve the
|
|
121
|
+
metadata **without** transferring the `_source`, use the new `#meta_only!` method.
|
|
122
|
+
|
|
123
|
+
### 3. `restore_table` replaces its `open:`-argument with `unblock:`
|
|
124
|
+
|
|
125
|
+
A restore runs through a `clone`, so the restored table **inherits** the 'write'-block of its
|
|
126
|
+
source - the table was open, but read-only. `unblock:` _(default: `true`)_ releases it again.
|
|
127
|
+
`ModelApi#restore!` follows along.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# 1.8.x
|
|
131
|
+
restore_table 'settings', from: 'settings-snapshot-2024', open: true
|
|
132
|
+
|
|
133
|
+
# 2.0
|
|
134
|
+
restore_table 'settings', from: 'settings-snapshot-2024', unblock: true
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. The plural table statements no longer skip AR-internal indices
|
|
138
|
+
|
|
139
|
+
`#open_tables`, `#close_tables`, `#refresh_tables` & `#truncate_tables` no longer subtract the
|
|
140
|
+
ActiveRecord-internal indices _(`schema_migrations` & `ar_internal_metadata`)_ - an explicitly
|
|
141
|
+
named index was silently dropped from the list. They now return an `Array` with an entry for
|
|
142
|
+
**every** provided name.
|
|
143
|
+
|
|
144
|
+
### 5. `truncate_table` raises for AR-internal indices
|
|
145
|
+
|
|
146
|
+
The statement runs a `drop` & `create` and would wipe the migration state.
|
|
147
|
+
`#drop_table` stays unguarded by design _(ActiveRecord resets both tables through it)_.
|
|
148
|
+
|
|
149
|
+
### 6. `esql` & `msearch` dropped their `async:`-argument
|
|
150
|
+
|
|
151
|
+
Both dispatch through the public `exec_query` now, since rails 7.1 made `internal_exec_query`
|
|
152
|
+
private - the `async:`-argument was dropped along with it.
|
|
153
|
+
|
|
154
|
+
-----
|
|
66
155
|
|
|
67
156
|
## Features
|
|
68
157
|
* ActiveRecord's `create, read, update & delete` behaviours
|
|
@@ -72,6 +161,8 @@ Or install it yourself as:
|
|
|
72
161
|
* additional relation methods to find records with `filter, must, must_not, should`
|
|
73
162
|
* aggregated queries with Elasticsearch `aggregation` methods
|
|
74
163
|
* resolve search response `hits`, `aggregations`, `buckets`, ... instead of ActiveRecord objects
|
|
164
|
+
* `SQL` & `ES|QL` queries resolve their **tabular** response into records _(`find_by_sql` with a String query, `find_by_esql`)_
|
|
165
|
+
* table (index) names are resolved within the current environment _(`table_name_prefix` / `table_name_suffix`)_
|
|
75
166
|
* Third-party gem support
|
|
76
167
|
* access `elasticsearch-dsl` query builder through `model.search{ ... }`
|
|
77
168
|
* Schema
|
|
@@ -255,6 +346,11 @@ buckets = Search.where(name: 'A nice object').aggregate(:total, {sum: {field: :a
|
|
|
255
346
|
results = Search.where(name: 'A nice object').pit_results
|
|
256
347
|
# > [{ "_id": "abc123", "name": "A nice object", ...
|
|
257
348
|
|
|
349
|
+
# resolves ONLY the metadata nodes of each hit - the '_source' is not transferred at all.
|
|
350
|
+
# (this is the replacement for a - no longer supported - 'select(:_id)')
|
|
351
|
+
metas = Search.where(name: 'A nice object').meta_only!.results
|
|
352
|
+
# > [{ "_id": "abc123", "_index": "search", "_score": 1.0 }, ...
|
|
353
|
+
|
|
258
354
|
# returns the total value of the query without querying again (it uses the total value from the response)
|
|
259
355
|
scope = Search.where(name: 'A nice object').limit(5)
|
|
260
356
|
results_count = scope.count
|
|
@@ -288,10 +384,7 @@ _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/ma
|
|
|
288
384
|
- must
|
|
289
385
|
- should
|
|
290
386
|
- aggregate
|
|
291
|
-
-
|
|
292
|
-
- hits_only!
|
|
293
|
-
- aggs_only!
|
|
294
|
-
- total_only!
|
|
387
|
+
- select _(raises on metadata fields - see @ [Upgrading to 2.0](#2-select-raises-on-metadata-fields))_
|
|
295
388
|
|
|
296
389
|
_see simple documentation about these methods @ {ElasticsearchRecord::Relation::QueryMethods rubydoc}_
|
|
297
390
|
|
|
@@ -308,7 +401,7 @@ _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/ma
|
|
|
308
401
|
- boxplot
|
|
309
402
|
- stats
|
|
310
403
|
- string_stats
|
|
311
|
-
- matrix_stats
|
|
404
|
+
- matrix_stats _(requires at least two columns)_
|
|
312
405
|
- median_absolute_deviation
|
|
313
406
|
- calculate
|
|
314
407
|
|
|
@@ -316,6 +409,13 @@ _see simple documentation about these methods @ {ElasticsearchRecord::Relation::
|
|
|
316
409
|
|
|
317
410
|
_(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/main/lib/elasticsearch_record/relation/calculation_methods.rb) )_
|
|
318
411
|
|
|
412
|
+
### Available query configuration methods
|
|
413
|
+
|
|
414
|
+
- hits_only! _(prevents to resolve aggs)_
|
|
415
|
+
- aggs_only! _(prevents to resolve hits / source data)_
|
|
416
|
+
- total_only! _(prevents to resolve aggs, hits / source data)_
|
|
417
|
+
- meta_only! _(resolves the metadata nodes (`_id`, `_score`, ...) of each hit without transferring the `_source`)_
|
|
418
|
+
|
|
319
419
|
### Available result methods
|
|
320
420
|
- aggregations
|
|
321
421
|
- buckets
|
|
@@ -544,7 +644,7 @@ Access these methods through the model's connection or within any `Migration`.
|
|
|
544
644
|
**Example migration:**
|
|
545
645
|
|
|
546
646
|
```ruby
|
|
547
|
-
class AddTests < ActiveRecord::Migration[7.
|
|
647
|
+
class AddTests < ActiveRecord::Migration[7.1]
|
|
548
648
|
def up
|
|
549
649
|
create_table "assignments", if_not_exists: true do |t|
|
|
550
650
|
t.string :key, primary_key: true
|
|
@@ -647,7 +747,7 @@ A single migration can be created to be used within each environment:
|
|
|
647
747
|
|
|
648
748
|
```ruby
|
|
649
749
|
# Example migration
|
|
650
|
-
class AddSettings < ActiveRecord::Migration[7.
|
|
750
|
+
class AddSettings < ActiveRecord::Migration[7.1]
|
|
651
751
|
def up
|
|
652
752
|
# creates 'settings-pro' on production & 'settings-dev' on development
|
|
653
753
|
create_table "settings", force: true do |t|
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# ElasticsearchRecord - CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [4.0.0] - 2026-09-08
|
|
4
|
+
* [add] **BREAKING**: requires `activerecord ~> 8.0.0`
|
|
5
|
+
* [add] `ElasticsearchRecord::Result#indexed_rows` - rails 8.0 instantiates records through this method instead of iterating the result set itself.
|
|
6
|
+
* [ref] `ElasticsearchAdapter#log` resolves the instrumenter through `#instrumenter` instead of the former `@instrumenter` ivar
|
|
7
|
+
* [ref] `ActiveRecord::ConnectionAdapters::Elasticsearch::SchemaDumper` remove redundant blank lines
|
|
8
|
+
|
|
9
|
+
## [3.0.0] - 2026-09-08
|
|
10
|
+
* [add] **BREAKING**: requires `activerecord ~> 7.2.0`
|
|
11
|
+
* [add] `Quoting::ClassMethods#quote_column_name` & `#quote_table_name` - identifier quoting is class-level & mandatory now
|
|
12
|
+
* [add] `ElasticsearchRecord::InternalMetadata` - always disabled _(the flag moved from the adapter to the database config)_
|
|
13
|
+
* [add] `patches/active_record/connection_pool_patch` - routes `#migrations_paths`, `#schema_migration` & `#internal_metadata` back to the adapter
|
|
14
|
+
* [add] specs for `ElasticsearchAdapter#translate_exception`
|
|
15
|
+
* [add] specs for `Relation::ValueMethods#limit_value=`
|
|
16
|
+
* [add] specs for `Core#id=`, `#id_was`, `#write_attribute` & `#read_attribute`
|
|
17
|
+
* [add] specs for `TableSettingDefinition`
|
|
18
|
+
* [add] specs for `InternalMetadata#enabled?`
|
|
19
|
+
* [ref] the adapter registers itself through `ActiveRecord::ConnectionAdapters.register` - `ConnectionHandling#elasticsearch_connection` removed
|
|
20
|
+
* [ref] the adapter provides `#internal_metadata` & `#migration_context` again; `#schema_migration` builds from the **pool**
|
|
21
|
+
* [ref] `SchemaMigration#integer_versions` resolves through `pool.with_connection`
|
|
22
|
+
* [ref] `#find_by_sql`, `#find_by_query` & `#find_by_esql` resolve through `with_connection`; `allow_retry:` forwarded down to `#internal_exec_query`
|
|
23
|
+
* [ref] `#_insert_record`, `#_insert_with_auto_increment` & `#cached_find_by_statement` take the connection as first argument
|
|
24
|
+
* [ref] `Arel::Collectors::ElasticsearchQuery#retryable` accessor added _(rails marks idempotent queries as retryable)_
|
|
25
|
+
* [ref] `assert_mutability!` -> `assert_modifiable!` & `ImmutableRelation` -> `UnmodifiableRelation`
|
|
26
|
+
* [ref] `Arel::Nodes::Or` is a nary node now - takes its children as a single `Array`
|
|
27
|
+
* [fix] `Querying::ClassMethods#search` to nest the `Elasticsearch::DSL` block result into the `body` and to only enter the DSL branch **if a block was provided**
|
|
28
|
+
|
|
3
29
|
## [2.0.0] - 2026-08-10
|
|
4
30
|
* [add] `ElasticsearchRecord.decorate_table_names` _(default: `true`)_ as global kill-switch for the table name decoration - only provides the default for an omitted `decorate:`-argument, `#_env_table_name` stays unaffected
|
|
5
31
|
* [add] `TableStatements#truncate_table` raises an `ArgumentError` for AR-internal indices - the statement runs a `drop` & `create` and would wipe the migration state. `#drop_table` stays unguarded by design _(AR resets both tables through it)_
|
|
@@ -14,7 +14,7 @@ DESC
|
|
|
14
14
|
|
|
15
15
|
spec.homepage = "https://github.com/ruby-smart/elasticsearch_record"
|
|
16
16
|
spec.license = "MIT"
|
|
17
|
-
spec.required_ruby_version = ">=
|
|
17
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
18
18
|
|
|
19
19
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
20
20
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
@@ -32,7 +32,7 @@ DESC
|
|
|
32
32
|
|
|
33
33
|
spec.require_paths = ["lib"]
|
|
34
34
|
|
|
35
|
-
spec.add_dependency 'activerecord', '~>
|
|
35
|
+
spec.add_dependency 'activerecord', '~> 8.0.0'
|
|
36
36
|
spec.add_dependency 'elasticsearch', '>= 7.17'
|
|
37
37
|
|
|
38
38
|
spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
|
|
File without changes
|
|
@@ -137,10 +137,11 @@ module ActiveRecord
|
|
|
137
137
|
# @param [Array] binds - not supported on the top-level and therefore ignored!
|
|
138
138
|
# @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
|
|
139
139
|
# @param [Boolean] async
|
|
140
|
+
# @param [Boolean] allow_retry
|
|
140
141
|
# @return [ElasticsearchRecord::Result]
|
|
141
|
-
def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
|
|
142
|
+
def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false, allow_retry: false)
|
|
142
143
|
build_result(
|
|
143
|
-
internal_execute(query, name, async: async),
|
|
144
|
+
internal_execute(query, name, async: async, allow_retry: allow_retry),
|
|
144
145
|
columns: query.columns
|
|
145
146
|
)
|
|
146
147
|
end
|
|
@@ -4,6 +4,22 @@ module ActiveRecord
|
|
|
4
4
|
module ConnectionAdapters
|
|
5
5
|
module Elasticsearch
|
|
6
6
|
module Quoting # :nodoc:
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
module ClassMethods # :nodoc:
|
|
10
|
+
# since rails 7.2 identifier quoting is class-level & mandatory (the abstract
|
|
11
|
+
# implementation raises a +NotImplementedError+). Elasticsearch fields & indices are
|
|
12
|
+
# plain JSON keys - so they pass through unquoted.
|
|
13
|
+
def quote_column_name(column_name)
|
|
14
|
+
column_name.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Quotes the table (index) name. Defaults to column name quoting.
|
|
18
|
+
def quote_table_name(table_name)
|
|
19
|
+
quote_column_name(table_name)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
7
23
|
def quoted_true
|
|
8
24
|
'true'
|
|
9
25
|
end
|
data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/column_methods.rb
CHANGED
|
File without changes
|
|
@@ -151,14 +151,12 @@ module ActiveRecord
|
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
tbl.puts " end"
|
|
154
|
-
tbl.puts
|
|
155
154
|
|
|
156
155
|
tbl.rewind
|
|
157
156
|
stream.print tbl.read
|
|
158
157
|
rescue => e
|
|
159
158
|
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
|
160
159
|
stream.puts "# #{e.message}"
|
|
161
|
-
stream.puts
|
|
162
160
|
ensure
|
|
163
161
|
self.table_name = nil
|
|
164
162
|
end
|
|
@@ -22,16 +22,6 @@ gem 'elasticsearch'
|
|
|
22
22
|
require 'elasticsearch'
|
|
23
23
|
|
|
24
24
|
module ActiveRecord # :nodoc:
|
|
25
|
-
module ConnectionHandling # :nodoc:
|
|
26
|
-
def elasticsearch_adapter_class
|
|
27
|
-
ConnectionAdapters::ElasticsearchAdapter
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def elasticsearch_connection(config)
|
|
31
|
-
elasticsearch_adapter_class.new(config)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
25
|
module ConnectionAdapters # :nodoc:
|
|
36
26
|
class ElasticsearchAdapter < AbstractAdapter
|
|
37
27
|
include Elasticsearch::UnsupportedImplementation
|
|
@@ -168,7 +158,17 @@ module ActiveRecord # :nodoc:
|
|
|
168
158
|
end
|
|
169
159
|
|
|
170
160
|
def schema_migration # :nodoc:
|
|
171
|
-
ElasticsearchRecord::SchemaMigration.new(
|
|
161
|
+
ElasticsearchRecord::SchemaMigration.new(pool)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def internal_metadata # :nodoc:
|
|
165
|
+
ElasticsearchRecord::InternalMetadata.new(pool)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# since rails 7.2 the migration context is resolved through the connection pool -
|
|
169
|
+
# provided here as connection method again (used by +#assume_migrated_upto_version+)
|
|
170
|
+
def migration_context # :nodoc:
|
|
171
|
+
pool.migration_context
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
# provide a table_name_prefix from the configuration to create & restrict schema creation
|
|
@@ -384,7 +384,9 @@ module ActiveRecord # :nodoc:
|
|
|
384
384
|
# @raise [Exception] any exception encountered during execution, after translation.
|
|
385
385
|
# @return [void]
|
|
386
386
|
def log(gate, arguments, name = 'QUERY', async: false, statistics: nil, &block)
|
|
387
|
-
|
|
387
|
+
# +instrumenter+ instead of the former +@instrumenter+ ivar - the abstract adapter
|
|
388
|
+
# resolves it per execution context now
|
|
389
|
+
instrumenter.instrument(
|
|
388
390
|
"query.elasticsearch_record",
|
|
389
391
|
gate: gate,
|
|
390
392
|
name: name,
|
|
@@ -429,4 +431,12 @@ module ActiveRecord # :nodoc:
|
|
|
429
431
|
::ActiveRecord::Type.register(:range, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
|
|
430
432
|
end
|
|
431
433
|
end
|
|
432
|
-
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# since rails 7.2 an adapter has to register itself - the former load-by-convention
|
|
437
|
+
# through +ConnectionHandling#elasticsearch_connection+ is gone
|
|
438
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
439
|
+
'elasticsearch',
|
|
440
|
+
'ActiveRecord::ConnectionAdapters::ElasticsearchAdapter',
|
|
441
|
+
'active_record/connection_adapters/elasticsearch_adapter'
|
|
442
|
+
)
|
|
@@ -9,6 +9,9 @@ module Arel # :nodoc: all
|
|
|
9
9
|
# required for ActiveRecord
|
|
10
10
|
attr_accessor :preparable
|
|
11
11
|
|
|
12
|
+
# required for ActiveRecord (rails 7.2 marks idempotent queries as retryable)
|
|
13
|
+
attr_accessor :retryable
|
|
14
|
+
|
|
12
15
|
# returns the current bind index (default: 1)
|
|
13
16
|
attr_reader :bind_index
|
|
14
17
|
|
|
@@ -96,7 +96,7 @@ module ElasticsearchRecord
|
|
|
96
96
|
# used to create a cacheable statement.
|
|
97
97
|
# This is a 1:1 copy, except that we use our own class +ElasticsearchRecord::StatementCache+
|
|
98
98
|
# see @ ActiveRecord::Core::ClassMethods#cached_find_by_statement
|
|
99
|
-
def cached_find_by_statement(key, &block)
|
|
99
|
+
def cached_find_by_statement(connection, key, &block)
|
|
100
100
|
cache = @find_by_statement_cache[connection.prepared_statements]
|
|
101
101
|
cache.compute_if_absent(key) { ElasticsearchRecord::StatementCache.create(connection, &block) }
|
|
102
102
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/internal_metadata"
|
|
4
|
+
|
|
5
|
+
module ElasticsearchRecord
|
|
6
|
+
class InternalMetadata < ::ActiveRecord::InternalMetadata
|
|
7
|
+
|
|
8
|
+
# overwrite method to always disable the internal metadata index.
|
|
9
|
+
#
|
|
10
|
+
# Elasticsearch cannot serve the internal metadata table
|
|
11
|
+
# (see +ElasticsearchAdapter#use_metadata_table?+) - but since rails 7.2 the flag is
|
|
12
|
+
# resolved through the database config (default: enabled) instead of the adapter,
|
|
13
|
+
# so a default configuration would suddenly create & write a 'ar_internal_metadata'
|
|
14
|
+
# index on the cluster.
|
|
15
|
+
def enabled?
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record/connection_adapters/abstract/connection_pool'
|
|
4
|
+
|
|
5
|
+
module ElasticsearchRecord
|
|
6
|
+
module Patches
|
|
7
|
+
module ActiveRecord
|
|
8
|
+
# Since rails 7.2 the migration plumbing is resolved through the connection POOL
|
|
9
|
+
# (+ConnectionPool#migration_context+ builds from +#migrations_paths+, +#schema_migration+ &
|
|
10
|
+
# +#internal_metadata+) instead of the connection - so the adapters own implementations are
|
|
11
|
+
# no longer consulted. Without this patch a Elasticsearch pool would migrate with the generic
|
|
12
|
+
# +ActiveRecord::SchemaMigration+ (which resolves the index name through +ActiveRecord::Base+
|
|
13
|
+
# and only ever sees the first ten migrations - Elasticsearch default search size), enable the
|
|
14
|
+
# internal metadata index (the adapter cannot serve it) and fall back to the default
|
|
15
|
+
# 'db/migrate' path.
|
|
16
|
+
#
|
|
17
|
+
# The patch simply routes those factories back to the adapter class, which keeps the
|
|
18
|
+
# Elasticsearch-specific implementations in one place.
|
|
19
|
+
module ConnectionPoolPatch
|
|
20
|
+
def self.included(base)
|
|
21
|
+
base.send(:prepend, PrependMethods)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module PrependMethods
|
|
25
|
+
def migrations_paths
|
|
26
|
+
return super unless elasticsearch_pool?
|
|
27
|
+
|
|
28
|
+
db_config.migrations_paths || ['db/migrate_elasticsearch']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def schema_migration
|
|
32
|
+
return super unless elasticsearch_pool?
|
|
33
|
+
|
|
34
|
+
ElasticsearchRecord::SchemaMigration.new(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def internal_metadata
|
|
38
|
+
return super unless elasticsearch_pool?
|
|
39
|
+
|
|
40
|
+
ElasticsearchRecord::InternalMetadata.new(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# returns true, if the pools config resolves to the Elasticsearch adapter
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def elasticsearch_pool?
|
|
48
|
+
db_config.adapter_class <= ::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# include once only!
|
|
57
|
+
::ActiveRecord::ConnectionAdapters::ConnectionPool.include(ElasticsearchRecord::Patches::ActiveRecord::ConnectionPoolPatch) unless ::ActiveRecord::ConnectionAdapters::ConnectionPool.included_modules.include?(ElasticsearchRecord::Patches::ActiveRecord::ConnectionPoolPatch)
|
|
@@ -5,14 +5,15 @@ module ElasticsearchRecord
|
|
|
5
5
|
module ClassMethods
|
|
6
6
|
# insert a new record into the Elasticsearch index
|
|
7
7
|
# NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
|
|
8
|
+
# @param [ActiveRecord::ConnectionAdapters::ElasticsearchAdapter] connection
|
|
8
9
|
# @param [ActiveModel::Attribute] values
|
|
9
10
|
# @return [Object] id
|
|
10
|
-
def _insert_record(values, returning)
|
|
11
|
+
def _insert_record(connection, values, returning)
|
|
11
12
|
# values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so the casted values gets resolved here
|
|
12
13
|
values = values.transform_values(&:value)
|
|
13
14
|
|
|
14
15
|
# resolve & update a auto_increment value, if configured
|
|
15
|
-
_insert_with_auto_increment(values) do |arguments|
|
|
16
|
+
_insert_with_auto_increment(connection, values) do |arguments|
|
|
16
17
|
# build new query
|
|
17
18
|
query = ElasticsearchRecord::Query.new(
|
|
18
19
|
index: table_name,
|
|
@@ -62,7 +63,7 @@ module ElasticsearchRecord
|
|
|
62
63
|
private
|
|
63
64
|
|
|
64
65
|
# Resolves the +auto_increment+ status from the tables +_meta+ attributes.
|
|
65
|
-
def _insert_with_auto_increment(values)
|
|
66
|
+
def _insert_with_auto_increment(connection, values)
|
|
66
67
|
# check, if the primary_key's value is provided.
|
|
67
68
|
# so, no need to resolve a +auto_increment+ value, but provide the id directly
|
|
68
69
|
if (id = values[self.primary_key]).present?
|
|
@@ -42,8 +42,9 @@ module ElasticsearchRecord
|
|
|
42
42
|
# @param [String, Hash, ElasticsearchRecord::Query] sql
|
|
43
43
|
# @param [Array] binds
|
|
44
44
|
# @param [nil] preparable
|
|
45
|
+
# @param [Boolean] allow_retry
|
|
45
46
|
# @param [Proc] block
|
|
46
|
-
def find_by_sql(sql, binds = [], preparable: nil, &block)
|
|
47
|
+
def find_by_sql(sql, binds = [], preparable: nil, allow_retry: false, &block)
|
|
47
48
|
query = case sql
|
|
48
49
|
when String # really find by SQL
|
|
49
50
|
ElasticsearchRecord::Query.new(
|
|
@@ -61,7 +62,11 @@ module ElasticsearchRecord
|
|
|
61
62
|
sql
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
result = with_connection do |c|
|
|
66
|
+
_query_by_sql(c, query, binds, preparable: preparable, allow_retry: allow_retry)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
_load_from_sql(result, &block)
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
# finds records by query arguments
|
|
@@ -74,7 +79,11 @@ module ElasticsearchRecord
|
|
|
74
79
|
# IMPORTANT: Always provide all columns to prevent unknown attributes that should be nil ...
|
|
75
80
|
columns: source_column_names)
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
result = with_connection do |c|
|
|
83
|
+
_query_by_sql(c, query)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
_load_from_sql(result, &block)
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
# ES|QL query API
|
|
@@ -90,7 +99,11 @@ module ElasticsearchRecord
|
|
|
90
99
|
# IMPORTANT: Always provide all columns
|
|
91
100
|
columns: source_column_names)
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
result = with_connection do |c|
|
|
103
|
+
_query_by_sql(c, query)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
_load_from_sql(result, &block)
|
|
94
107
|
end
|
|
95
108
|
|
|
96
109
|
# executes a +esql+ by provided *ES|SL* query
|
|
@@ -123,17 +136,28 @@ module ElasticsearchRecord
|
|
|
123
136
|
connection.exec_query(query, "#{name} Msearch")
|
|
124
137
|
end
|
|
125
138
|
|
|
126
|
-
# executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded
|
|
139
|
+
# executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded.
|
|
140
|
+
#
|
|
141
|
+
# Without a block the trailing options-hash is used as query arguments.
|
|
142
|
+
# With a block the +Elasticsearch::DSL+ gem builds the query - if the gem is not
|
|
143
|
+
# available (or the block fails), the block is SILENTLY ignored and the
|
|
144
|
+
# options-hash is used instead.
|
|
127
145
|
def search(*args, &block)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
query = if block_given?
|
|
147
|
+
begin
|
|
148
|
+
# require the Elasticsearch::DSL gem, if loaded
|
|
149
|
+
require 'elasticsearch/dsl'
|
|
150
|
+
# PLEASE NOTE: +Search#to_hash+ returns the request BODY - it must be nested
|
|
151
|
+
# into the query arguments, otherwise it is sent as (invalid) URL parameters.
|
|
152
|
+
{ body: ::Elasticsearch::DSL::Search::Search.new(*args, &block).to_hash }
|
|
153
|
+
rescue LoadError
|
|
154
|
+
args.extract_options!
|
|
155
|
+
rescue
|
|
156
|
+
args.extract_options!
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
args.extract_options!
|
|
160
|
+
end
|
|
137
161
|
|
|
138
162
|
find_by_query(query)
|
|
139
163
|
end
|
|
@@ -10,7 +10,7 @@ module ElasticsearchRecord
|
|
|
10
10
|
# transforms the current relation into arel, compiles it to query and executes the query.
|
|
11
11
|
# returns the result object.
|
|
12
12
|
#
|
|
13
|
-
# PLEASE NOTE: This makes the query +immutable+ and raises a +ActiveRecord::
|
|
13
|
+
# PLEASE NOTE: This makes the query +immutable+ and raises a +ActiveRecord::UnmodifiableRelation+
|
|
14
14
|
# if you try to change it's values.
|
|
15
15
|
#
|
|
16
16
|
# PLEASE NOTE: resolving records _(instantiate)_ is never possible after calling this method!
|
|
@@ -109,7 +109,8 @@ module ElasticsearchRecord # :nodoc:
|
|
|
109
109
|
left = left[key]
|
|
110
110
|
right = right[key]
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
# since rails 7.2 +Arel::Nodes::Or+ is a nary node and takes its children as a single Array
|
|
113
|
+
or_clause = Arel::Nodes::Or.new([left, right])
|
|
113
114
|
|
|
114
115
|
common.predicates[key] = ElasticsearchRecord::Relation::QueryClause.new(key, [Arel::Nodes::Grouping.new(or_clause)])
|
|
115
116
|
common
|
|
@@ -260,7 +260,7 @@ module ElasticsearchRecord
|
|
|
260
260
|
unless _valid_unscoping_values.include?(scope)
|
|
261
261
|
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{_valid_unscoping_values.to_a.join(", :")}."
|
|
262
262
|
end
|
|
263
|
-
|
|
263
|
+
assert_modifiable!
|
|
264
264
|
@values.delete(scope)
|
|
265
265
|
when Hash
|
|
266
266
|
scope.each do |key, target_value|
|
|
@@ -8,7 +8,7 @@ module ElasticsearchRecord
|
|
|
8
8
|
|
|
9
9
|
def kind_value=(value)
|
|
10
10
|
# checks if records are already loaded - in this case we cannot mutate the query anymore
|
|
11
|
-
|
|
11
|
+
assert_modifiable!
|
|
12
12
|
|
|
13
13
|
@values[:kind] = value.to_sym
|
|
14
14
|
end
|
|
@@ -18,7 +18,7 @@ module ElasticsearchRecord
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def configure_value=(value)
|
|
21
|
-
|
|
21
|
+
assert_modifiable!
|
|
22
22
|
|
|
23
23
|
@values[:configure] = value
|
|
24
24
|
end
|
|
@@ -28,7 +28,7 @@ module ElasticsearchRecord
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def query_clause=(value)
|
|
31
|
-
|
|
31
|
+
assert_modifiable!
|
|
32
32
|
|
|
33
33
|
@values[:query] = value
|
|
34
34
|
end
|
|
@@ -38,7 +38,7 @@ module ElasticsearchRecord
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def aggs_clause=(value)
|
|
41
|
-
|
|
41
|
+
assert_modifiable!
|
|
42
42
|
|
|
43
43
|
@values[:aggs] = value
|
|
44
44
|
end
|
|
@@ -172,6 +172,16 @@ module ElasticsearchRecord
|
|
|
172
172
|
n ? computed_results.last(n) : computed_results.last
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# Returns the rows to instantiate records from - ActiveRecord resolves them through this
|
|
176
|
+
# method instead of iterating the result set itself.
|
|
177
|
+
# Each element is a Hash keyed by column / metadata name and therefore already provides the
|
|
178
|
+
# read surface of +ActiveRecord::Result::IndexedRow+ (+key?+, +[]+, +fetch+, +keys+, ...).
|
|
179
|
+
# used by +ActiveRecord+
|
|
180
|
+
# @return [Array<Hash>]
|
|
181
|
+
def indexed_rows # :nodoc:
|
|
182
|
+
computed_results
|
|
183
|
+
end
|
|
184
|
+
|
|
175
185
|
# used by ActiveRecord
|
|
176
186
|
def cancel # :nodoc:
|
|
177
187
|
self
|
|
@@ -14,9 +14,12 @@ module ElasticsearchRecord
|
|
|
14
14
|
sm = Arel::SelectManager.new(arel_table)
|
|
15
15
|
sm.project(arel_table[primary_key])
|
|
16
16
|
sm.order(arel_table[primary_key].asc)
|
|
17
|
-
sm.take(connection.max_result_window(table_name))
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
@pool.with_connection do |connection|
|
|
19
|
+
sm.take(connection.max_result_window(table_name))
|
|
20
|
+
|
|
21
|
+
connection.select_values(sm, "#{self.class} Load")
|
|
22
|
+
end
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
File without changes
|
data/lib/elasticsearch_record.rb
CHANGED
|
@@ -22,6 +22,7 @@ module ElasticsearchRecord
|
|
|
22
22
|
eager_autoload do
|
|
23
23
|
autoload :Base
|
|
24
24
|
autoload :Core
|
|
25
|
+
autoload :InternalMetadata
|
|
25
26
|
autoload :ModelSchema
|
|
26
27
|
autoload :ModelApi
|
|
27
28
|
autoload :Persistence
|
|
@@ -84,6 +85,7 @@ end
|
|
|
84
85
|
|
|
85
86
|
ActiveSupport.on_load(:active_record) do
|
|
86
87
|
# load patches
|
|
88
|
+
require 'elasticsearch_record/patches/active_record/connection_pool_patch'
|
|
87
89
|
require 'elasticsearch_record/patches/active_record/relation_merger_patch'
|
|
88
90
|
require 'elasticsearch_record/patches/arel/select_core_patch'
|
|
89
91
|
require 'elasticsearch_record/patches/arel/select_manager_patch'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: elasticsearch_record
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tobias Gonsior
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 8.0.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 8.0.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: elasticsearch
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -186,8 +186,10 @@ files:
|
|
|
186
186
|
- lib/elasticsearch_record/instrumentation/controller_runtime.rb
|
|
187
187
|
- lib/elasticsearch_record/instrumentation/log_subscriber.rb
|
|
188
188
|
- lib/elasticsearch_record/instrumentation/railtie.rb
|
|
189
|
+
- lib/elasticsearch_record/internal_metadata.rb
|
|
189
190
|
- lib/elasticsearch_record/model_api.rb
|
|
190
191
|
- lib/elasticsearch_record/model_schema.rb
|
|
192
|
+
- lib/elasticsearch_record/patches/active_record/connection_pool_patch.rb
|
|
191
193
|
- lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb
|
|
192
194
|
- lib/elasticsearch_record/patches/arel/select_core_patch.rb
|
|
193
195
|
- lib/elasticsearch_record/patches/arel/select_manager_patch.rb
|
|
@@ -226,7 +228,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
226
228
|
requirements:
|
|
227
229
|
- - ">="
|
|
228
230
|
- !ruby/object:Gem::Version
|
|
229
|
-
version:
|
|
231
|
+
version: 3.1.0
|
|
230
232
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
231
233
|
requirements:
|
|
232
234
|
- - ">="
|