elasticgraph-graphql 0.19.1.0 → 0.19.2.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/LICENSE.txt +1 -1
- data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/computation.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/key.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +2 -2
- data/lib/elastic_graph/graphql/aggregation/path_segment.rb +2 -2
- data/lib/elastic_graph/graphql/aggregation/query.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +33 -6
- data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +2 -6
- data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +26 -6
- data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +5 -6
- data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +10 -8
- data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +2 -2
- data/lib/elastic_graph/graphql/client.rb +1 -1
- data/lib/elastic_graph/graphql/config.rb +21 -6
- data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +10 -5
- data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +2 -3
- data/lib/elastic_graph/graphql/datastore_query/paginator.rb +1 -1
- data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +2 -3
- data/lib/elastic_graph/graphql/datastore_query.rb +66 -74
- data/lib/elastic_graph/graphql/datastore_response/document.rb +1 -1
- data/lib/elastic_graph/graphql/datastore_response/search_response.rb +83 -9
- data/lib/elastic_graph/graphql/datastore_search_router.rb +19 -4
- data/lib/elastic_graph/graphql/decoded_cursor.rb +1 -1
- data/lib/elastic_graph/graphql/filtering/boolean_query.rb +1 -1
- data/lib/elastic_graph/graphql/filtering/field_path.rb +1 -1
- data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +2 -2
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +10 -5
- data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +2 -2
- data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +17 -2
- data/lib/elastic_graph/graphql/filtering/range_query.rb +1 -1
- data/lib/elastic_graph/graphql/http_endpoint.rb +2 -2
- data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +1 -1
- data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +1 -1
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +1 -1
- data/lib/elastic_graph/graphql/query_adapter/pagination.rb +1 -1
- data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +18 -3
- data/lib/elastic_graph/graphql/query_adapter/sort.rb +1 -1
- data/lib/elastic_graph/graphql/query_details_tracker.rb +13 -4
- data/lib/elastic_graph/graphql/query_executor.rb +12 -5
- data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +6 -12
- data/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb +123 -0
- data/lib/elastic_graph/graphql/resolvers/list_records.rb +4 -4
- data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +57 -27
- data/lib/elastic_graph/graphql/resolvers/nested_relationships_source.rb +324 -0
- data/lib/elastic_graph/graphql/resolvers/object.rb +36 -0
- data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +2 -2
- data/lib/elastic_graph/graphql/resolvers/query_source.rb +6 -3
- data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +1 -1
- data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +1 -1
- data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +1 -1
- data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +1 -1
- data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +1 -1
- data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +2 -7
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +1 -1
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +1 -1
- data/lib/elastic_graph/graphql/schema/arguments.rb +1 -1
- data/lib/elastic_graph/graphql/schema/enum_value.rb +1 -1
- data/lib/elastic_graph/graphql/schema/field.rb +12 -27
- data/lib/elastic_graph/graphql/schema/relation_join.rb +17 -9
- data/lib/elastic_graph/graphql/schema/type.rb +15 -7
- data/lib/elastic_graph/graphql/schema.rb +11 -31
- data/lib/elastic_graph/graphql.rb +38 -40
- data/script/dump_time_zones +1 -1
- metadata +25 -27
- data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +0 -114
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -30,45 +30,8 @@ module ElasticGraph
|
|
30
30
|
:total_document_count_needed, :aggregations, :logger, :filter_interpreter, :routing_picker,
|
31
31
|
:index_expression_builder, :default_page_size, :search_index_definitions, :max_page_size,
|
32
32
|
:filters, :sort, :document_pagination, :requested_fields, :individual_docs_needed,
|
33
|
-
:monotonic_clock_deadline, :schema_element_names
|
34
|
-
)
|
35
|
-
def initialize(
|
36
|
-
filter: nil,
|
37
|
-
filters: nil,
|
38
|
-
sort: nil,
|
39
|
-
document_pagination: nil,
|
40
|
-
aggregations: nil,
|
41
|
-
requested_fields: nil,
|
42
|
-
individual_docs_needed: false,
|
43
|
-
total_document_count_needed: false,
|
44
|
-
monotonic_clock_deadline: nil,
|
45
|
-
**kwargs
|
46
|
-
)
|
47
|
-
# Deal with `:filter` vs `:filters` input and normalize it to a single `filters` set.
|
48
|
-
filters = ::Set.new(filters || [])
|
49
|
-
filters << filter if filter && !filter.empty?
|
50
|
-
filters.freeze
|
51
|
-
|
52
|
-
aggregations ||= {}
|
53
|
-
requested_fields ||= []
|
54
|
-
|
55
|
-
super(
|
56
|
-
filters: filters,
|
57
|
-
sort: sort || [],
|
58
|
-
document_pagination: document_pagination || {},
|
59
|
-
aggregations: aggregations,
|
60
|
-
requested_fields: requested_fields.to_set,
|
61
|
-
individual_docs_needed: individual_docs_needed || !requested_fields.empty?,
|
62
|
-
total_document_count_needed: total_document_count_needed || aggregations.values.any?(&:needs_total_doc_count?),
|
63
|
-
monotonic_clock_deadline: monotonic_clock_deadline,
|
64
|
-
**kwargs
|
65
|
-
)
|
66
|
-
|
67
|
-
if search_index_definitions.empty?
|
68
|
-
raise Errors::SearchFailedError, "Query is invalid, since it contains no `search_index_definitions`."
|
69
|
-
end
|
70
|
-
end
|
71
|
-
}
|
33
|
+
:size_multiplier, :monotonic_clock_deadline, :schema_element_names
|
34
|
+
)
|
72
35
|
# Load these files after the `Query` class has been defined, to avoid
|
73
36
|
# `TypeError: superclass mismatch for class Query`
|
74
37
|
require "elastic_graph/graphql/datastore_query/document_paginator"
|
@@ -129,34 +92,31 @@ module ElasticGraph
|
|
129
92
|
end
|
130
93
|
end
|
131
94
|
|
132
|
-
# Merges the provided
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
95
|
+
# Merges in the provided attribute overrides, honoring the intended semantics and invariants of `DatastoreQuery`.
|
96
|
+
def merge_with(
|
97
|
+
individual_docs_needed: false,
|
98
|
+
total_document_count_needed: false,
|
99
|
+
filters: [],
|
100
|
+
sort: [],
|
101
|
+
requested_fields: [],
|
102
|
+
document_pagination: {},
|
103
|
+
size_multiplier: 1,
|
104
|
+
monotonic_clock_deadline: nil,
|
105
|
+
aggregations: {}
|
106
|
+
)
|
140
107
|
with(
|
141
|
-
individual_docs_needed: individual_docs_needed ||
|
142
|
-
total_document_count_needed: total_document_count_needed ||
|
143
|
-
filters: filters +
|
144
|
-
sort: merge_attribute(
|
145
|
-
requested_fields: requested_fields +
|
146
|
-
document_pagination: merge_attribute(
|
147
|
-
|
148
|
-
|
108
|
+
individual_docs_needed: self.individual_docs_needed || individual_docs_needed || !requested_fields.empty?,
|
109
|
+
total_document_count_needed: self.total_document_count_needed || total_document_count_needed || aggregations.values.any?(&:needs_total_doc_count?),
|
110
|
+
filters: self.filters + filters,
|
111
|
+
sort: merge_attribute(:sort, sort),
|
112
|
+
requested_fields: self.requested_fields + requested_fields,
|
113
|
+
document_pagination: merge_attribute(:document_pagination, document_pagination),
|
114
|
+
size_multiplier: self.size_multiplier * size_multiplier,
|
115
|
+
monotonic_clock_deadline: [self.monotonic_clock_deadline, monotonic_clock_deadline].compact.min,
|
116
|
+
aggregations: self.aggregations.merge(aggregations)
|
149
117
|
)
|
150
118
|
end
|
151
119
|
|
152
|
-
# Convenience method for merging when you do not have access to an
|
153
|
-
# `DatastoreQuery::Builder`. Allows you to pass the query options you
|
154
|
-
# would like to merge. As with `#merge`, leaves the original query unchanged
|
155
|
-
# and returns a combined query object.
|
156
|
-
def merge_with(**query_options)
|
157
|
-
merge(with(**query_options))
|
158
|
-
end
|
159
|
-
|
160
120
|
# Pairs the multi-search headers and body into a tuple, as per the format required by the datastore:
|
161
121
|
# https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html#search-multi-search-api-desc
|
162
122
|
def to_datastore_msearch_header_and_body
|
@@ -176,6 +136,10 @@ module ElasticGraph
|
|
176
136
|
).to_s
|
177
137
|
end
|
178
138
|
|
139
|
+
def excluding_indices?
|
140
|
+
search_index_expression.split(",").any? { |expr| expr.start_with?("-") }
|
141
|
+
end
|
142
|
+
|
179
143
|
# Returns the name of the datastore cluster as a String where this query should be setn.
|
180
144
|
# Unless exactly 1 cluster name is found, this method raises a Errors::ConfigError.
|
181
145
|
def cluster_name
|
@@ -256,6 +220,8 @@ module ElasticGraph
|
|
256
220
|
total_document_count_needed: total_document_count_needed,
|
257
221
|
decoded_cursor_factory: decoded_cursor_factory,
|
258
222
|
schema_element_names: schema_element_names,
|
223
|
+
size_multiplier: size_multiplier,
|
224
|
+
max_effective_size: search_index_definitions.map { |i| i.max_result_window }.min,
|
259
225
|
paginator: Paginator.new(
|
260
226
|
default_page_size: default_page_size,
|
261
227
|
max_page_size: max_page_size,
|
@@ -268,11 +234,14 @@ module ElasticGraph
|
|
268
234
|
)
|
269
235
|
end
|
270
236
|
|
237
|
+
def effective_size
|
238
|
+
document_paginator.effective_size
|
239
|
+
end
|
240
|
+
|
271
241
|
private
|
272
242
|
|
273
|
-
def merge_attribute(
|
243
|
+
def merge_attribute(attribute, other_value)
|
274
244
|
value = public_send(attribute)
|
275
|
-
other_value = other_query.public_send(attribute)
|
276
245
|
|
277
246
|
if value.empty?
|
278
247
|
other_value
|
@@ -281,7 +250,7 @@ module ElasticGraph
|
|
281
250
|
elsif value == other_value
|
282
251
|
value
|
283
252
|
else
|
284
|
-
logger.warn("Tried to merge
|
253
|
+
logger.warn("Tried to merge conflicting values of `#{attribute}`; using the value from the merge override: #{value} (vs. #{other_value})")
|
285
254
|
other_value
|
286
255
|
end
|
287
256
|
end
|
@@ -344,11 +313,7 @@ module ElasticGraph
|
|
344
313
|
|
345
314
|
# Encapsulates dependencies of `Query`, giving us something we can expose off of `application`
|
346
315
|
# to build queries when desired.
|
347
|
-
class Builder < Support::MemoizableData.define(:runtime_metadata, :logger, :filter_node_interpreter, :
|
348
|
-
def self.with(runtime_metadata:, logger:, filter_node_interpreter:, **query_defaults)
|
349
|
-
new(runtime_metadata:, logger:, filter_node_interpreter:, query_defaults:)
|
350
|
-
end
|
351
|
-
|
316
|
+
class Builder < Support::MemoizableData.define(:runtime_metadata, :logger, :filter_interpreter, :filter_node_interpreter, :default_page_size, :max_page_size)
|
352
317
|
def routing_picker
|
353
318
|
@routing_picker ||= RoutingPicker.new(
|
354
319
|
filter_node_interpreter: filter_node_interpreter,
|
@@ -363,13 +328,40 @@ module ElasticGraph
|
|
363
328
|
)
|
364
329
|
end
|
365
330
|
|
366
|
-
def new_query(
|
331
|
+
def new_query(
|
332
|
+
search_index_definitions:,
|
333
|
+
filters: [],
|
334
|
+
sort: [],
|
335
|
+
document_pagination: {},
|
336
|
+
size_multiplier: 1,
|
337
|
+
aggregations: {},
|
338
|
+
requested_fields: [],
|
339
|
+
individual_docs_needed: false,
|
340
|
+
total_document_count_needed: false,
|
341
|
+
monotonic_clock_deadline: nil
|
342
|
+
)
|
343
|
+
if search_index_definitions.empty?
|
344
|
+
raise Errors::SearchFailedError, "Query is invalid, since it contains no `search_index_definitions`."
|
345
|
+
end
|
346
|
+
|
367
347
|
DatastoreQuery.new(
|
368
348
|
routing_picker: routing_picker,
|
369
349
|
index_expression_builder: index_expression_builder,
|
370
350
|
logger: logger,
|
371
351
|
schema_element_names: runtime_metadata.schema_element_names,
|
372
|
-
|
352
|
+
search_index_definitions: search_index_definitions,
|
353
|
+
filters: filters.to_set,
|
354
|
+
sort: sort,
|
355
|
+
document_pagination: document_pagination,
|
356
|
+
size_multiplier: size_multiplier,
|
357
|
+
aggregations: aggregations,
|
358
|
+
requested_fields: requested_fields.to_set,
|
359
|
+
individual_docs_needed: individual_docs_needed || !requested_fields.empty?,
|
360
|
+
total_document_count_needed: total_document_count_needed || aggregations.values.any?(&:needs_total_doc_count?),
|
361
|
+
monotonic_clock_deadline: monotonic_clock_deadline,
|
362
|
+
filter_interpreter: filter_interpreter,
|
363
|
+
default_page_size: default_page_size,
|
364
|
+
max_page_size: max_page_size
|
373
365
|
)
|
374
366
|
end
|
375
367
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -9,6 +9,7 @@
|
|
9
9
|
require "elastic_graph/errors"
|
10
10
|
require "elastic_graph/graphql/decoded_cursor"
|
11
11
|
require "elastic_graph/graphql/datastore_response/document"
|
12
|
+
require "elastic_graph/support/hash_util"
|
12
13
|
require "forwardable"
|
13
14
|
|
14
15
|
module ElasticGraph
|
@@ -17,15 +18,17 @@ module ElasticGraph
|
|
17
18
|
# Represents a search response from the datastore. Exposes both the raw metadata
|
18
19
|
# provided by the datastore and the collection of documents. Can be treated as a
|
19
20
|
# collection of documents when you don't care about the metadata.
|
20
|
-
class SearchResponse < ::Data.define(:raw_data, :metadata, :documents, :total_document_count)
|
21
|
+
class SearchResponse < ::Data.define(:raw_data, :metadata, :documents, :total_document_count, :aggregations_unavailable_reason, :decoded_cursor_factory)
|
21
22
|
include Enumerable
|
22
23
|
extend Forwardable
|
23
24
|
|
25
|
+
private :raw_data
|
26
|
+
|
24
27
|
def_delegators :documents, :each, :to_a, :size, :empty?
|
25
28
|
|
26
29
|
EXCLUDED_METADATA_KEYS = %w[hits aggregations].freeze
|
27
30
|
|
28
|
-
def self.build(raw_data, decoded_cursor_factory: DecodedCursor::Factory::Null)
|
31
|
+
def self.build(raw_data, decoded_cursor_factory: DecodedCursor::Factory::Null, aggregations_unavailable_reason: nil)
|
29
32
|
documents = raw_data.fetch("hits").fetch("hits").map do |doc|
|
30
33
|
Document.build(doc, decoded_cursor_factory: decoded_cursor_factory)
|
31
34
|
end
|
@@ -50,23 +53,94 @@ module ElasticGraph
|
|
50
53
|
total_document_count = metadata.dig("hits", "total", "value")
|
51
54
|
|
52
55
|
new(
|
53
|
-
raw_data
|
54
|
-
metadata
|
55
|
-
documents
|
56
|
-
total_document_count
|
56
|
+
raw_data:,
|
57
|
+
metadata:,
|
58
|
+
documents:,
|
59
|
+
total_document_count:,
|
60
|
+
aggregations_unavailable_reason:,
|
61
|
+
decoded_cursor_factory:
|
57
62
|
)
|
58
63
|
end
|
59
64
|
|
65
|
+
def self.synthesize_from_ids(index, ids, decoded_cursor_factory: DecodedCursor::Factory::Null)
|
66
|
+
hits = ids.map do |id|
|
67
|
+
{
|
68
|
+
"_index" => index,
|
69
|
+
"_type" => "_doc",
|
70
|
+
"_id" => id,
|
71
|
+
"_score" => nil,
|
72
|
+
"_source" => {"id" => id},
|
73
|
+
"sort" => [id]
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
raw_data = {
|
78
|
+
"took" => 0,
|
79
|
+
"timed_out" => false,
|
80
|
+
"_shards" => {
|
81
|
+
"total" => 0,
|
82
|
+
"successful" => 0,
|
83
|
+
"skipped" => 0,
|
84
|
+
"failed" => 0
|
85
|
+
},
|
86
|
+
"hits" => {
|
87
|
+
"total" => {
|
88
|
+
"value" => ids.size,
|
89
|
+
"relation" => "eq"
|
90
|
+
},
|
91
|
+
"max_score" => nil,
|
92
|
+
"hits" => hits
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
build(raw_data, decoded_cursor_factory: decoded_cursor_factory)
|
97
|
+
end
|
98
|
+
|
60
99
|
# Benign empty response that can be used in place of datastore response errors as needed.
|
61
100
|
RAW_EMPTY = {"hits" => {"hits" => [], "total" => {"value" => 0}}}.freeze
|
62
101
|
EMPTY = build(RAW_EMPTY)
|
63
102
|
|
103
|
+
# Returns a response filtered to results that have matching `values` at the given `field_path`, limiting
|
104
|
+
# the results to the first `size` results.
|
105
|
+
#
|
106
|
+
# This is designed for use in situations where we have N different datastore queries which are identical
|
107
|
+
# except for differing filter values. For efficiency, we combine those queries into a single query that
|
108
|
+
# filters on the set union of values. We can then use this method to "split" the single response into what
|
109
|
+
# the separate responses would have been if we hadn't combined into a single query.
|
110
|
+
def filter_results(field_path, values, size)
|
111
|
+
filter =
|
112
|
+
if field_path == ["id"]
|
113
|
+
# `id` filtering is a very common case, and we want to avoid having to request
|
114
|
+
# `id` within `_source`, given it's available as `_id`.
|
115
|
+
->(hit) { values.include?(hit.fetch("_id")) }
|
116
|
+
else
|
117
|
+
->(hit) { values.intersect?(Support::HashUtil.fetch_leaf_values_at_path(hit.fetch("_source"), field_path).to_set) }
|
118
|
+
end
|
119
|
+
|
120
|
+
hits = raw_data.fetch("hits").fetch("hits").select(&filter).first(size)
|
121
|
+
updated_raw_data = Support::HashUtil.deep_merge(raw_data, {"hits" => {"hits" => hits, "total" => nil}})
|
122
|
+
|
123
|
+
SearchResponse.build(
|
124
|
+
updated_raw_data,
|
125
|
+
decoded_cursor_factory: decoded_cursor_factory,
|
126
|
+
aggregations_unavailable_reason: "aggregations cannot be provided accurately on a search response filtered in memory"
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
64
130
|
def docs_description
|
65
131
|
(documents.size < 3) ? documents.inspect : "[#{documents.first}, ..., #{documents.last}]"
|
66
132
|
end
|
67
133
|
|
68
|
-
def total_document_count
|
69
|
-
super || raise(Errors::CountUnavailableError, "#{__method__} is unavailable; set `query.total_document_count_needed = true` to make it available")
|
134
|
+
def total_document_count(default: nil)
|
135
|
+
super() || default || raise(Errors::CountUnavailableError, "#{__method__} is unavailable; set `query.total_document_count_needed = true` to make it available")
|
136
|
+
end
|
137
|
+
|
138
|
+
def aggregations
|
139
|
+
if (reason = aggregations_unavailable_reason)
|
140
|
+
raise Errors::AggregationsUnavailableError, "Aggregations are unavailable on this search response: #{reason}."
|
141
|
+
end
|
142
|
+
|
143
|
+
raw_data["aggregations"] || {}
|
70
144
|
end
|
71
145
|
|
72
146
|
def to_s
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -28,6 +28,8 @@ module ElasticGraph
|
|
28
28
|
@config = config
|
29
29
|
end
|
30
30
|
|
31
|
+
INDICES_NOT_CONFIGURED_MESSAGE = "The datastore indices have not been configured. They must be configured before ElasticGraph can serve queries."
|
32
|
+
|
31
33
|
# Sends the datastore a multi-search request based on the given queries.
|
32
34
|
# Returns a hash of responses keyed by the query.
|
33
35
|
def msearch(queries, query_tracker: QueryDetailsTracker.empty)
|
@@ -78,9 +80,22 @@ module ElasticGraph
|
|
78
80
|
[response["took"], queries_for_cluster.zip(ordered_responses)]
|
79
81
|
end
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
83
|
+
queried_shard_count = server_took_and_results.reduce(0) do |outer_accum, (query, queries_and_responses)|
|
84
|
+
outer_accum + queries_and_responses.reduce(0) do |inner_accum, (query, response)|
|
85
|
+
shards_total = response.dig("_shards", "total")
|
86
|
+
|
87
|
+
if shards_total == 0 && !query.excluding_indices?
|
88
|
+
raise ::GraphQL::ExecutionError, INDICES_NOT_CONFIGURED_MESSAGE
|
89
|
+
end
|
90
|
+
|
91
|
+
inner_accum + (shards_total || 0)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
query_tracker.record_datastore_query_metrics(
|
96
|
+
client_duration_ms: @monotonic_clock.now_in_ms - datastore_query_started_at,
|
97
|
+
server_duration_ms: server_took_and_results.map(&:first).compact.max,
|
98
|
+
queried_shard_count: queried_shard_count
|
84
99
|
)
|
85
100
|
|
86
101
|
server_took_and_results.flat_map(&:last).to_h.tap do |responses_by_query|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -37,7 +37,7 @@ module ElasticGraph
|
|
37
37
|
when ::Hash
|
38
38
|
filter_object.to_h do |key, value|
|
39
39
|
field = parent_type.field_named(key)
|
40
|
-
[field.name_in_index
|
40
|
+
[field.name_in_index, convert(field.type.unwrap_fully, value)]
|
41
41
|
end
|
42
42
|
when ::Array
|
43
43
|
filter_object.map { |value| convert(parent_type, value) }
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -219,10 +219,14 @@ module ElasticGraph
|
|
219
219
|
end
|
220
220
|
|
221
221
|
def process_all_of_expression(bool_node, expressions, field_path)
|
222
|
-
# `all_of` represents an AND
|
223
|
-
#
|
222
|
+
# `all_of` represents an AND of multiple subexpressions.
|
223
|
+
# To achieve this, we build a new bool sub-filter for each subexpression and push it onto
|
224
|
+
# the parent’s `:filter` array. Each item in `:filter` is independently required (AND).
|
224
225
|
expressions.each do |sub_expression|
|
225
|
-
|
226
|
+
sub_filter = build_bool_hash do |sub_bool_node|
|
227
|
+
process_filter_hash(sub_bool_node, sub_expression, field_path)
|
228
|
+
end
|
229
|
+
bool_node[:filter] << sub_filter if sub_filter
|
226
230
|
end
|
227
231
|
end
|
228
232
|
|
@@ -339,7 +343,8 @@ module ElasticGraph
|
|
339
343
|
end
|
340
344
|
|
341
345
|
def build_bool_hash(&block)
|
342
|
-
bool_node = Hash.new { |h, k| h[k] = [] }
|
346
|
+
bool_node = Hash.new { |h, k| h[k] = [] } # : stringOrSymbolHash
|
347
|
+
bool_node.tap(&block)
|
343
348
|
|
344
349
|
# To treat "empty" filter predicates as `true` we need to return `nil` here.
|
345
350
|
return nil if bool_node.empty?
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -168,7 +168,7 @@ module ElasticGraph
|
|
168
168
|
when ::Array
|
169
169
|
value.map { |v| to_datastore_value(v) }
|
170
170
|
when Schema::EnumValue
|
171
|
-
value.name
|
171
|
+
value.name
|
172
172
|
else
|
173
173
|
value
|
174
174
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -99,6 +99,8 @@ module ElasticGraph
|
|
99
99
|
filter_value_set_for_filter_hash(filter_value, target_field_path_parts, traversed_field_path_parts + [field_or_op], negate: negate)
|
100
100
|
when :any_of
|
101
101
|
filter_value_set_for_any_of(filter_value, target_field_path_parts, traversed_field_path_parts, negate: negate)
|
102
|
+
when :all_of
|
103
|
+
filter_value_set_for_all_of(filter_value, target_field_path_parts, traversed_field_path_parts, negate: negate)
|
102
104
|
when :operator
|
103
105
|
# Check to make sure the operator applies to the target field. If not, we have no information
|
104
106
|
# in this clause. The set is unbounded, and may have exclusions.
|
@@ -106,7 +108,7 @@ module ElasticGraph
|
|
106
108
|
|
107
109
|
set = filter_value_set_for_field_filter(field_or_op, filter_value)
|
108
110
|
negate ? set.negate : set
|
109
|
-
when :
|
111
|
+
when :list_any_filter, :list_count, :unknown
|
110
112
|
# We have no information in this clause. The set is unbounded, and may have exclusions.
|
111
113
|
UnboundedSetWithExclusions
|
112
114
|
else
|
@@ -134,6 +136,19 @@ module ElasticGraph
|
|
134
136
|
end
|
135
137
|
end
|
136
138
|
|
139
|
+
# Determines the set of filter values for an `all_of` clause, which is used for ANDing multiple filters together.
|
140
|
+
def filter_value_set_for_all_of(filter_hashes, target_field_path_parts, traversed_field_path_parts, negate:)
|
141
|
+
# all_of: [] => match all values
|
142
|
+
if filter_hashes.empty?
|
143
|
+
return negate ? @empty_set : @all_values_set
|
144
|
+
end
|
145
|
+
|
146
|
+
# With all_of (AND), we do an intersection of subfilters
|
147
|
+
map_reduce_sets(filter_hashes, :intersection, negate: negate) do |filter_hash|
|
148
|
+
filter_value_set_for_filter_hash(filter_hash, target_field_path_parts, traversed_field_path_parts, negate: negate)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
137
152
|
# Determines the set of filter values for a single filter on a single field.
|
138
153
|
def filter_value_set_for_field_filter(filter_op, filter_value)
|
139
154
|
operator_name = @schema_names.canonical_name_for(filter_op)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -227,7 +227,7 @@ module ElasticGraph
|
|
227
227
|
# Steep weirdly expects them here...
|
228
228
|
# @dynamic initialize, config, logger, runtime_metadata, graphql_schema_string, datastore_core, clock
|
229
229
|
# @dynamic graphql_http_endpoint, graphql_query_executor, schema, datastore_search_router, filter_interpreter, filter_node_interpreter
|
230
|
-
# @dynamic datastore_query_builder, graphql_gem_plugins,
|
230
|
+
# @dynamic datastore_query_builder, graphql_gem_plugins, graphql_adapter, resolver_query_adapter, named_graphql_resolvers, datastore_query_adapters, monotonic_clock
|
231
231
|
# @dynamic load_dependencies_eagerly, self.from_parsed_yaml, filter_args_translator, sub_aggregation_grouping_adapter
|
232
232
|
end
|
233
233
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -14,13 +14,28 @@ module ElasticGraph
|
|
14
14
|
# need to satisfy the GraphQL query. This results in more efficient datastore queries,
|
15
15
|
# similar to doing `SELECT f1, f2, ...` instead of `SELECT *` for a SQL query.
|
16
16
|
class RequestedFields
|
17
|
+
# Partially applied `RequestedFields` -- essentially the `RequestedFields` without the schema,
|
18
|
+
# so that it can be instantiated before the `Schema` instance exists, instead providing it from
|
19
|
+
# `context` at query time.
|
20
|
+
class WithoutSchema
|
21
|
+
def call(field:, query:, lookahead:, args:, context:)
|
22
|
+
return query if field.type.unwrap_fully.indexed_aggregation?
|
23
|
+
|
24
|
+
RequestedFields.new(context.fetch(:elastic_graph_schema)).call(
|
25
|
+
field: field,
|
26
|
+
query: query,
|
27
|
+
lookahead: lookahead,
|
28
|
+
args: args,
|
29
|
+
context: context
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
17
34
|
def initialize(schema)
|
18
35
|
@schema = schema
|
19
36
|
end
|
20
37
|
|
21
38
|
def call(field:, query:, lookahead:, args:, context:)
|
22
|
-
return query if field.type.unwrap_fully.indexed_aggregation?
|
23
|
-
|
24
39
|
attributes = query_attributes_for(field: field, lookahead: lookahead)
|
25
40
|
query.merge_with(**attributes)
|
26
41
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Block, Inc.
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
2
|
#
|
3
3
|
# Use of this source code is governed by an MIT-style
|
4
4
|
# license that can be found in the LICENSE file or at
|
@@ -21,6 +21,7 @@ module ElasticGraph
|
|
21
21
|
:query_counts_per_datastore_request,
|
22
22
|
:datastore_query_server_duration_ms,
|
23
23
|
:datastore_query_client_duration_ms,
|
24
|
+
:queried_shard_count,
|
24
25
|
:mutex
|
25
26
|
)
|
26
27
|
def self.empty
|
@@ -31,6 +32,7 @@ module ElasticGraph
|
|
31
32
|
query_counts_per_datastore_request: [],
|
32
33
|
datastore_query_server_duration_ms: 0,
|
33
34
|
datastore_query_client_duration_ms: 0,
|
35
|
+
queried_shard_count: 0,
|
34
36
|
mutex: ::Thread::Mutex.new
|
35
37
|
)
|
36
38
|
end
|
@@ -49,12 +51,19 @@ module ElasticGraph
|
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
52
|
-
def
|
54
|
+
def record_datastore_query_metrics(client_duration_ms:, server_duration_ms:, queried_shard_count:)
|
53
55
|
mutex.synchronize do
|
54
|
-
self.datastore_query_client_duration_ms +=
|
55
|
-
self.datastore_query_server_duration_ms +=
|
56
|
+
self.datastore_query_client_duration_ms += client_duration_ms
|
57
|
+
self.datastore_query_server_duration_ms += server_duration_ms if server_duration_ms
|
58
|
+
self.queried_shard_count += queried_shard_count
|
56
59
|
end
|
57
60
|
end
|
61
|
+
|
62
|
+
# Indicates how long was spent on transport between the client and the datastore server, including
|
63
|
+
# network time, JSON serialization time, etc.
|
64
|
+
def datastore_request_transport_duration_ms
|
65
|
+
datastore_query_client_duration_ms - datastore_query_server_duration_ms
|
66
|
+
end
|
58
67
|
end
|
59
68
|
end
|
60
69
|
end
|