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
|
@@ -39,7 +39,7 @@ module ElasticsearchRecord
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# delegated dangerous methods with confirm parameter (created with exclamation mark)
|
|
42
|
-
#
|
|
42
|
+
# an exception will be raised, if +confirm:true+ is missing.
|
|
43
43
|
#
|
|
44
44
|
# @example
|
|
45
45
|
# drop!(confirm: true)
|
|
@@ -183,12 +183,15 @@ module ElasticsearchRecord
|
|
|
183
183
|
# @param [Boolean] close
|
|
184
184
|
# @return [String] backup_name
|
|
185
185
|
|
|
186
|
-
# @!method restore!(from:, timeout: nil,
|
|
186
|
+
# @!method restore!(from:, timeout: nil, unblock: true, drop_backup: false)
|
|
187
187
|
# Shortcut for restore_table
|
|
188
|
+
# PLEASE NOTE: the restore runs through a +clone+, so the restored table inherits the
|
|
189
|
+
# 'write'-block of its source - +unblock+ releases it again.
|
|
188
190
|
# @param [String] from
|
|
189
191
|
# @param [String (frozen)] timeout
|
|
190
|
-
# @param [Boolean]
|
|
191
|
-
# @
|
|
192
|
+
# @param [Boolean] unblock - releases the inherited 'write'-block (default: true)
|
|
193
|
+
# @param [Boolean] drop_backup
|
|
194
|
+
# @return [nil]
|
|
192
195
|
|
|
193
196
|
# @!method reindex!(target_name, **options)
|
|
194
197
|
# Shortcut for reindex_table
|
|
@@ -267,7 +270,7 @@ module ElasticsearchRecord
|
|
|
267
270
|
def bulk(data, operation = :index, refresh: true, **options)
|
|
268
271
|
data = [data] unless data.is_a?(Array)
|
|
269
272
|
|
|
270
|
-
_connection.api(:
|
|
273
|
+
_connection.api(:bulk, {
|
|
271
274
|
index: _index_name,
|
|
272
275
|
body: case operation
|
|
273
276
|
when :update
|
|
@@ -45,7 +45,7 @@ module ElasticsearchRecord
|
|
|
45
45
|
# so this is a array of real document (+_source+) attributes of the index.
|
|
46
46
|
# @return [Array<String>]
|
|
47
47
|
def source_column_names
|
|
48
|
-
@source_column_names ||= columns.reject(&:virtual?).map(&:name) - ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.
|
|
48
|
+
@source_column_names ||= columns.reject(&:virtual?).map(&:name) - ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# returns an array with columns names, that are searchable (also includes nested fields & properties )
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -5,22 +5,23 @@ 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
|
-
index:
|
|
19
|
-
type:
|
|
19
|
+
index: table_name,
|
|
20
|
+
type: ElasticsearchRecord::Query::TYPE_CREATE,
|
|
20
21
|
# IMPORTANT: always exclude possible provided +_id+ field
|
|
21
|
-
body:
|
|
22
|
+
body: values.except('_id'),
|
|
22
23
|
arguments: arguments,
|
|
23
|
-
refresh:
|
|
24
|
+
refresh: true)
|
|
24
25
|
|
|
25
26
|
# execute query and return inserted id
|
|
26
27
|
connection.insert(query, "#{self} Create", returning: returning)
|
|
@@ -35,11 +36,11 @@ module ElasticsearchRecord
|
|
|
35
36
|
|
|
36
37
|
# build new query
|
|
37
38
|
query = ElasticsearchRecord::Query.new(
|
|
38
|
-
index:
|
|
39
|
-
type:
|
|
40
|
-
body:
|
|
39
|
+
index: table_name,
|
|
40
|
+
type: ElasticsearchRecord::Query::TYPE_UPDATE,
|
|
41
|
+
body: { doc: values },
|
|
41
42
|
arguments: { id: constraints[self.primary_key] },
|
|
42
|
-
refresh:
|
|
43
|
+
refresh: true)
|
|
43
44
|
|
|
44
45
|
# execute query and return total updates
|
|
45
46
|
connection.update(query, "#{self} Update")
|
|
@@ -50,10 +51,10 @@ module ElasticsearchRecord
|
|
|
50
51
|
def _delete_record(constraints)
|
|
51
52
|
# build new query
|
|
52
53
|
query = ElasticsearchRecord::Query.new(
|
|
53
|
-
index:
|
|
54
|
-
type:
|
|
54
|
+
index: table_name,
|
|
55
|
+
type: ElasticsearchRecord::Query::TYPE_DELETE,
|
|
55
56
|
arguments: { id: constraints[self.primary_key] },
|
|
56
|
-
refresh:
|
|
57
|
+
refresh: true)
|
|
57
58
|
|
|
58
59
|
# execute query and return total deletes
|
|
59
60
|
connection.delete(query, "#{self} Delete")
|
|
@@ -62,11 +63,11 @@ 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?
|
|
69
|
-
yield({id: id})
|
|
70
|
+
yield({ id: id })
|
|
70
71
|
elsif auto_increment?
|
|
71
72
|
# future increments: uuid (+uuidv6 ?), hex, radix(2-36), integer
|
|
72
73
|
# allocated through: primary_key_type
|
|
@@ -75,17 +76,34 @@ module ElasticsearchRecord
|
|
|
75
76
|
# try to resolve the current-auto-increment value from the tables meta
|
|
76
77
|
connection.table_metas(self.table_name).dig('auto_increment').to_i + 1,
|
|
77
78
|
# for secure reasons: also resolve the current maximum value for the primary key
|
|
78
|
-
self.unscoped.
|
|
79
|
+
self.unscoped.maximum(self.primary_key).to_i + 1
|
|
79
80
|
]
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
# yield and resolve the new inserted *result*
|
|
83
|
+
result = yield({ id: ids.max })
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
# IMPORTANT: the block returns whatever +connection.insert+ resolved, which is NOT a plain id:
|
|
86
|
+
# ActiveRecord always provides the primary_key as +returning+ column, so an ARRAY of the
|
|
87
|
+
# returning column values is resolved (see @ ActiveRecord::Persistence#_create_record).
|
|
88
|
+
# The +_meta+ must be updated with the PLAIN id - storing the raw result breaks the
|
|
89
|
+
# +.to_i+ of the NEXT insert (which then only resolves a 0 - or fails altogether).
|
|
90
|
+
id = case result
|
|
91
|
+
when Array
|
|
92
|
+
result.first
|
|
93
|
+
when Hash
|
|
94
|
+
result[:id] || result['id']
|
|
95
|
+
else
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if id.present? && id.to_i > 0
|
|
100
|
+
# IMPORTANT: elasticsearch resolves a document +_id+ as String - the +_meta+ must keep the
|
|
101
|
+
# INTEGER it was created with, so the schema does not change its type behind the first insert.
|
|
102
|
+
connection.change_meta(self.table_name, :auto_increment, id.to_i)
|
|
85
103
|
end
|
|
86
104
|
|
|
87
|
-
# return
|
|
88
|
-
|
|
105
|
+
# return the UNCHANGED insert result - it is zipped against the +returning+ columns
|
|
106
|
+
result
|
|
89
107
|
else
|
|
90
108
|
yield({})
|
|
91
109
|
end
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
module ElasticsearchRecord
|
|
2
2
|
class Query
|
|
3
3
|
# STATUS CONSTANTS
|
|
4
|
-
STATUS_VALID
|
|
4
|
+
STATUS_VALID = :valid
|
|
5
5
|
STATUS_FAILED = :failed
|
|
6
6
|
|
|
7
7
|
# -- UNDEFINED TYPE ------------------------------------------------------------------------------------------------
|
|
8
8
|
TYPE_UNDEFINED = :undefined
|
|
9
9
|
|
|
10
10
|
# -- QUERY TYPES ---------------------------------------------------------------------------------------------------
|
|
11
|
-
TYPE_COUNT
|
|
12
|
-
TYPE_SEARCH
|
|
11
|
+
TYPE_COUNT = :count
|
|
12
|
+
TYPE_SEARCH = :search
|
|
13
13
|
TYPE_MSEARCH = :msearch
|
|
14
|
-
TYPE_SQL
|
|
15
|
-
|
|
14
|
+
TYPE_SQL = :sql
|
|
15
|
+
|
|
16
|
+
# PLEASE NOTE: ES|QL requires Elasticsearch >= 8.11
|
|
17
|
+
# (the +esql+ API namespace does not exist before that)
|
|
18
|
+
TYPE_ESQL = :esql
|
|
16
19
|
|
|
17
20
|
# -- DOCUMENT TYPES ------------------------------------------------------------------------------------------------
|
|
18
|
-
TYPE_CREATE
|
|
19
|
-
TYPE_UPDATE
|
|
21
|
+
TYPE_CREATE = :create
|
|
22
|
+
TYPE_UPDATE = :update
|
|
20
23
|
TYPE_UPDATE_BY_QUERY = :update_by_query
|
|
21
|
-
TYPE_DELETE
|
|
24
|
+
TYPE_DELETE = :delete
|
|
22
25
|
TYPE_DELETE_BY_QUERY = :delete_by_query
|
|
23
26
|
|
|
24
27
|
# -- INDEX TYPES ---------------------------------------------------------------------------------------------------
|
|
25
28
|
TYPE_INDEX_CREATE = :index_create
|
|
26
|
-
TYPE_INDEX_CLONE
|
|
29
|
+
TYPE_INDEX_CLONE = :index_clone
|
|
27
30
|
# INDEX update is not implemented by Elasticsearch
|
|
28
31
|
# - this is handled through individual updates of +mappings+, +settings+ & +aliases+.
|
|
29
32
|
# INDEX delete is handled directly as API-call
|
|
30
33
|
TYPE_INDEX_UPDATE_MAPPING = :index_update_mapping
|
|
31
34
|
TYPE_INDEX_UPDATE_SETTING = :index_update_setting
|
|
32
|
-
TYPE_INDEX_UPDATE_ALIAS
|
|
33
|
-
TYPE_INDEX_DELETE_ALIAS
|
|
35
|
+
TYPE_INDEX_UPDATE_ALIAS = :index_update_alias
|
|
36
|
+
TYPE_INDEX_DELETE_ALIAS = :index_delete_alias
|
|
34
37
|
|
|
35
38
|
# includes valid types only
|
|
36
39
|
TYPES = [
|
|
@@ -54,22 +57,39 @@ module ElasticsearchRecord
|
|
|
54
57
|
# acts like the SQL-query "where('1=0')"
|
|
55
58
|
FAILED_BODIES = {
|
|
56
59
|
TYPE_SEARCH => { size: 0, query: { bool: { filter: [{ term: { _id: '_' } }] } } },
|
|
57
|
-
TYPE_COUNT
|
|
60
|
+
TYPE_COUNT => { query: { bool: { filter: [{ term: { _id: '_' } }] } } }
|
|
58
61
|
}.freeze
|
|
59
62
|
|
|
60
63
|
# defines special api gates to be used per type.
|
|
61
64
|
# if no special type is defined, it simply uses +[:core,self.type]+
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
# @return [Hash<Symbol=>String>]
|
|
66
|
+
GATES_MAP = {
|
|
67
|
+
TYPE_SQL => 'sql.query',
|
|
68
|
+
TYPE_ESQL => 'esql.query',
|
|
69
|
+
TYPE_INDEX_CREATE => 'indices.create',
|
|
70
|
+
TYPE_INDEX_CLONE => 'indices.clone',
|
|
71
|
+
TYPE_INDEX_UPDATE_MAPPING => 'indices.put_mapping',
|
|
72
|
+
TYPE_INDEX_UPDATE_SETTING => 'indices.put_settings',
|
|
73
|
+
TYPE_INDEX_UPDATE_ALIAS => 'indices.put_alias',
|
|
74
|
+
TYPE_INDEX_DELETE_ALIAS => 'indices.delete_alias'
|
|
71
75
|
}.freeze
|
|
72
76
|
|
|
77
|
+
# -- PROJECTION MARKERS --------------------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
# defines a projection marker that forces a query to return *no* +_source+ fields at all.
|
|
80
|
+
# metadata fields ('_id', '_score', ...) are not part of the +_source+ - they are always
|
|
81
|
+
# returned on the document level and therefore stay accessible.
|
|
82
|
+
#
|
|
83
|
+
# this is the only way to clear the columns that +visit_Arel_Nodes_SelectCore+ claims for
|
|
84
|
+
# every relation - a +configure+ can only reach the query-body, never the columns.
|
|
85
|
+
#
|
|
86
|
+
# HINT: only evaluated as the *first* projection - combining it with other fields
|
|
87
|
+
# (e.g. +select(COLUMNS_NONE, :name)+) silently discards them.
|
|
88
|
+
#
|
|
89
|
+
# see @ ElasticsearchRecord::Relation::ResultMethods#meta_only!
|
|
90
|
+
# see @ Arel::Visitors::ElasticsearchQuery#visit_Selects
|
|
91
|
+
COLUMNS_NONE = '!'
|
|
92
|
+
|
|
73
93
|
# defines the index the query should be executed on
|
|
74
94
|
# @!attribute String
|
|
75
95
|
attr_reader :index
|
|
@@ -101,14 +121,14 @@ module ElasticsearchRecord
|
|
|
101
121
|
attr_reader :columns
|
|
102
122
|
|
|
103
123
|
def initialize(index: nil, type: TYPE_UNDEFINED, status: STATUS_VALID, body: nil, refresh: nil, timeout: nil, arguments: {}, columns: [])
|
|
104
|
-
@index
|
|
105
|
-
@type
|
|
106
|
-
@status
|
|
107
|
-
@refresh
|
|
108
|
-
@timeout
|
|
109
|
-
@body
|
|
124
|
+
@index = index
|
|
125
|
+
@type = type
|
|
126
|
+
@status = status
|
|
127
|
+
@refresh = refresh
|
|
128
|
+
@timeout = timeout
|
|
129
|
+
@body = body
|
|
110
130
|
@arguments = arguments
|
|
111
|
-
@columns
|
|
131
|
+
@columns = columns
|
|
112
132
|
end
|
|
113
133
|
|
|
114
134
|
# sets the failed status for this query.
|
|
@@ -136,9 +156,9 @@ module ElasticsearchRecord
|
|
|
136
156
|
# returns the API gate to be called to execute the query.
|
|
137
157
|
# each query type needs a different endpoint.
|
|
138
158
|
# @see Elasticsearch::API
|
|
139
|
-
# @return [
|
|
159
|
+
# @return [Symbol, String] - API gate "<namespace>.<action>" | <:action>
|
|
140
160
|
def gate
|
|
141
|
-
|
|
161
|
+
GATES_MAP[self.type].presence || self.type
|
|
142
162
|
end
|
|
143
163
|
|
|
144
164
|
# returns true if this is a write query
|
|
@@ -161,13 +181,13 @@ module ElasticsearchRecord
|
|
|
161
181
|
# Also used possible PRE-defined arguments to be merged with those mentioned attributes.
|
|
162
182
|
# @return [Hash]
|
|
163
183
|
def query_arguments
|
|
164
|
-
args
|
|
184
|
+
args = @arguments.deep_dup
|
|
165
185
|
|
|
166
186
|
# set index, if present
|
|
167
|
-
args[:index]
|
|
187
|
+
args[:index] = self.index if self.index.present?
|
|
168
188
|
|
|
169
189
|
# set body, if present
|
|
170
|
-
args[:body]
|
|
190
|
+
args[:body] = self.body if self.body.present?
|
|
171
191
|
|
|
172
192
|
# set refresh, if defined (also includes false value)
|
|
173
193
|
args[:refresh] = self.refresh unless self.refresh.nil?
|
|
@@ -42,13 +42,14 @@ 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(
|
|
50
51
|
type: ElasticsearchRecord::Query::TYPE_SQL,
|
|
51
|
-
body: { query:
|
|
52
|
+
body: { query: sql },
|
|
52
53
|
# IMPORTANT: Always provide all columns
|
|
53
54
|
columns: source_column_names)
|
|
54
55
|
when Hash
|
|
@@ -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,14 +99,17 @@ 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
|
|
97
110
|
# Does NOT instantiate records.
|
|
98
111
|
# @param [String] esql
|
|
99
|
-
|
|
100
|
-
def esql(esql, async: false)
|
|
112
|
+
def esql(esql)
|
|
101
113
|
# build new query
|
|
102
114
|
query = ElasticsearchRecord::Query.new(
|
|
103
115
|
type: ElasticsearchRecord::Query::TYPE_ESQL,
|
|
@@ -105,15 +117,14 @@ module ElasticsearchRecord
|
|
|
105
117
|
# IMPORTANT: Always provide all columns
|
|
106
118
|
columns: source_column_names)
|
|
107
119
|
|
|
108
|
-
connection.
|
|
120
|
+
connection.exec_query(query, "#{name} ES|QL")
|
|
109
121
|
end
|
|
110
122
|
|
|
111
123
|
|
|
112
124
|
# executes a +msearch+ by provided *RAW* queries.
|
|
113
125
|
# Does NOT instantiate records.
|
|
114
126
|
# @param [Array<String>] queries
|
|
115
|
-
|
|
116
|
-
def msearch(queries, async: false)
|
|
127
|
+
def msearch(queries)
|
|
117
128
|
# build new msearch query
|
|
118
129
|
query = ElasticsearchRecord::Query.new(
|
|
119
130
|
index: table_name,
|
|
@@ -122,20 +133,31 @@ module ElasticsearchRecord
|
|
|
122
133
|
# IMPORTANT: Always provide all columns
|
|
123
134
|
columns: source_column_names)
|
|
124
135
|
|
|
125
|
-
connection.
|
|
136
|
+
connection.exec_query(query, "#{name} Msearch")
|
|
126
137
|
end
|
|
127
138
|
|
|
128
|
-
# 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.
|
|
129
145
|
def search(*args, &block)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
161
|
|
|
140
162
|
find_by_query(query)
|
|
141
163
|
end
|
|
@@ -8,6 +8,12 @@ module ElasticsearchRecord
|
|
|
8
8
|
#
|
|
9
9
|
# Person.all.count(:age)
|
|
10
10
|
# => returns the total count of all people whose age is present in database
|
|
11
|
+
#
|
|
12
|
+
# Person.all.limit(10).count
|
|
13
|
+
# => returns at most 10 - the SQL 'LIMIT n OFFSET m' semantic is applied on the resolved total
|
|
14
|
+
#
|
|
15
|
+
# @param [Symbol, String, nil] column_name
|
|
16
|
+
# @return [Integer, Hash]
|
|
11
17
|
def count(column_name = nil)
|
|
12
18
|
# fallback to default
|
|
13
19
|
return super() if block_given?
|
|
@@ -36,12 +42,12 @@ module ElasticsearchRecord
|
|
|
36
42
|
# HINT: +:__query__+ directly interacts with the query-object and sets the 'terminate_after' argument
|
|
37
43
|
# see @ ElasticsearchRecord::Query#arguments & Arel::Collectors::ElasticsearchQuery#assign
|
|
38
44
|
arel = spawn.unscope!(:offset, :limit, :order, :configure, :aggs).configure!(:__query__, argument: { terminate_after: limit_value }).arel
|
|
39
|
-
klass.connection.select_count(arel, "#{klass.name} Count")
|
|
45
|
+
_resolve_limited_count(klass.connection.select_count(arel, "#{klass.name} Count"))
|
|
40
46
|
else
|
|
41
47
|
# since total will be limited to 10000 results, we need to resolve the real values by a custom query.
|
|
42
48
|
# This query is called through +#select_count+.
|
|
43
49
|
arel = spawn.unscope!(:offset, :limit, :order, :configure, :aggs)
|
|
44
|
-
klass.connection.select_count(arel, "#{klass.name} Count")
|
|
50
|
+
_resolve_limited_count(klass.connection.select_count(arel, "#{klass.name} Count"))
|
|
45
51
|
end
|
|
46
52
|
end
|
|
47
53
|
|
|
@@ -129,9 +135,17 @@ module ElasticsearchRecord
|
|
|
129
135
|
#
|
|
130
136
|
# @note returns *nil* on a *NullRelation*
|
|
131
137
|
#
|
|
138
|
+
# PLEASE NOTE: the aggregation quantifies the relationship BETWEEN fields, so it requires at
|
|
139
|
+
# least two of them. A single column would additionally take the 'field'-branch of
|
|
140
|
+
# +#calculate_aggregation+ - but the metric only accepts a 'fields' node.
|
|
141
|
+
#
|
|
132
142
|
# @param [Array<Symbol|String>] column_names
|
|
143
|
+
# @raise [ArgumentError] if less than two columns were provided
|
|
133
144
|
# @return [Hash,nil]
|
|
134
145
|
def matrix_stats(*column_names)
|
|
146
|
+
# ensure minimum number of names are provided
|
|
147
|
+
raise(ArgumentError, "Unable to build a 'matrix_stats' aggregation with less than two columns (#{column_names.size} provided) @ #{klass.name}!") if column_names.size < 2
|
|
148
|
+
|
|
135
149
|
calculate_aggregation(:matrix_stats, *column_names)
|
|
136
150
|
end
|
|
137
151
|
|
|
@@ -316,6 +330,28 @@ module ElasticsearchRecord
|
|
|
316
330
|
end
|
|
317
331
|
|
|
318
332
|
alias_method :calculate, :calculate_aggregation
|
|
333
|
+
|
|
334
|
+
private
|
|
335
|
+
|
|
336
|
+
# applies the SQL +LIMIT n OFFSET m+ semantic onto an already resolved total.
|
|
337
|
+
#
|
|
338
|
+
# IMPORTANT: Elasticsearch always answers a count with the FULL total. The +terminate_after+
|
|
339
|
+
# argument (see @ +#count+) cannot provide this semantic on its own:
|
|
340
|
+
# - it limits the *collected* documents, but a count query collects none - so it never fires
|
|
341
|
+
# - and it acts PER SHARD, which would resolve +limit * shards+ on a multi-shard index
|
|
342
|
+
#
|
|
343
|
+
# Clamping the resolved total is exact either way: a per-shard early termination can only
|
|
344
|
+
# return a value between +min(limit, total)+ and +total+, so the minimum stays the same.
|
|
345
|
+
#
|
|
346
|
+
# @param [Integer] total - the resolved (full) total
|
|
347
|
+
# @return [Integer]
|
|
348
|
+
def _resolve_limited_count(total)
|
|
349
|
+
# the offset is subtracted first - it can never result in a negative count
|
|
350
|
+
total -= offset_value if offset_value
|
|
351
|
+
return 0 if total < 0
|
|
352
|
+
|
|
353
|
+
limit_value ? [total, limit_value].min : total
|
|
354
|
+
end
|
|
319
355
|
end
|
|
320
356
|
end
|
|
321
357
|
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!
|
|
File without changes
|
|
@@ -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
|