elasticsearch_record 1.8.2 → 2.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 +75 -9
- data/Rakefile +0 -0
- data/docs/CHANGELOG.md +46 -0
- data/docs/CODE_OF_CONDUCT.md +0 -0
- data/docs/LICENSE +0 -0
- data/elasticsearch_record.gemspec +1 -1
- data/lib/active_record/connection_adapters/elasticsearch/column.rb +0 -0
- data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +18 -17
- data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +0 -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/column_methods.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 +144 -87
- data/lib/arel/collectors/elasticsearch_query.rb +0 -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 +0 -0
- 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/model_api.rb +8 -5
- data/lib/elasticsearch_record/model_schema.rb +1 -1
- 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 +35 -18
- data/lib/elasticsearch_record/query.rb +53 -33
- data/lib/elasticsearch_record/querying.rb +5 -7
- data/lib/elasticsearch_record/relation/calculation_methods.rb +38 -2
- data/lib/elasticsearch_record/relation/core_methods.rb +0 -0
- data/lib/elasticsearch_record/relation/query_clause.rb +0 -0
- data/lib/elasticsearch_record/relation/query_clause_tree.rb +0 -0
- data/lib/elasticsearch_record/relation/query_methods.rb +30 -0
- data/lib/elasticsearch_record/relation/result_methods.rb +47 -43
- data/lib/elasticsearch_record/relation/value_methods.rb +0 -0
- data/lib/elasticsearch_record/result.rb +144 -60
- data/lib/elasticsearch_record/schema_migration.rb +0 -0
- 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 +15 -0
- metadata +17 -6
|
@@ -15,12 +15,12 @@ module ElasticsearchRecord
|
|
|
15
15
|
_insert_with_auto_increment(values) do |arguments|
|
|
16
16
|
# build new query
|
|
17
17
|
query = ElasticsearchRecord::Query.new(
|
|
18
|
-
index:
|
|
19
|
-
type:
|
|
18
|
+
index: table_name,
|
|
19
|
+
type: ElasticsearchRecord::Query::TYPE_CREATE,
|
|
20
20
|
# IMPORTANT: always exclude possible provided +_id+ field
|
|
21
|
-
body:
|
|
21
|
+
body: values.except('_id'),
|
|
22
22
|
arguments: arguments,
|
|
23
|
-
refresh:
|
|
23
|
+
refresh: true)
|
|
24
24
|
|
|
25
25
|
# execute query and return inserted id
|
|
26
26
|
connection.insert(query, "#{self} Create", returning: returning)
|
|
@@ -35,11 +35,11 @@ module ElasticsearchRecord
|
|
|
35
35
|
|
|
36
36
|
# build new query
|
|
37
37
|
query = ElasticsearchRecord::Query.new(
|
|
38
|
-
index:
|
|
39
|
-
type:
|
|
40
|
-
body:
|
|
38
|
+
index: table_name,
|
|
39
|
+
type: ElasticsearchRecord::Query::TYPE_UPDATE,
|
|
40
|
+
body: { doc: values },
|
|
41
41
|
arguments: { id: constraints[self.primary_key] },
|
|
42
|
-
refresh:
|
|
42
|
+
refresh: true)
|
|
43
43
|
|
|
44
44
|
# execute query and return total updates
|
|
45
45
|
connection.update(query, "#{self} Update")
|
|
@@ -50,10 +50,10 @@ module ElasticsearchRecord
|
|
|
50
50
|
def _delete_record(constraints)
|
|
51
51
|
# build new query
|
|
52
52
|
query = ElasticsearchRecord::Query.new(
|
|
53
|
-
index:
|
|
54
|
-
type:
|
|
53
|
+
index: table_name,
|
|
54
|
+
type: ElasticsearchRecord::Query::TYPE_DELETE,
|
|
55
55
|
arguments: { id: constraints[self.primary_key] },
|
|
56
|
-
refresh:
|
|
56
|
+
refresh: true)
|
|
57
57
|
|
|
58
58
|
# execute query and return total deletes
|
|
59
59
|
connection.delete(query, "#{self} Delete")
|
|
@@ -66,7 +66,7 @@ module ElasticsearchRecord
|
|
|
66
66
|
# check, if the primary_key's value is provided.
|
|
67
67
|
# so, no need to resolve a +auto_increment+ value, but provide the id directly
|
|
68
68
|
if (id = values[self.primary_key]).present?
|
|
69
|
-
yield({id: id})
|
|
69
|
+
yield({ id: id })
|
|
70
70
|
elsif auto_increment?
|
|
71
71
|
# future increments: uuid (+uuidv6 ?), hex, radix(2-36), integer
|
|
72
72
|
# allocated through: primary_key_type
|
|
@@ -75,17 +75,34 @@ module ElasticsearchRecord
|
|
|
75
75
|
# try to resolve the current-auto-increment value from the tables meta
|
|
76
76
|
connection.table_metas(self.table_name).dig('auto_increment').to_i + 1,
|
|
77
77
|
# for secure reasons: also resolve the current maximum value for the primary key
|
|
78
|
-
self.unscoped.
|
|
78
|
+
self.unscoped.maximum(self.primary_key).to_i + 1
|
|
79
79
|
]
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
# yield and resolve the new inserted *result*
|
|
82
|
+
result = yield({ id: ids.max })
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
# IMPORTANT: the block returns whatever +connection.insert+ resolved, which is NOT a plain id:
|
|
85
|
+
# ActiveRecord always provides the primary_key as +returning+ column, so an ARRAY of the
|
|
86
|
+
# returning column values is resolved (see @ ActiveRecord::Persistence#_create_record).
|
|
87
|
+
# The +_meta+ must be updated with the PLAIN id - storing the raw result breaks the
|
|
88
|
+
# +.to_i+ of the NEXT insert (which then only resolves a 0 - or fails altogether).
|
|
89
|
+
id = case result
|
|
90
|
+
when Array
|
|
91
|
+
result.first
|
|
92
|
+
when Hash
|
|
93
|
+
result[:id] || result['id']
|
|
94
|
+
else
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if id.present? && id.to_i > 0
|
|
99
|
+
# IMPORTANT: elasticsearch resolves a document +_id+ as String - the +_meta+ must keep the
|
|
100
|
+
# INTEGER it was created with, so the schema does not change its type behind the first insert.
|
|
101
|
+
connection.change_meta(self.table_name, :auto_increment, id.to_i)
|
|
85
102
|
end
|
|
86
103
|
|
|
87
|
-
# return
|
|
88
|
-
|
|
104
|
+
# return the UNCHANGED insert result - it is zipped against the +returning+ columns
|
|
105
|
+
result
|
|
89
106
|
else
|
|
90
107
|
yield({})
|
|
91
108
|
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?
|
|
@@ -48,7 +48,7 @@ module ElasticsearchRecord
|
|
|
48
48
|
when String # really find by SQL
|
|
49
49
|
ElasticsearchRecord::Query.new(
|
|
50
50
|
type: ElasticsearchRecord::Query::TYPE_SQL,
|
|
51
|
-
body: { query:
|
|
51
|
+
body: { query: sql },
|
|
52
52
|
# IMPORTANT: Always provide all columns
|
|
53
53
|
columns: source_column_names)
|
|
54
54
|
when Hash
|
|
@@ -96,8 +96,7 @@ module ElasticsearchRecord
|
|
|
96
96
|
# executes a +esql+ by provided *ES|SL* query
|
|
97
97
|
# Does NOT instantiate records.
|
|
98
98
|
# @param [String] esql
|
|
99
|
-
|
|
100
|
-
def esql(esql, async: false)
|
|
99
|
+
def esql(esql)
|
|
101
100
|
# build new query
|
|
102
101
|
query = ElasticsearchRecord::Query.new(
|
|
103
102
|
type: ElasticsearchRecord::Query::TYPE_ESQL,
|
|
@@ -105,15 +104,14 @@ module ElasticsearchRecord
|
|
|
105
104
|
# IMPORTANT: Always provide all columns
|
|
106
105
|
columns: source_column_names)
|
|
107
106
|
|
|
108
|
-
connection.
|
|
107
|
+
connection.exec_query(query, "#{name} ES|QL")
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
|
|
112
111
|
# executes a +msearch+ by provided *RAW* queries.
|
|
113
112
|
# Does NOT instantiate records.
|
|
114
113
|
# @param [Array<String>] queries
|
|
115
|
-
|
|
116
|
-
def msearch(queries, async: false)
|
|
114
|
+
def msearch(queries)
|
|
117
115
|
# build new msearch query
|
|
118
116
|
query = ElasticsearchRecord::Query.new(
|
|
119
117
|
index: table_name,
|
|
@@ -122,7 +120,7 @@ module ElasticsearchRecord
|
|
|
122
120
|
# IMPORTANT: Always provide all columns
|
|
123
121
|
columns: source_column_names)
|
|
124
122
|
|
|
125
|
-
connection.
|
|
123
|
+
connection.exec_query(query, "#{name} Msearch")
|
|
126
124
|
end
|
|
127
125
|
|
|
128
126
|
# executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -50,6 +50,7 @@ module ElasticsearchRecord
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# same like +#configure!+, but on the same relation (no spawn)
|
|
53
|
+
# @return [self]
|
|
53
54
|
def configure!(*args)
|
|
54
55
|
check_if_method_has_arguments!(__callee__, args)
|
|
55
56
|
|
|
@@ -274,6 +275,27 @@ module ElasticsearchRecord
|
|
|
274
275
|
self
|
|
275
276
|
end
|
|
276
277
|
|
|
278
|
+
# overwrite to prevent metadata fields within the projection.
|
|
279
|
+
# Metadata fields (like '_id' or '_score') are NOT part of the +_source+ node, so they cannot be
|
|
280
|
+
# resolved through the +_source+-filter this method builds - providing them would silently create
|
|
281
|
+
# a filter that never matches.
|
|
282
|
+
# HINT: This is different to the +pluck+-method which allows to resolve meta keys directly.
|
|
283
|
+
# see @ Arel::Visitors::ElasticsearchQuery#visit_Selects
|
|
284
|
+
# @param [Array] fields
|
|
285
|
+
def select(*fields)
|
|
286
|
+
# IMPORTANT: +select+ can also be called with a block (and without any fields) - in this case
|
|
287
|
+
# ActiveRecord directly forwards to +super()+, so we must not interfere here.
|
|
288
|
+
if fields.any? && (invalid = _invalid_projection_fields(fields)).present?
|
|
289
|
+
raise(ActiveRecord::UnknownAttributeReference,
|
|
290
|
+
"Unable to select metadata attributes: #{invalid.map(&:inspect).join(", ")}. " \
|
|
291
|
+
"Metadata fields are not part of the '_source' node but are always returned and accessible within the record. " \
|
|
292
|
+
"(e.g. #{klass.name}.first.#{invalid.first})."
|
|
293
|
+
)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
super
|
|
297
|
+
end
|
|
298
|
+
|
|
277
299
|
private
|
|
278
300
|
|
|
279
301
|
def build_where_clause(opts, _rest = [])
|
|
@@ -342,6 +364,14 @@ module ElasticsearchRecord
|
|
|
342
364
|
|
|
343
365
|
arel
|
|
344
366
|
end
|
|
367
|
+
|
|
368
|
+
# returns any provided field that is a metadata field and therefore not resolvable
|
|
369
|
+
# through a projection.
|
|
370
|
+
# @param [Array] fields
|
|
371
|
+
# @return [Array<String>]
|
|
372
|
+
def _invalid_projection_fields(fields)
|
|
373
|
+
ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys & fields.flatten.select{|fld| fld.is_a?(String) || fld.is_a?(Symbol)}.map(&:to_s)
|
|
374
|
+
end
|
|
345
375
|
end
|
|
346
376
|
end
|
|
347
377
|
end
|
|
@@ -66,7 +66,7 @@ module ElasticsearchRecord
|
|
|
66
66
|
# @return [nil, String] - either returns the pit_id (no block given) or nil
|
|
67
67
|
def point_in_time(keep_alive: '1m')
|
|
68
68
|
# resolve a initial PIT id
|
|
69
|
-
initial_pit_id = klass.connection.api(:
|
|
69
|
+
initial_pit_id = klass.connection.api(:open_point_in_time, { index: klass.table_name, keep_alive: keep_alive }, "#{klass} Open Pit").dig('id')
|
|
70
70
|
|
|
71
71
|
return initial_pit_id unless block_given?
|
|
72
72
|
|
|
@@ -74,7 +74,7 @@ module ElasticsearchRecord
|
|
|
74
74
|
yield initial_pit_id
|
|
75
75
|
|
|
76
76
|
# close PIT
|
|
77
|
-
klass.connection.api(:
|
|
77
|
+
klass.connection.api(:close_point_in_time, { body: { id: initial_pit_id } }, "#{klass} Close Pit")
|
|
78
78
|
|
|
79
79
|
# return nil if everything was ok
|
|
80
80
|
nil
|
|
@@ -90,13 +90,12 @@ module ElasticsearchRecord
|
|
|
90
90
|
#
|
|
91
91
|
# @param [String] keep_alive - how long to keep alive (for each single request) - default: '1m'
|
|
92
92
|
# @param [Integer] batch_size - how many results per query (default: 1000 - this means at least 10 queries before reaching the +max_result_window+)
|
|
93
|
-
# @param [Boolean] ids_only - resolve ids only from results
|
|
94
93
|
# @return [Integer, Array] either returns the results-array (no block provided) or the total amount of results
|
|
95
|
-
def pit_results(keep_alive: '1m', batch_size: 1000
|
|
96
|
-
raise(ArgumentError, "Batch size cannot be above the 'max_result_window' (#{klass.max_result_window}) !") if batch_size > klass.max_result_window
|
|
94
|
+
def pit_results(keep_alive: '1m', batch_size: 1000)
|
|
95
|
+
raise(ArgumentError, "Batch size cannot be above the 'max_result_window' (#{batch_size} > #{klass.max_result_window}) !") if batch_size > klass.max_result_window
|
|
97
96
|
|
|
98
97
|
# check if limit or offset values where provided
|
|
99
|
-
results_limit
|
|
98
|
+
results_limit = limit_value ? limit_value : Float::INFINITY
|
|
100
99
|
results_offset = offset_value ? offset_value : 0
|
|
101
100
|
|
|
102
101
|
# search_after requires a order - we resolve a order either from provided value or by default ...
|
|
@@ -107,9 +106,6 @@ module ElasticsearchRecord
|
|
|
107
106
|
# see @ https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
|
|
108
107
|
relation.order!(_shard_doc: :asc) if relation.order_values.empty? && klass.connection.access_shard_doc?
|
|
109
108
|
|
|
110
|
-
# resolve ids only
|
|
111
|
-
relation.reselect!('_id') if ids_only
|
|
112
|
-
|
|
113
109
|
# clear limit & offset
|
|
114
110
|
relation.offset!(nil).limit!(nil)
|
|
115
111
|
|
|
@@ -117,47 +113,47 @@ module ElasticsearchRecord
|
|
|
117
113
|
relation.configure!(:__query__, { index: nil })
|
|
118
114
|
|
|
119
115
|
# we store the results in this array
|
|
120
|
-
results
|
|
116
|
+
results = []
|
|
121
117
|
results_total = 0
|
|
122
118
|
|
|
123
119
|
# resolve a new pit and auto-close after we finished
|
|
124
120
|
point_in_time(keep_alive: keep_alive) do |pit_id|
|
|
121
|
+
# set the initial pit hash, used to configure the ES query
|
|
125
122
|
current_pit_hash = { pit: { id: pit_id, keep_alive: keep_alive } }
|
|
126
123
|
|
|
127
124
|
# resolve new data until we got all we need
|
|
128
125
|
loop do
|
|
129
126
|
# change pit settings & limit (spawn is required, since a +resolve+ will make the relation immutable)
|
|
130
|
-
|
|
127
|
+
# @type [ElasticsearchRecord::Result]
|
|
128
|
+
current_result = relation.spawn.configure!(current_pit_hash).limit!(batch_size).resolve('Pit Results')
|
|
131
129
|
|
|
132
|
-
# resolve
|
|
133
|
-
current_results
|
|
134
|
-
current_response['hits']['hits'].map { |result| result['_id'] }
|
|
135
|
-
else
|
|
136
|
-
current_response['hits']['hits'].map { |result| result['_source'].merge('_id' => result['_id']) }
|
|
137
|
-
end
|
|
130
|
+
# resolve all results, depending on the existing query (select, ...)
|
|
131
|
+
current_results = current_result.to_ary
|
|
138
132
|
|
|
133
|
+
# temporary store the absolute length - used for pagination or stop
|
|
139
134
|
current_results_length = current_results.length
|
|
140
135
|
|
|
141
136
|
# check if we reached the required offset
|
|
142
137
|
if results_offset < current_results_length
|
|
143
138
|
# check for parts
|
|
144
|
-
# (maybe
|
|
139
|
+
# (maybe an offset of 6300 was provided but the batch size is 1000 - so we need to skip a part ...)
|
|
145
140
|
results_from = results_offset > 0 ? results_offset : 0
|
|
146
|
-
results_to
|
|
141
|
+
results_to = (results_total + current_results_length - results_from) > results_limit ? results_limit - results_total + results_from - 1 : -1
|
|
147
142
|
|
|
148
|
-
|
|
143
|
+
# reduce the *current_results* by calculated +from..to+ range
|
|
144
|
+
current_results = current_results[results_from..results_to] if results_from != 0 || results_to != -1
|
|
149
145
|
|
|
150
146
|
if block_given?
|
|
151
|
-
yield
|
|
147
|
+
yield current_results
|
|
152
148
|
else
|
|
153
|
-
results +=
|
|
149
|
+
results += current_results
|
|
154
150
|
end
|
|
155
151
|
|
|
156
152
|
# add to total
|
|
157
|
-
results_total +=
|
|
153
|
+
results_total += current_results.length
|
|
158
154
|
end
|
|
159
155
|
|
|
160
|
-
#
|
|
156
|
+
# -- BREAK conditions --------------------------------------------------------------------------------------
|
|
161
157
|
|
|
162
158
|
# we reached our maximum value
|
|
163
159
|
break if results_total >= results_limit
|
|
@@ -166,22 +162,24 @@ module ElasticsearchRecord
|
|
|
166
162
|
break if current_results_length < batch_size
|
|
167
163
|
|
|
168
164
|
# additional security - prevents infinite loops
|
|
169
|
-
|
|
165
|
+
if current_pit_hash[:search_after] == current_result.response['hits']['hits'][-1]['sort'] && current_pit_hash[:pit][:id] == current_result.response['pit_id']
|
|
166
|
+
raise(::ActiveRecord::StatementInvalid, "'pit_results' aborted due an infinite loop error (invalid or missing order)")
|
|
167
|
+
end
|
|
170
168
|
|
|
171
|
-
#
|
|
169
|
+
# -- NEXT LOOP changes -------------------------------------------------------------------------------------
|
|
172
170
|
|
|
173
171
|
# reduce the offset
|
|
174
172
|
results_offset -= current_results_length
|
|
175
173
|
|
|
176
174
|
# assign new pit
|
|
177
|
-
current_pit_hash = { search_after:
|
|
175
|
+
current_pit_hash = { search_after: current_result.response['hits']['hits'][-1]['sort'], pit: { id: current_result.response['pit_id'], keep_alive: keep_alive } }
|
|
178
176
|
|
|
179
|
-
# we need to justify the +batch_size+ if the query
|
|
180
|
-
batch_size
|
|
177
|
+
# we need to justify the +batch_size+ if the query reaches over the limit
|
|
178
|
+
batch_size = results_limit - results_total if results_offset < batch_size && (results_total + batch_size) > results_limit
|
|
181
179
|
end
|
|
182
180
|
end
|
|
183
181
|
|
|
184
|
-
#
|
|
182
|
+
# returns either to total number of +pit+ results or an array of all collected results
|
|
185
183
|
if block_given?
|
|
186
184
|
results_total
|
|
187
185
|
else
|
|
@@ -193,17 +191,19 @@ module ElasticsearchRecord
|
|
|
193
191
|
|
|
194
192
|
# executes a delete query in a +point_in_time+ scope.
|
|
195
193
|
# this will provide the possibility to delete more than the +max_result_window+ (default: 10000) docs in a batched process.
|
|
196
|
-
# @param [String] keep_alive
|
|
197
|
-
# @param [Integer] batch_size
|
|
198
|
-
# @param [Boolean] refresh index after delete finished (default: true)
|
|
194
|
+
# @param [String] keep_alive - defines the keep alive time per +pit+ (not in total) - should be relative to *batch_size*
|
|
195
|
+
# @param [Integer] batch_size - the size of entries to delete per +pit+
|
|
196
|
+
# @param [Boolean] refresh - auto-refresh index after delete finished (default: true)
|
|
199
197
|
# @return [Integer] total amount of deleted docs
|
|
200
|
-
def pit_delete(keep_alive: '1m', batch_size:
|
|
201
|
-
|
|
198
|
+
def pit_delete(keep_alive: '1m', batch_size: 1_000, refresh: true)
|
|
199
|
+
# spawns a new query with disabled results (so only ids will be resolved)
|
|
200
|
+
delete_count = spawn.meta_only!.pit_results(keep_alive: keep_alive, batch_size: batch_size) do |results|
|
|
202
201
|
# skip empty results
|
|
203
|
-
next unless
|
|
202
|
+
next unless results.any?
|
|
204
203
|
|
|
205
|
-
# delete all IDs
|
|
206
|
-
|
|
204
|
+
# delete all IDs through +API+
|
|
205
|
+
# does not refresh index at this point (this is done below, if not disabled)
|
|
206
|
+
klass.connection.api(:bulk, { index: klass.table_name, body: results.map { |result| { delete: { _id: result['_id'] } } }, refresh: false }, "#{klass} Pit Delete")
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
# refresh index
|
|
@@ -249,24 +249,28 @@ module ElasticsearchRecord
|
|
|
249
249
|
end
|
|
250
250
|
|
|
251
251
|
# sets query as "hits"-only query (drops the aggs from the query)
|
|
252
|
+
# @return [self]
|
|
252
253
|
def hits_only!
|
|
253
254
|
configure!({ aggs: nil })
|
|
254
|
-
|
|
255
|
-
self
|
|
256
255
|
end
|
|
257
256
|
|
|
258
257
|
# sets query as "aggs"-only query (drops the size & sort options - so no hits will return)
|
|
258
|
+
# @return [self]
|
|
259
259
|
def aggs_only!
|
|
260
260
|
configure!({ size: 0, from: nil, sort: nil, _source: false })
|
|
261
|
-
|
|
262
|
-
self
|
|
263
261
|
end
|
|
264
262
|
|
|
265
263
|
# sets query as "total"-only query (drops the size, sort & aggs options - so no hits & aggs will be returned)
|
|
264
|
+
# @return [self]
|
|
266
265
|
def total_only!
|
|
267
266
|
configure!({ size: 0, from: nil, aggs: nil, sort: nil, _source: false })
|
|
267
|
+
end
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
# sets query as "meta"-only query (drops aggs and source).
|
|
270
|
+
# This is used to prevent resolving documents from the index and only returns "meta" information (like _id, _score, _type, ...)
|
|
271
|
+
# @return [self]
|
|
272
|
+
def meta_only!
|
|
273
|
+
select(::ElasticsearchRecord::Query::COLUMNS_NONE).configure!({ aggs: nil, _source: false })
|
|
270
274
|
end
|
|
271
275
|
end
|
|
272
276
|
end
|
|
File without changes
|