elasticsearch_record 1.8.2 → 3.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/.rspec +0 -0
- data/.yardopts +0 -0
- data/Gemfile +7 -0
- data/README.md +177 -19
- data/Rakefile +0 -0
- data/docs/CHANGELOG.md +66 -0
- data/docs/CODE_OF_CONDUCT.md +0 -0
- data/docs/LICENSE +0 -0
- data/elasticsearch_record.gemspec +3 -3
- data/lib/active_record/connection_adapters/elasticsearch/column.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +19 -17
- data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +16 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_creation.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/attribute_methods.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/clone_table_definition.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/create_table_definition.rb +6 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_alias_definition.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_definition.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_meta_definition.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_setting_definition.rb +7 -5
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/update_table_definition.rb +3 -1
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +24 -3
- data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +39 -34
- data/lib/active_record/connection_adapters/elasticsearch/table_statements.rb +251 -72
- data/lib/active_record/connection_adapters/elasticsearch/transactions.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/format_string.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/nested.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/object.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/range.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/type.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/unsupported_implementation.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +164 -99
- data/lib/arel/collectors/elasticsearch_query.rb +3 -0
- data/lib/arel/nodes/select_agg.rb +0 -0
- data/lib/arel/nodes/select_configure.rb +0 -0
- data/lib/arel/nodes/select_kind.rb +0 -0
- data/lib/arel/nodes/select_query.rb +0 -0
- data/lib/arel/visitors/elasticsearch.rb +0 -0
- data/lib/arel/visitors/elasticsearch_query.rb +51 -9
- data/lib/elasticsearch_record/base.rb +0 -0
- data/lib/elasticsearch_record/core.rb +1 -1
- data/lib/elasticsearch_record/errors.rb +0 -0
- data/lib/elasticsearch_record/extensions/relation.rb +0 -0
- data/lib/elasticsearch_record/gem_version.rb +3 -3
- data/lib/elasticsearch_record/instrumentation/controller_runtime.rb +0 -0
- data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +11 -9
- data/lib/elasticsearch_record/instrumentation/railtie.rb +0 -0
- data/lib/elasticsearch_record/instrumentation.rb +0 -0
- data/lib/elasticsearch_record/internal_metadata.rb +19 -0
- data/lib/elasticsearch_record/model_api.rb +8 -5
- data/lib/elasticsearch_record/model_schema.rb +1 -1
- data/lib/elasticsearch_record/patches/active_record/connection_pool_patch.rb +57 -0
- data/lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb +0 -0
- data/lib/elasticsearch_record/patches/arel/select_core_patch.rb +0 -0
- data/lib/elasticsearch_record/patches/arel/select_manager_patch.rb +0 -0
- data/lib/elasticsearch_record/patches/arel/select_statement_patch.rb +0 -0
- data/lib/elasticsearch_record/patches/arel/update_manager_patch.rb +0 -0
- data/lib/elasticsearch_record/patches/arel/update_statement_patch.rb +0 -0
- data/lib/elasticsearch_record/persistence.rb +39 -21
- data/lib/elasticsearch_record/query.rb +53 -33
- data/lib/elasticsearch_record/querying.rb +43 -21
- data/lib/elasticsearch_record/relation/calculation_methods.rb +38 -2
- data/lib/elasticsearch_record/relation/core_methods.rb +1 -1
- data/lib/elasticsearch_record/relation/query_clause.rb +0 -0
- data/lib/elasticsearch_record/relation/query_clause_tree.rb +2 -1
- data/lib/elasticsearch_record/relation/query_methods.rb +31 -1
- data/lib/elasticsearch_record/relation/result_methods.rb +47 -43
- data/lib/elasticsearch_record/relation/value_methods.rb +4 -4
- data/lib/elasticsearch_record/result.rb +144 -60
- data/lib/elasticsearch_record/schema_migration.rb +5 -2
- data/lib/elasticsearch_record/statement_cache.rb +0 -0
- data/lib/elasticsearch_record/tasks/elasticsearch_database_tasks.rb +0 -0
- data/lib/elasticsearch_record/version.rb +0 -0
- data/lib/elasticsearch_record.rb +17 -0
- metadata +22 -9
|
@@ -22,23 +22,23 @@ 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
|
|
27
|
+
include Elasticsearch::UnsupportedImplementation
|
|
28
|
+
include Elasticsearch::Quoting
|
|
29
|
+
include Elasticsearch::DatabaseStatements
|
|
30
|
+
include Elasticsearch::SchemaStatements
|
|
31
|
+
include Elasticsearch::TableStatements
|
|
32
|
+
include Elasticsearch::Transactions
|
|
33
|
+
|
|
37
34
|
# defines the *Elasticsearch* adapter name.
|
|
38
|
-
|
|
35
|
+
# @return [String]
|
|
36
|
+
ADAPTER_NAME = "Elasticsearch".freeze
|
|
39
37
|
|
|
40
38
|
# defines the Elasticsearch 'base' structure, which is always included but cannot be resolved through mappings ...
|
|
41
|
-
|
|
39
|
+
# - see @ https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html
|
|
40
|
+
# @return [Hash]
|
|
41
|
+
METADATA_FIELDS = [
|
|
42
42
|
{ 'name' => '_id', 'type' => 'keyword', 'meta' => { 'primary_key' => 'true' } },
|
|
43
43
|
{ 'name' => '_index', 'type' => 'keyword', 'virtual' => true },
|
|
44
44
|
{ 'name' => '_score', 'type' => 'float', 'virtual' => true },
|
|
@@ -46,17 +46,10 @@ module ActiveRecord # :nodoc:
|
|
|
46
46
|
{ 'name' => '_ignored', 'type' => 'boolean', 'virtual' => true }
|
|
47
47
|
].freeze
|
|
48
48
|
|
|
49
|
-
include Elasticsearch::UnsupportedImplementation
|
|
50
|
-
include Elasticsearch::Quoting
|
|
51
|
-
include Elasticsearch::DatabaseStatements
|
|
52
|
-
include Elasticsearch::SchemaStatements
|
|
53
|
-
include Elasticsearch::TableStatements
|
|
54
|
-
include Elasticsearch::Transactions
|
|
55
|
-
|
|
56
49
|
class << self
|
|
57
|
-
def
|
|
50
|
+
def metadata_keys
|
|
58
51
|
# using a class_variable to not reinitialize for descendants
|
|
59
|
-
@@
|
|
52
|
+
@@metadata_keys ||= METADATA_FIELDS.map { |struct| struct['name'] }.freeze
|
|
60
53
|
end
|
|
61
54
|
|
|
62
55
|
def new_client(config)
|
|
@@ -65,7 +58,7 @@ module ActiveRecord # :nodoc:
|
|
|
65
58
|
# add rails logger manually, if +:log+ is true
|
|
66
59
|
client_config[:logger] = logger if client_config.delete(:log)
|
|
67
60
|
|
|
68
|
-
# build
|
|
61
|
+
# build and return new client
|
|
69
62
|
::Elasticsearch::Client.new(client_config)
|
|
70
63
|
rescue ::Elastic::Transport::Transport::Errors::Unauthorized
|
|
71
64
|
raise ::ActiveRecord::DatabaseConnectionError.username_error(config[:user])
|
|
@@ -129,20 +122,22 @@ module ActiveRecord # :nodoc:
|
|
|
129
122
|
end
|
|
130
123
|
end
|
|
131
124
|
|
|
125
|
+
# -- TYPE MAP -- DO NOT insert before this line (*initialize_type_map* needs to be rewritten)
|
|
132
126
|
# reinitialize the constant with new types
|
|
133
127
|
TYPE_MAP = ActiveRecord::Type::HashLookupTypeMap.new.tap { |m| initialize_type_map(m) }
|
|
134
128
|
|
|
135
129
|
# define native types - which will be used for schema-dumping
|
|
130
|
+
# @return [Hash]
|
|
136
131
|
NATIVE_DATABASE_TYPES = {
|
|
137
132
|
primary_key: { name: 'long' }, # maybe this hae to changed to 'keyword'
|
|
138
133
|
string: { name: 'keyword' },
|
|
139
134
|
blob: { name: 'binary' },
|
|
140
135
|
datetime: { name: 'date' },
|
|
141
136
|
bigint: { name: 'long' },
|
|
142
|
-
json: { name: 'object' }
|
|
143
|
-
|
|
144
|
-
TYPE_MAP.keys.map { |key| [key.to_sym, { name: key }] }.to_h
|
|
145
|
-
|
|
137
|
+
json: { name: 'object' },
|
|
138
|
+
|
|
139
|
+
**TYPE_MAP.keys.map { |key| [key.to_sym, { name: key }] }.to_h
|
|
140
|
+
}.freeze
|
|
146
141
|
|
|
147
142
|
def initialize(...)
|
|
148
143
|
super
|
|
@@ -163,15 +158,27 @@ module ActiveRecord # :nodoc:
|
|
|
163
158
|
end
|
|
164
159
|
|
|
165
160
|
def schema_migration # :nodoc:
|
|
166
|
-
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
|
|
167
172
|
end
|
|
168
173
|
|
|
169
174
|
# provide a table_name_prefix from the configuration to create & restrict schema creation
|
|
175
|
+
# HINT: This is not an official setting and only introduced to ElasticsearchRecord.
|
|
170
176
|
def table_name_prefix
|
|
171
177
|
@config.fetch(:table_name_prefix, '')
|
|
172
178
|
end
|
|
173
179
|
|
|
174
180
|
# provide a table_name_suffix from the configuration to create & restrict schema creation
|
|
181
|
+
# HINT: This is not an official setting and only introduced to ElasticsearchRecord.
|
|
175
182
|
def table_name_suffix
|
|
176
183
|
@config.fetch(:table_name_suffix, '')
|
|
177
184
|
end
|
|
@@ -188,7 +195,7 @@ module ActiveRecord # :nodoc:
|
|
|
188
195
|
end
|
|
189
196
|
|
|
190
197
|
# Does this adapter support transactions in general?
|
|
191
|
-
# HINT: This is
|
|
198
|
+
# HINT: This is not an official setting and only introduced to ElasticsearchRecord.
|
|
192
199
|
def supports_transactions?
|
|
193
200
|
false
|
|
194
201
|
end
|
|
@@ -229,51 +236,69 @@ module ActiveRecord # :nodoc:
|
|
|
229
236
|
NATIVE_DATABASE_TYPES
|
|
230
237
|
end
|
|
231
238
|
|
|
232
|
-
# calls the +elasticsearch-api+ endpoints by provided
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
# @param [Symbol] namespace - the API namespace (e.g. indices, nodes, sql, ...)
|
|
236
|
-
# @param [Symbol] action - the API action to call in tha namespace
|
|
237
|
-
# @param [Hash] arguments - action arguments
|
|
239
|
+
# calls the +elasticsearch-api+ endpoints by provided gate and returns a response object.
|
|
240
|
+
# @param [Symbol,String] gate - the API namespace & action gate (e.g. 'indices.get','cluster.health', :bulk, ...)
|
|
241
|
+
# @param [Hash] arguments - gate arguments
|
|
238
242
|
# @param [String (frozen)] name - the logging name
|
|
239
|
-
# @param [Boolean] async - send async (default: false) -
|
|
240
|
-
# @param [Boolean]
|
|
243
|
+
# @param [Boolean] async - send async (default: false) - NOT supported!
|
|
244
|
+
# @param [Boolean] allow_retry - allows to retry a possible failing query (default: false)
|
|
245
|
+
# @param [Boolean] materialize_transactions - materializes transactions (default: false) - NOT supported!
|
|
241
246
|
# @return [Elasticsearch::API::Response, Object]
|
|
242
|
-
def api(
|
|
247
|
+
def api(gate, arguments = {}, name = 'API', async: false, allow_retry: false, materialize_transactions: false)
|
|
248
|
+
raise ::ArgumentError, "ElasticsearchRecord API call is now using a single `gate` instead of providing `namespace & action`.\n'core' namespace must not provided (only provide action symbol) - any other must be provided as string (e.g. 'nodes.stats')" if arguments.is_a?(Symbol)
|
|
243
249
|
raise ::StandardError, 'ASYNC api calls are not supported' if async
|
|
244
250
|
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
# drop 'core.' prefix, if provided
|
|
252
|
+
if gate.is_a?(String) && gate[0..4] == 'core.'
|
|
253
|
+
# add deprecation warning
|
|
254
|
+
::ActiveRecord.deprecator.warn(<<~MSG)
|
|
255
|
+
Providing the 'core.' namespace prefix in the `gate` parameter is deprecated and will be removed in a future version.
|
|
256
|
+
Please provide only the action symbol (e.g.:bulk) or the full namespace and action as a string (e.g.'nodes.stats')
|
|
257
|
+
MSG
|
|
258
|
+
|
|
259
|
+
# IMPORTANT: only strip the *prefix* - a +gsub+ would also replace any later occurrence
|
|
260
|
+
# (e.g. 'core.score.thing' would become :sthing)
|
|
261
|
+
gate = gate.delete_prefix('core.').to_sym
|
|
262
|
+
end
|
|
257
263
|
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
# PLEASE NOTE: Don't remove the +statistics+ assignment here.
|
|
265
|
+
# - this is required as referenced hash for the instrumentation
|
|
266
|
+
log(gate, arguments, name, async: async, statistics: (statistics = {})) do
|
|
267
|
+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
|
|
268
|
+
response = ::ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
|
269
|
+
# determinate the correct target from the provided gate
|
|
270
|
+
if gate.is_a?(Symbol) || !gate.include?('.')
|
|
271
|
+
conn.__send__(gate, arguments)
|
|
272
|
+
else
|
|
273
|
+
namespace, action = gate.split('.')
|
|
274
|
+
conn.__send__(namespace).__send__(action, arguments)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# marks the connection as verified (an AbstractAdapter method, which is used to decide how errors and connections are handled)
|
|
279
|
+
verified!
|
|
280
|
+
|
|
281
|
+
if response.is_a?(::Elasticsearch::API::Response)
|
|
282
|
+
# reverse information for the LogSubscriber - shows the 'query-time' in the logs
|
|
283
|
+
# this works, since we use a referenced hash ...
|
|
284
|
+
statistics[:took] = response['took']
|
|
285
|
+
|
|
286
|
+
# raise timeouts
|
|
287
|
+
raise(::ActiveRecord::StatementTimeout, "Elasticsearch api request failed due a timeout") if response['timed_out']
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# return response
|
|
291
|
+
response
|
|
260
292
|
end
|
|
261
|
-
|
|
262
|
-
response
|
|
263
293
|
end
|
|
264
294
|
end
|
|
265
295
|
|
|
266
|
-
private
|
|
267
|
-
|
|
268
|
-
def connect
|
|
269
|
-
@raw_connection = self.class.new_client(@connection_parameters)
|
|
270
|
-
rescue ::ActiveRecord::ConnectionNotEstablished => ex
|
|
271
|
-
raise ex.set_pool(@pool)
|
|
272
|
-
end
|
|
273
|
-
|
|
274
296
|
def reconnect
|
|
275
|
-
@
|
|
276
|
-
|
|
297
|
+
@lock.synchronize do
|
|
298
|
+
# no need to 'close' a connection for +::Elasticsearch::Client+ / +::Elastic::Transport+
|
|
299
|
+
@raw_connection = nil
|
|
300
|
+
connect
|
|
301
|
+
end
|
|
277
302
|
end
|
|
278
303
|
|
|
279
304
|
def active?
|
|
@@ -283,62 +308,94 @@ module ActiveRecord # :nodoc:
|
|
|
283
308
|
# Disconnects from the database if already connected.
|
|
284
309
|
# Otherwise, this method does nothing.
|
|
285
310
|
def disconnect!
|
|
286
|
-
|
|
287
|
-
|
|
311
|
+
@lock.synchronize do
|
|
312
|
+
super
|
|
313
|
+
# no need to 'close' a connection for +::Elasticsearch::Client+ / +::Elastic::Transport+
|
|
314
|
+
@raw_connection = nil
|
|
315
|
+
end
|
|
288
316
|
end
|
|
289
317
|
|
|
290
318
|
alias :reset! :reconnect!
|
|
291
319
|
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def connect
|
|
323
|
+
@raw_connection = self.class.new_client(@connection_parameters)
|
|
324
|
+
rescue ::ActiveRecord::ConnectionNotEstablished => ex
|
|
325
|
+
raise ex.set_pool(@pool)
|
|
326
|
+
end
|
|
327
|
+
|
|
292
328
|
def type_map
|
|
293
329
|
TYPE_MAP
|
|
294
330
|
end
|
|
295
331
|
|
|
332
|
+
# OVERWRITE - to support +::ActiveRecord::StatementTimeout+ as possible retry.
|
|
333
|
+
# IMPORTANT: From the point ES supports transactions (+#supports_transactions?+) this must be redefined with a *super* call.
|
|
334
|
+
def retryable_query_error?(exception)
|
|
335
|
+
exception.is_a?(::ActiveRecord::StatementTimeout) || exception.is_a?(::ActiveRecord::Deadlocked) || exception.is_a?(::ActiveRecord::LockWaitTimeout)
|
|
336
|
+
end
|
|
337
|
+
|
|
296
338
|
# catch Elasticsearch Transport-errors to be treated as +StatementInvalid+ (the original message is still readable ...)
|
|
297
339
|
def translate_exception(exception, message:, sql:, binds:)
|
|
298
340
|
case exception
|
|
299
341
|
when ::Elastic::Transport::Transport::Errors::ClientClosedRequest
|
|
300
|
-
::ActiveRecord::QueryCanceled.new(message, sql: sql, binds: binds)
|
|
342
|
+
::ActiveRecord::QueryCanceled.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
301
343
|
when ::Elastic::Transport::Transport::Errors::RequestTimeout
|
|
302
|
-
::ActiveRecord::StatementTimeout.new(message, sql: sql, binds: binds)
|
|
344
|
+
::ActiveRecord::StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
303
345
|
when ::Elastic::Transport::Transport::Errors::Conflict
|
|
304
|
-
::ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds)
|
|
346
|
+
::ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
305
347
|
when ::Elastic::Transport::Transport::Errors::BadRequest
|
|
306
348
|
if exception.message.match?(/resource_already_exists_exception/)
|
|
307
|
-
::ActiveRecord::DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
|
|
349
|
+
::ActiveRecord::DatabaseAlreadyExists.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
308
350
|
else
|
|
309
|
-
::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
|
|
351
|
+
::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
310
352
|
end
|
|
311
353
|
when ::Elastic::Transport::Transport::Errors::Unauthorized
|
|
312
354
|
::ActiveRecord::DatabaseConnectionError.username_error(@config[:username])
|
|
313
355
|
# must be last 'Elastic' error
|
|
314
356
|
when ::Elastic::Transport::Transport::ServerError
|
|
315
|
-
::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
|
|
357
|
+
::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
|
316
358
|
else
|
|
317
359
|
# just forward the exception ...
|
|
318
360
|
exception
|
|
319
361
|
end
|
|
320
362
|
end
|
|
321
363
|
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
364
|
+
# Logs and instruments Elasticsearch operations.
|
|
365
|
+
#
|
|
366
|
+
# This method facilitates logging and instrumentation of Elasticsearch queries
|
|
367
|
+
# by delegating the execution to the instrumenter. It also captures any raised
|
|
368
|
+
# exceptions during execution, translating them into a domain-specific class.
|
|
369
|
+
#
|
|
370
|
+
# Queries are tracked under the "query.elasticsearch_record" event, allowing
|
|
371
|
+
# listeners or subscribers to analyze the performance and behavior of the
|
|
372
|
+
# queries.
|
|
373
|
+
#
|
|
374
|
+
# A `translate_exception_class` helper is used to ensure that any exceptions
|
|
375
|
+
# raised are appropriately captured and translated with contextual details.
|
|
376
|
+
#
|
|
377
|
+
# @param [Symbol, String] gate the target gate for the Elasticsearch operation,
|
|
378
|
+
# representing either a `<namespace>.<action>` or an `core.<action>`.
|
|
379
|
+
# @param [Hash] arguments the request parameters to be sent for the operation.
|
|
380
|
+
# @param [String] name a label for the operation, defaulting to 'QUERY'.
|
|
381
|
+
# @param [Boolean] async whether the operation is asynchronous. Defaults to `false`.
|
|
382
|
+
# @param [Hash, nil] statistics an optional hash for gathering operation metadata.
|
|
383
|
+
# @yield [block] the operation block to be executed under logging and instrumentation.
|
|
384
|
+
# @raise [Exception] any exception encountered during execution, after translation.
|
|
385
|
+
# @return [void]
|
|
386
|
+
def log(gate, arguments, name = 'QUERY', async: false, statistics: nil, &block)
|
|
387
|
+
@instrumenter.instrument(
|
|
388
|
+
"query.elasticsearch_record",
|
|
389
|
+
gate: gate,
|
|
390
|
+
name: name,
|
|
391
|
+
arguments: arguments,
|
|
392
|
+
async: async,
|
|
393
|
+
statistics: statistics,
|
|
394
|
+
connection: self,
|
|
395
|
+
&block
|
|
396
|
+
)
|
|
397
|
+
rescue => e
|
|
398
|
+
raise translate_exception_class(e, arguments, [])
|
|
342
399
|
end
|
|
343
400
|
|
|
344
401
|
# returns a new collector for the Arel visitor.
|
|
@@ -348,13 +405,13 @@ module ActiveRecord # :nodoc:
|
|
|
348
405
|
# so we don't have to check for +prepared_statements+ here.
|
|
349
406
|
# Also, bindings are (currently) not supported.
|
|
350
407
|
# So, we just need a single, simple query collector...
|
|
351
|
-
Arel::Collectors::ElasticsearchQuery.new
|
|
408
|
+
::Arel::Collectors::ElasticsearchQuery.new
|
|
352
409
|
end
|
|
353
410
|
|
|
354
411
|
# returns a new visitor to compile Arel into Elasticsearch Hashes (in this case we use a query object)
|
|
355
412
|
# @return [Arel::Visitors::Elasticsearch]
|
|
356
413
|
def arel_visitor
|
|
357
|
-
Arel::Visitors::Elasticsearch.new(self)
|
|
414
|
+
::Arel::Visitors::Elasticsearch.new(self)
|
|
358
415
|
end
|
|
359
416
|
|
|
360
417
|
# Builds the result object.
|
|
@@ -362,14 +419,22 @@ module ActiveRecord # :nodoc:
|
|
|
362
419
|
# custom result objects with response-specific data.
|
|
363
420
|
# @return [ElasticsearchRecord::Result]
|
|
364
421
|
def build_result(response, columns: [], column_types: {})
|
|
365
|
-
ElasticsearchRecord::Result.new(response, columns, column_types)
|
|
422
|
+
::ElasticsearchRecord::Result.new(response, columns, column_types)
|
|
366
423
|
end
|
|
367
424
|
|
|
368
425
|
# register native types
|
|
369
|
-
ActiveRecord::Type.register(:format_string, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString, adapter: :elasticsearch)
|
|
370
|
-
ActiveRecord::Type.register(:multicast_value, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue, adapter: :elasticsearch)
|
|
371
|
-
ActiveRecord::Type.register(:object, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object, adapter: :elasticsearch, override: false)
|
|
372
|
-
ActiveRecord::Type.register(:range, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
|
|
426
|
+
::ActiveRecord::Type.register(:format_string, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString, adapter: :elasticsearch)
|
|
427
|
+
::ActiveRecord::Type.register(:multicast_value, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue, adapter: :elasticsearch)
|
|
428
|
+
::ActiveRecord::Type.register(:object, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object, adapter: :elasticsearch, override: false)
|
|
429
|
+
::ActiveRecord::Type.register(:range, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
|
|
373
430
|
end
|
|
374
431
|
end
|
|
375
|
-
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# since rails 7.2 an adapter has to register itself - the former load-by-convention
|
|
435
|
+
# through +ConnectionHandling#elasticsearch_connection+ is gone
|
|
436
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
437
|
+
'elasticsearch',
|
|
438
|
+
'ActiveRecord::ConnectionAdapters::ElasticsearchAdapter',
|
|
439
|
+
'active_record/connection_adapters/elasticsearch_adapter'
|
|
440
|
+
)
|
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -122,7 +122,8 @@ module Arel # :nodoc: all
|
|
|
122
122
|
# IMPORTANT: Since Elasticsearch does not store nil-values in the +_source+ / +doc+ it will NOT return
|
|
123
123
|
# empty / nil columns - instead the nil columns do not exist!!!
|
|
124
124
|
# This is a big mess, because those missing columns are +not+ editable or savable in any way after we initialize the record...
|
|
125
|
-
# To prevent NOT-accessible attributes, we need to provide the "full-column-definition" to the query
|
|
125
|
+
# To prevent NOT-accessible attributes, we need to provide the "full-column-definition" to the query as +default+.
|
|
126
|
+
# This may be overwritten by the 'visit_Selects'
|
|
126
127
|
resource_klass = o.source.left.instance_variable_get(:@klass)
|
|
127
128
|
claim(:columns, resource_klass.source_column_names) if resource_klass.respond_to?(:source_column_names)
|
|
128
129
|
|
|
@@ -193,22 +194,54 @@ module Arel # :nodoc: all
|
|
|
193
194
|
when '*'
|
|
194
195
|
# force return all fields
|
|
195
196
|
# assign(:_source, true)
|
|
197
|
+
when ::ElasticsearchRecord::Query::COLUMNS_NONE
|
|
198
|
+
# force return NO fields - metadata fields ('_id', '_score', ...) are not part of the
|
|
199
|
+
# +_source+ and are always returned on the document level, so they stay accessible.
|
|
200
|
+
assign(:_source, false)
|
|
201
|
+
|
|
202
|
+
# clears the columns claimed by +visit_Arel_Nodes_SelectCore+ - a projection is the only
|
|
203
|
+
# place that runs late enough to undo them.
|
|
204
|
+
# HINT: an empty array equals the +Query+ default, so +Result#_results_from_hits+ takes its
|
|
205
|
+
# "no columns" branch and returns the raw document. Together with the +_source: false+ above
|
|
206
|
+
# this leaves exactly the metadata fields.
|
|
207
|
+
claim(:columns, [])
|
|
196
208
|
when ::ActiveRecord::FinderMethods::ONE_AS_ONE
|
|
197
209
|
# force return NO fields
|
|
198
210
|
assign(:_source, false)
|
|
211
|
+
|
|
212
|
+
# also clear the columns in the query (which will be forwarded to +ElasticsearchRecord::Result+)
|
|
213
|
+
# HINT: If future changes rely on the first column value (in this case '1' -> SELECT 1 AS one) this must be fixed within the +ElasticsearchRecord::Result+.
|
|
214
|
+
# Maybe check on the columns[0] value within the #rows method ...
|
|
215
|
+
claim(:columns, %w[one])
|
|
199
216
|
else
|
|
200
|
-
|
|
201
|
-
#
|
|
217
|
+
# IMPORTANT: metadata fields (like '_id' or '_score') are NOT part of the +_source+ node - they
|
|
218
|
+
# are always returned on the document level. Providing them to the +_source+-filter would create
|
|
219
|
+
# a filter that never matches, so they must be removed here.
|
|
220
|
+
source_fields = fields - ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys
|
|
221
|
+
|
|
222
|
+
# if ONLY metadata fields were provided we must not resolve any +_source+ at all.
|
|
223
|
+
assign(:_source, source_fields.presence || false)
|
|
224
|
+
|
|
225
|
+
# also overwrite the columns in the query (which will be forwarded to +ElasticsearchRecord::Result+)
|
|
226
|
+
# HINT: metadata fields must stay within the columns - they are resolved from the document level.
|
|
202
227
|
claim(:columns, fields)
|
|
203
228
|
end
|
|
204
229
|
end
|
|
205
230
|
|
|
206
231
|
# CUSTOM node by elasticsearch_record
|
|
207
232
|
def visit_Create(o)
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
# detect, if *columns* where provided.
|
|
234
|
+
# This happens through the +::Arel::InsertManager#insert+ by splitting up the columns & values.
|
|
235
|
+
values = if o.columns.present?
|
|
236
|
+
# IMPORTANT: we do not "visit" the columns & rows here but directly build a final hash.
|
|
237
|
+
# o.values.rows.*first* (first) is correct here, since +::Arel::InsertManager#create_values+ will provide all values in a nested Array
|
|
238
|
+
collect(Hash[o.columns.map(&:name).zip(o.values.rows.first)])
|
|
239
|
+
elsif o.values.present?
|
|
240
|
+
collect(o.values)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
if values.present?
|
|
244
|
+
claim(:body, values)
|
|
212
245
|
else
|
|
213
246
|
failed!
|
|
214
247
|
end
|
|
@@ -356,9 +389,9 @@ module Arel # :nodoc: all
|
|
|
356
389
|
self.collector.add_binds(values, o.proc_for_binds)
|
|
357
390
|
|
|
358
391
|
if o.type == :in
|
|
359
|
-
assign(:filter, [{ terms: { o.
|
|
392
|
+
assign(:filter, [{ terms: { visit(o.left) => o.casted_values } }])
|
|
360
393
|
else
|
|
361
|
-
assign(:must_not, [{ terms: { o.
|
|
394
|
+
assign(:must_not, [{ terms: { visit(o.left) => o.casted_values } }])
|
|
362
395
|
end
|
|
363
396
|
end
|
|
364
397
|
|
|
@@ -454,6 +487,7 @@ module Arel # :nodoc: all
|
|
|
454
487
|
|
|
455
488
|
# alias for ATTRIBUTE returns
|
|
456
489
|
alias :visit_Arel_Attributes_Attribute :visit_Struct_Attribute
|
|
490
|
+
alias :visit_Arel_Attribute :visit_Struct_Attribute
|
|
457
491
|
alias :visit_Arel_Nodes_UnqualifiedColumn :visit_Struct_Attribute
|
|
458
492
|
alias :visit_ActiveModel_Attribute_FromUser :visit_Struct_Attribute
|
|
459
493
|
|
|
@@ -487,6 +521,14 @@ module Arel # :nodoc: all
|
|
|
487
521
|
|
|
488
522
|
# alias for ARRAY returns
|
|
489
523
|
alias :visit_Set :visit_Array
|
|
524
|
+
|
|
525
|
+
def visit_Arel_Nodes_True(o)
|
|
526
|
+
true
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def visit_Arel_Nodes_False(o)
|
|
530
|
+
false
|
|
531
|
+
end
|
|
490
532
|
end
|
|
491
533
|
end
|
|
492
534
|
end
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -37,12 +37,14 @@ module ElasticsearchRecord
|
|
|
37
37
|
end
|
|
38
38
|
name = "CACHE #{name}" if payload[:cached]
|
|
39
39
|
|
|
40
|
-
# nice feature: displays the REAL query-time from elasticsearch response
|
|
40
|
+
# nice feature: displays the REAL query-time from elasticsearch response
|
|
41
41
|
# this is handled through the +::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter#api+ method
|
|
42
|
-
|
|
42
|
+
if payload[:statistics].present?
|
|
43
|
+
name = "#{name} (took: #{payload[:statistics][:took].round(1)}ms)" if payload[:statistics][:took]
|
|
44
|
+
end
|
|
43
45
|
|
|
44
46
|
# build query
|
|
45
|
-
query = payload[:arguments].
|
|
47
|
+
query = payload[:arguments].inspect.gsub(/:(\w+)=>/, '\1: ').truncate((payload[:truncate] || 1000), omission: color(' (pruned)', RED))
|
|
46
48
|
|
|
47
49
|
# final coloring
|
|
48
50
|
name = color(name, name_color(payload[:name]), bold: true)
|
|
@@ -62,21 +64,21 @@ module ElasticsearchRecord
|
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def gate_color(gate, name)
|
|
65
|
-
case gate
|
|
67
|
+
case gate.to_s
|
|
66
68
|
# SELECTS
|
|
67
|
-
when '
|
|
69
|
+
when 'get', 'mget', 'search', 'msearch', 'count', 'exists', 'sql.query'
|
|
68
70
|
BLUE
|
|
69
71
|
# DELETES
|
|
70
|
-
when '
|
|
72
|
+
when 'delete', 'delete_by_query'
|
|
71
73
|
RED
|
|
72
74
|
# CREATES
|
|
73
|
-
when '
|
|
75
|
+
when 'create', 'reindex'
|
|
74
76
|
GREEN
|
|
75
77
|
# UPDATES
|
|
76
|
-
when '
|
|
78
|
+
when 'update', 'update_by_query'
|
|
77
79
|
YELLOW
|
|
78
80
|
# MIXINS
|
|
79
|
-
when /indices\.\w+/, '
|
|
81
|
+
when /indices\.\w+/, 'bulk', 'index'
|
|
80
82
|
if name.end_with?('Pit Delete')
|
|
81
83
|
RED
|
|
82
84
|
else
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|