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
|
@@ -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
|
|
|
@@ -259,7 +260,7 @@ module ElasticsearchRecord
|
|
|
259
260
|
unless _valid_unscoping_values.include?(scope)
|
|
260
261
|
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{_valid_unscoping_values.to_a.join(", :")}."
|
|
261
262
|
end
|
|
262
|
-
|
|
263
|
+
assert_modifiable!
|
|
263
264
|
@values.delete(scope)
|
|
264
265
|
when Hash
|
|
265
266
|
scope.each do |key, target_value|
|
|
@@ -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
|
|
@@ -8,7 +8,7 @@ module ElasticsearchRecord
|
|
|
8
8
|
|
|
9
9
|
def kind_value=(value)
|
|
10
10
|
# checks if records are already loaded - in this case we cannot mutate the query anymore
|
|
11
|
-
|
|
11
|
+
assert_modifiable!
|
|
12
12
|
|
|
13
13
|
@values[:kind] = value.to_sym
|
|
14
14
|
end
|
|
@@ -18,7 +18,7 @@ module ElasticsearchRecord
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def configure_value=(value)
|
|
21
|
-
|
|
21
|
+
assert_modifiable!
|
|
22
22
|
|
|
23
23
|
@values[:configure] = value
|
|
24
24
|
end
|
|
@@ -28,7 +28,7 @@ module ElasticsearchRecord
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def query_clause=(value)
|
|
31
|
-
|
|
31
|
+
assert_modifiable!
|
|
32
32
|
|
|
33
33
|
@values[:query] = value
|
|
34
34
|
end
|
|
@@ -38,7 +38,7 @@ module ElasticsearchRecord
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def aggs_clause=(value)
|
|
41
|
-
|
|
41
|
+
assert_modifiable!
|
|
42
42
|
|
|
43
43
|
@values[:aggs] = value
|
|
44
44
|
end
|
|
@@ -7,8 +7,8 @@ module ElasticsearchRecord
|
|
|
7
7
|
include Enumerable
|
|
8
8
|
|
|
9
9
|
# creates an empty response
|
|
10
|
-
# @return [ElasticsearchRecord::Result (frozen)]
|
|
11
|
-
def self.empty(async: false)
|
|
10
|
+
# @return [ElasticsearchRecord::Result (frozen), ActiveRecord::FutureResult::Complete (frozen)]
|
|
11
|
+
def self.empty(async: false)
|
|
12
12
|
if async
|
|
13
13
|
EMPTY_ASYNC
|
|
14
14
|
else
|
|
@@ -23,7 +23,7 @@ module ElasticsearchRecord
|
|
|
23
23
|
# @param [Array] columns
|
|
24
24
|
# @param [Hash] column_types
|
|
25
25
|
def initialize(response, columns = [], column_types = {})
|
|
26
|
-
# contains either the response or creates
|
|
26
|
+
# contains either the response or creates an empty hash (if nil)
|
|
27
27
|
@response = response.presence || {}
|
|
28
28
|
|
|
29
29
|
# used to build computed_results
|
|
@@ -39,37 +39,69 @@ module ElasticsearchRecord
|
|
|
39
39
|
response['took']
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# returns the response result string
|
|
43
|
+
# @return [String]
|
|
44
|
+
def result
|
|
45
|
+
response['result'] || ''
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
# returns the response total value.
|
|
43
49
|
# either chops the +total+ value directly from response, from hits or aggregations.
|
|
44
50
|
# @return [Integer]
|
|
45
51
|
def total
|
|
46
|
-
# chop total
|
|
47
|
-
@total ||=
|
|
52
|
+
# chop total from response and not from the generated data
|
|
53
|
+
@total ||= _total
|
|
48
54
|
end
|
|
49
55
|
|
|
50
|
-
#
|
|
51
|
-
# PLEASE NOTE: Does not return the nested hits (+response['hits']['hits']+) array!
|
|
52
|
-
# @return [ActiveSupport::HashWithIndifferentAccess, Hash]
|
|
53
|
-
def hits
|
|
54
|
-
response.key?('hits') ? response['hits'].with_indifferent_access : {}
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Returns the RAW +_source+ data from each hit - aka. +rows+.
|
|
56
|
+
# Returns the RAW +_source+ data from each hit.
|
|
58
57
|
# PLEASE NOTE: The array will only contain the RAW data from each +_source+ (meta info like '_id' or '_score' are not included)
|
|
59
58
|
# @return [Array]
|
|
60
59
|
def results
|
|
61
|
-
|
|
60
|
+
# IMPORTANT: check against missing hits without any '_source' node.
|
|
61
|
+
# This happens if the Elasticsearch query has the +_source:false+ flag!
|
|
62
|
+
if response['hits']
|
|
63
|
+
response['hits']['hits'].map { |doc| doc['_source'] || {} }
|
|
64
|
+
elsif _tabular?
|
|
65
|
+
# a tabular (+SQL+ / +ES|QL+) response has no '_source' node - the row values are the data
|
|
66
|
+
_results_from_tabular
|
|
67
|
+
else
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
62
71
|
|
|
63
|
-
|
|
72
|
+
# returns an array of all rows.
|
|
73
|
+
# => All result values, depending on the provided columns.
|
|
74
|
+
# The +rows+ is used by the ActiveRecord ConnectionAdapters and must not be removed!
|
|
75
|
+
# @return [Array]
|
|
76
|
+
def rows
|
|
77
|
+
# a tabular (+SQL+ / +ES|QL+) response is ALREADY positional - and it is positional to the
|
|
78
|
+
# response's own columns, not to the (requested) +columns+ of the query.
|
|
79
|
+
return _tabular_values if _tabular?
|
|
80
|
+
|
|
81
|
+
# IMPORTANT: without provided +columns+ we cannot build positional rows - mapping over an
|
|
82
|
+
# empty +columns+ array would return an empty array per hit and silently lose all data.
|
|
83
|
+
# In this case we fall back to the raw +_source+ values.
|
|
84
|
+
return results.map(&:values) if columns.blank?
|
|
85
|
+
|
|
86
|
+
results.map { |doc|
|
|
87
|
+
columns.map { |column|
|
|
88
|
+
doc[column]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
64
91
|
end
|
|
65
92
|
|
|
66
|
-
#
|
|
67
|
-
|
|
93
|
+
# returns the response RAW hits hash.
|
|
94
|
+
# PLEASE NOTE: Does not return the nested hits (+response['hits']['hits']+) array!
|
|
95
|
+
#
|
|
96
|
+
# @return [ActiveSupport::HashWithIndifferentAccess, Hash]
|
|
97
|
+
def hits
|
|
98
|
+
response['hits']&.with_indifferent_access || {}
|
|
99
|
+
end
|
|
68
100
|
|
|
69
101
|
# returns the response RAW aggregations hash.
|
|
70
102
|
# @return [ActiveSupport::HashWithIndifferentAccess, Hash]
|
|
71
103
|
def aggregations
|
|
72
|
-
response
|
|
104
|
+
response['aggregations']&.with_indifferent_access || {}
|
|
73
105
|
end
|
|
74
106
|
|
|
75
107
|
# returns the (nested) bucket values (and aggregated values) from the response aggregations.
|
|
@@ -84,13 +116,14 @@ module ElasticsearchRecord
|
|
|
84
116
|
end
|
|
85
117
|
|
|
86
118
|
# Returns true if this result set includes the column named +name+.
|
|
87
|
-
# used by ActiveRecord
|
|
119
|
+
# used by +ActiveRecord+
|
|
88
120
|
def includes_column?(name)
|
|
89
121
|
@columns&.include?(name)
|
|
90
122
|
end
|
|
91
123
|
|
|
92
124
|
# Returns the number of elements in the response array.
|
|
93
|
-
# Either uses the +hits+ length
|
|
125
|
+
# Either uses the +hits+ length, the +responses+ length _(msearch)_ or the length of the
|
|
126
|
+
# tabular value rows _(SQL / ES|QL)_.
|
|
94
127
|
# @return [Integer]
|
|
95
128
|
def length
|
|
96
129
|
if response.key?('hits')
|
|
@@ -98,6 +131,9 @@ module ElasticsearchRecord
|
|
|
98
131
|
elsif response.key?('responses')
|
|
99
132
|
# used by +msearch+
|
|
100
133
|
response['responses'].length
|
|
134
|
+
elsif _tabular?
|
|
135
|
+
# used by +sql+ & +esql+
|
|
136
|
+
_tabular_values.length
|
|
101
137
|
else
|
|
102
138
|
0
|
|
103
139
|
end
|
|
@@ -131,51 +167,56 @@ module ElasticsearchRecord
|
|
|
131
167
|
computed_results[idx]
|
|
132
168
|
end
|
|
133
169
|
|
|
134
|
-
# Returns the last record from the
|
|
170
|
+
# Returns the last record(s) from the *computed_results* collection.
|
|
135
171
|
def last(n = nil)
|
|
136
172
|
n ? computed_results.last(n) : computed_results.last
|
|
137
173
|
end
|
|
138
174
|
|
|
139
|
-
# returns the response result string
|
|
140
|
-
# @return [String]
|
|
141
|
-
def result
|
|
142
|
-
response['result'] || ''
|
|
143
|
-
end
|
|
144
|
-
|
|
145
175
|
# used by ActiveRecord
|
|
146
176
|
def cancel # :nodoc:
|
|
147
177
|
self
|
|
148
178
|
end
|
|
149
179
|
|
|
150
|
-
# used by ActiveRecord
|
|
180
|
+
# used by ActiveRecord for "pluck"
|
|
151
181
|
def cast_values(type_overrides = {})
|
|
152
|
-
#
|
|
182
|
+
# fast escape, if no hits are available
|
|
183
|
+
return [] unless response['hits']
|
|
184
|
+
|
|
185
|
+
# HINT: This is separated to avoid allocating a (nested) array per row
|
|
153
186
|
if columns.one?
|
|
154
|
-
#
|
|
187
|
+
# resolve the column key
|
|
155
188
|
key = columns.first
|
|
156
189
|
|
|
157
|
-
type
|
|
158
|
-
|
|
159
|
-
else
|
|
160
|
-
column_type(columns.first, type_overrides)
|
|
161
|
-
end
|
|
190
|
+
# resolve type from overrides or +#column_type+ method
|
|
191
|
+
type = type_overrides.is_a?(Array) ? type_overrides.first : column_type(key, type_overrides)
|
|
162
192
|
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
# EDGE-case for metadata fields
|
|
194
|
+
if ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys.include?(key)
|
|
195
|
+
# directly read from doc
|
|
196
|
+
response['hits']['hits'].map { |doc| type.deserialize(doc[key]) }
|
|
197
|
+
else
|
|
198
|
+
results.map do |result|
|
|
199
|
+
type.deserialize(result[key])
|
|
200
|
+
end
|
|
165
201
|
end
|
|
166
202
|
else
|
|
167
|
-
types
|
|
168
|
-
|
|
169
|
-
else
|
|
170
|
-
columns.map { |name| column_type(name, type_overrides) }
|
|
171
|
-
end
|
|
203
|
+
# resolve types from overrides or +#column_type+ method
|
|
204
|
+
types = type_overrides.is_a?(Array) ? type_overrides : columns.map { |name| column_type(name, type_overrides) }
|
|
172
205
|
|
|
173
206
|
size = types.size
|
|
174
207
|
|
|
175
|
-
|
|
208
|
+
# EDGE-case for metadata fields - they have to be resolved from the doc, so we merge them into the +_source+
|
|
209
|
+
rows = if (ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys & columns).any?
|
|
210
|
+
response['hits']['hits'].map { |doc|
|
|
211
|
+
(doc['_source'] || {}).merge(doc.slice(*ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys))
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
response['hits']['hits'].map { |doc| doc['_source'] || {} }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
rows.map do |result|
|
|
176
218
|
Array.new(size) { |i|
|
|
177
|
-
|
|
178
|
-
types[i].deserialize(result[key])
|
|
219
|
+
types[i].deserialize(result[columns[i]])
|
|
179
220
|
}
|
|
180
221
|
end
|
|
181
222
|
end
|
|
@@ -185,45 +226,84 @@ module ElasticsearchRecord
|
|
|
185
226
|
|
|
186
227
|
# used by ActiveRecord
|
|
187
228
|
def column_type(name, type_overrides = {})
|
|
188
|
-
type_overrides.fetch(name, Type.default_value)
|
|
229
|
+
type_overrides.fetch(name, ::ActiveRecord::Type.default_value)
|
|
189
230
|
end
|
|
190
231
|
|
|
191
|
-
#
|
|
232
|
+
# resolves total value from response
|
|
192
233
|
# @return [Integer]
|
|
193
|
-
def
|
|
234
|
+
def _total
|
|
194
235
|
return self.response['total'] if self.response.key?('total')
|
|
195
236
|
return self.response['hits']['total']['value'] if self.response.key?('hits')
|
|
196
237
|
return self.response['aggregations'].count if self.response.key?('aggregations')
|
|
197
|
-
|
|
238
|
+
# a tabular response has no total - the transferred rows are all there is
|
|
239
|
+
return _tabular_values.length if _tabular?
|
|
198
240
|
|
|
199
241
|
0
|
|
200
242
|
end
|
|
201
243
|
|
|
244
|
+
# true if the response is TABULAR - which is what the +sql+ & +esql+ APIs return instead of a
|
|
245
|
+
# (nested) 'hits' node: a flat 'columns' definition and positional value rows.
|
|
246
|
+
# @return [Boolean]
|
|
247
|
+
def _tabular?
|
|
248
|
+
response.key?('columns') && (response.key?('rows') || response.key?('values'))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# returns the column names of a tabular response.
|
|
252
|
+
# Both APIs describe their columns as a {'name' =>, 'type' =>} pair.
|
|
253
|
+
# @return [Array<String>]
|
|
254
|
+
def _tabular_columns
|
|
255
|
+
response['columns'].map { |column| column['name'] }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# returns the positional value rows of a tabular response.
|
|
259
|
+
# PLEASE NOTE: the +sql+ API names this node 'rows', the +esql+ API names it 'values'.
|
|
260
|
+
# @return [Array<Array>]
|
|
261
|
+
def _tabular_values
|
|
262
|
+
response['rows'] || response['values']
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# used for +sql+ & +esql+ results
|
|
266
|
+
# IMPORTANT: the rows are positional to the RESPONSE columns - not to the (requested) +columns+
|
|
267
|
+
# of the query. A projecting query (e.g. 'SELECT name FROM ...') returns fewer columns, so
|
|
268
|
+
# zipping against the query's columns would shift every value.
|
|
269
|
+
# @return [Array]
|
|
270
|
+
def _results_from_tabular
|
|
271
|
+
# We freeze the strings to prevent them getting duped when
|
|
272
|
+
# used as keys in ActiveRecord::Base's @attributes hash.
|
|
273
|
+
keys = _tabular_columns.map(&:-@)
|
|
274
|
+
|
|
275
|
+
_tabular_values.map { |row| keys.zip(row).to_h }
|
|
276
|
+
end
|
|
277
|
+
|
|
202
278
|
# used for +msearch+ results
|
|
203
279
|
# @return [Array]
|
|
204
|
-
def
|
|
280
|
+
def _results_from_responses
|
|
205
281
|
response['responses'].map { |response| self.class.new(response, self.columns, self.column_types) }
|
|
206
282
|
end
|
|
207
283
|
|
|
208
284
|
# used for +search+ results
|
|
209
285
|
# @return [Array]
|
|
210
|
-
def
|
|
286
|
+
def _results_from_hits
|
|
211
287
|
# PLEASE NOTE: the 'hits' response has multiple nodes: BASE nodes & the +_source+ node.
|
|
212
|
-
# The real data is within the source node, but we also want the
|
|
213
|
-
|
|
288
|
+
# The real data is within the source node, but we also want the METADATA nodes for possible score & type check
|
|
289
|
+
metadata_fields = ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys
|
|
214
290
|
|
|
215
291
|
# check for provided columns
|
|
216
292
|
if @columns.present?
|
|
217
293
|
# We freeze the strings to prevent them getting duped when
|
|
218
294
|
# used as keys in ActiveRecord::Base's @attributes hash.
|
|
219
|
-
#
|
|
220
|
-
columns =
|
|
295
|
+
# IMPORTANT: remove *metadata_fields* from possible provided columns ( &:-@ -> freeze strings )
|
|
296
|
+
columns = (@columns - metadata_fields).map(&:-@)
|
|
221
297
|
|
|
222
298
|
# this is the hashed result array
|
|
223
299
|
response['hits']['hits'].map { |doc|
|
|
224
|
-
result
|
|
300
|
+
# allocate new result hash with all known metadata keys ('_id', ...)
|
|
301
|
+
result = doc.slice(*metadata_fields)
|
|
302
|
+
|
|
303
|
+
# iterate through each requested column
|
|
225
304
|
columns.each do |column|
|
|
226
|
-
|
|
305
|
+
# in case no source was provided, it prevents an exception
|
|
306
|
+
result[column] = doc.dig('_source', column)
|
|
227
307
|
end
|
|
228
308
|
|
|
229
309
|
result
|
|
@@ -234,7 +314,8 @@ module ElasticsearchRecord
|
|
|
234
314
|
|
|
235
315
|
# this is the hashed result array
|
|
236
316
|
response['hits']['hits'].map { |doc|
|
|
237
|
-
|
|
317
|
+
# in case no source was provided, it prevents an exception
|
|
318
|
+
doc.slice(*metadata_fields).merge!(doc['_source'] || {})
|
|
238
319
|
}
|
|
239
320
|
end
|
|
240
321
|
end
|
|
@@ -270,10 +351,13 @@ module ElasticsearchRecord
|
|
|
270
351
|
# @return [Array]
|
|
271
352
|
def computed_results
|
|
272
353
|
@computed_results ||= if response.key?('hits')
|
|
273
|
-
|
|
354
|
+
_results_from_hits
|
|
274
355
|
elsif response.key?('responses')
|
|
275
356
|
# used by +msearch+
|
|
276
|
-
|
|
357
|
+
_results_from_responses
|
|
358
|
+
elsif _tabular?
|
|
359
|
+
# used by +sql+ & +esql+
|
|
360
|
+
_results_from_tabular
|
|
277
361
|
else
|
|
278
362
|
[]
|
|
279
363
|
end
|
|
@@ -14,9 +14,12 @@ module ElasticsearchRecord
|
|
|
14
14
|
sm = Arel::SelectManager.new(arel_table)
|
|
15
15
|
sm.project(arel_table[primary_key])
|
|
16
16
|
sm.order(arel_table[primary_key].asc)
|
|
17
|
-
sm.take(connection.max_result_window(table_name))
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
@pool.with_connection do |connection|
|
|
19
|
+
sm.take(connection.max_result_window(table_name))
|
|
20
|
+
|
|
21
|
+
connection.select_values(sm, "#{self.class} Load")
|
|
22
|
+
end
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|