elasticgraph-graphql 0.19.2.2 → 1.0.0.rc1
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/lib/elastic_graph/graphql/aggregation/query_adapter.rb +6 -38
- data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +2 -2
- data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +8 -7
- data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +3 -3
- data/lib/elastic_graph/graphql/config.rb +0 -15
- data/lib/elastic_graph/graphql/datastore_query.rb +59 -14
- data/lib/elastic_graph/graphql/datastore_response/document.rb +4 -0
- data/lib/elastic_graph/graphql/filtering/boolean_query.rb +23 -6
- data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +62 -22
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +10 -11
- data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +16 -1
- data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +1 -2
- data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +1 -3
- data/lib/elastic_graph/graphql/resolvers/nested_relationships_source.rb +7 -74
- data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +8 -5
- data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +6 -4
- data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +66 -2
- data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +3 -3
- data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +3 -3
- data/lib/elastic_graph/graphql/schema/type.rb +4 -0
- metadata +25 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24f8c6670bb21bcf8936b9bd55cc2e3d6964e78ef9e3742881e720dc0e212404
|
4
|
+
data.tar.gz: 8b1de56fd5b6732905fb07fc4d2ece0480784104e6b860d42b0f62027463cef2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 399538281bc06bcea0a5ede7fc1c63990658def8aca9e9d2d22994e59d57d157fb0ef6b7d6c6b0270b528b278e8b77b3e20303bb9cbec2a5894bfccdc209e9fa
|
7
|
+
data.tar.gz: '072188ff912c10f81da236a0f5c9faab646976ad0bd33e57a0008bb2fc82add9e38896aa4e55331c308520145c4e79e6a5eba942e8f8b903862d05b29b2c82b6'
|
@@ -209,29 +209,9 @@ module ElasticGraph
|
|
209
209
|
# New date/time grouping API (DateGroupedBy, DateTimeGroupedBy)
|
210
210
|
if field.type.elasticgraph_category == :date_grouped_by_object
|
211
211
|
date_time_groupings_from(field_path: field_path, node: node)
|
212
|
-
|
213
212
|
elsif !field.type.object?
|
214
|
-
case field.type.name
|
215
|
-
# Legacy date grouping API
|
216
|
-
when "Date"
|
217
|
-
legacy_date_histogram_groupings_from(
|
218
|
-
field_path: field_path,
|
219
|
-
node: node,
|
220
|
-
get_time_zone: ->(args) {},
|
221
|
-
get_offset: ->(args) { args[element_names.offset_days]&.then { |days| "#{days}d" } }
|
222
|
-
)
|
223
|
-
# Legacy datetime grouping API
|
224
|
-
when "DateTime"
|
225
|
-
legacy_date_histogram_groupings_from(
|
226
|
-
field_path: field_path,
|
227
|
-
node: node,
|
228
|
-
get_time_zone: ->(args) { args.fetch(element_names.time_zone) },
|
229
|
-
get_offset: ->(args) { datetime_offset_from(node, args) }
|
230
|
-
)
|
231
213
|
# Non-date/time grouping
|
232
|
-
|
233
|
-
[FieldTermGrouping.new(field_path: field_path)]
|
234
|
-
end
|
214
|
+
[FieldTermGrouping.new(field_path: field_path)]
|
235
215
|
end
|
236
216
|
end
|
237
217
|
end
|
@@ -264,7 +244,7 @@ module ElasticGraph
|
|
264
244
|
field_path: child_field_path,
|
265
245
|
script_id: runtime_metadata.static_script_ids_by_scoped_name.fetch("field/as_time_of_day"),
|
266
246
|
params: {
|
267
|
-
"interval" => interval_from(child_node, schema_args
|
247
|
+
"interval" => interval_from(child_node, schema_args),
|
268
248
|
"offset_ms" => datetime_offset_as_ms_from(child_node, schema_args),
|
269
249
|
"time_zone" => time_zone
|
270
250
|
}
|
@@ -272,7 +252,7 @@ module ElasticGraph
|
|
272
252
|
else
|
273
253
|
DateHistogramGrouping.new(
|
274
254
|
field_path: child_field_path,
|
275
|
-
interval: interval_from(child_node, schema_args
|
255
|
+
interval: interval_from(child_node, schema_args),
|
276
256
|
offset: datetime_offset_from(child_node, schema_args),
|
277
257
|
time_zone: time_zone
|
278
258
|
)
|
@@ -280,22 +260,10 @@ module ElasticGraph
|
|
280
260
|
end
|
281
261
|
end
|
282
262
|
|
283
|
-
def legacy_date_histogram_groupings_from(field_path:, node:, get_time_zone:, get_offset:)
|
284
|
-
schema_args = Schema::Arguments.to_schema_form(node.arguments, node.field)
|
285
|
-
|
286
|
-
[DateHistogramGrouping.new(
|
287
|
-
field_path: field_path,
|
288
|
-
interval: interval_from(node, schema_args, interval_unit_key: element_names.granularity),
|
289
|
-
time_zone: get_time_zone.call(schema_args),
|
290
|
-
offset: get_offset.call(schema_args)
|
291
|
-
)]
|
292
|
-
end
|
293
|
-
|
294
263
|
# Figure out the Date histogram grouping interval for the given node based on the `grouped_by` argument.
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
enum_value_name = schema_args.fetch(interval_unit_key)
|
264
|
+
def interval_from(node, schema_args)
|
265
|
+
enum_type_name = node.field.arguments.fetch(element_names.truncation_unit).type.unwrap.graphql_name
|
266
|
+
enum_value_name = schema_args.fetch(element_names.truncation_unit)
|
299
267
|
enum_value = schema.type_named(enum_type_name).enum_value_named(enum_value_name)
|
300
268
|
|
301
269
|
_ = enum_value.runtime_metadata.datastore_value
|
@@ -29,7 +29,7 @@ module ElasticGraph
|
|
29
29
|
|
30
30
|
def sub_aggregations
|
31
31
|
@sub_aggregations ||= SubAggregations.new(
|
32
|
-
|
32
|
+
schema,
|
33
33
|
query.sub_aggregations,
|
34
34
|
parent_queries + [query],
|
35
35
|
bucket,
|
@@ -42,7 +42,7 @@ module ElasticGraph
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def count_detail
|
45
|
-
@count_detail ||= CountDetail.new(
|
45
|
+
@count_detail ||= CountDetail.new(schema, bucket)
|
46
46
|
end
|
47
47
|
|
48
48
|
def cursor
|
@@ -15,18 +15,19 @@ module ElasticGraph
|
|
15
15
|
module Aggregation
|
16
16
|
module Resolvers
|
17
17
|
module RelayConnectionBuilder
|
18
|
-
def self.build_from_search_response(query:, search_response:,
|
19
|
-
build_from_buckets(query: query, parent_queries: [],
|
18
|
+
def self.build_from_search_response(query:, search_response:, schema:)
|
19
|
+
build_from_buckets(query: query, parent_queries: [], schema: schema) do
|
20
20
|
extract_buckets_from(search_response, for_query: query)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def self.build_from_buckets(query:, parent_queries:,
|
24
|
+
def self.build_from_buckets(query:, parent_queries:, schema:, field_path: [], &build_buckets)
|
25
25
|
GraphQL::Resolvers::RelayConnection::GenericAdapter.new(
|
26
|
-
|
27
|
-
raw_nodes: raw_nodes_for(query, parent_queries,
|
26
|
+
schema: schema,
|
27
|
+
raw_nodes: raw_nodes_for(query, parent_queries, schema, field_path, &build_buckets),
|
28
28
|
paginator: query.paginator,
|
29
29
|
get_total_edge_count: -> {},
|
30
|
+
edge_class: (_ = GraphQL::Resolvers::RelayConnection::GenericAdapter::Edge),
|
30
31
|
to_sort_value: ->(node, decoded_cursor) do
|
31
32
|
query.groupings.map do |grouping|
|
32
33
|
DatastoreQuery::Paginator::SortValue.new(
|
@@ -39,13 +40,13 @@ module ElasticGraph
|
|
39
40
|
)
|
40
41
|
end
|
41
42
|
|
42
|
-
private_class_method def self.raw_nodes_for(query, parent_queries,
|
43
|
+
private_class_method def self.raw_nodes_for(query, parent_queries, schema, field_path)
|
43
44
|
# The `DecodedCursor::SINGLETON` is a special case, so handle it here.
|
44
45
|
return [] if query.paginator.paginated_from_singleton_cursor?
|
45
46
|
|
46
47
|
yield.map do |bucket|
|
47
48
|
Node.new(
|
48
|
-
|
49
|
+
schema: schema,
|
49
50
|
query: query,
|
50
51
|
parent_queries: parent_queries,
|
51
52
|
bucket: bucket,
|
@@ -19,7 +19,7 @@ module ElasticGraph
|
|
19
19
|
class GraphQL
|
20
20
|
module Aggregation
|
21
21
|
module Resolvers
|
22
|
-
class SubAggregations < ::Data.define(:
|
22
|
+
class SubAggregations < ::Data.define(:schema, :sub_aggregations, :parent_queries, :sub_aggs_by_agg_key, :field_path)
|
23
23
|
def resolve(field:, object:, args:, context:, lookahead:)
|
24
24
|
path_segment = PathSegment.for(field: field, lookahead: lookahead)
|
25
25
|
new_field_path = field_path + [path_segment]
|
@@ -31,7 +31,7 @@ module ElasticGraph
|
|
31
31
|
RelayConnectionBuilder.build_from_buckets(
|
32
32
|
query: sub_agg_query,
|
33
33
|
parent_queries: parent_queries,
|
34
|
-
|
34
|
+
schema: schema,
|
35
35
|
field_path: new_field_path
|
36
36
|
) { extract_buckets(sub_agg_key, args) }
|
37
37
|
end
|
@@ -41,7 +41,7 @@ module ElasticGraph
|
|
41
41
|
def extract_buckets(aggregation_field_path, args)
|
42
42
|
# When the client passes `first: 0`, we omit the sub-aggregation from the query body entirely,
|
43
43
|
# and it wont' be in `sub_aggs_by_agg_key`. Instead, we can just return an empty list of buckets.
|
44
|
-
return [] if args[
|
44
|
+
return [] if args[schema.element_names.first] == 0
|
45
45
|
|
46
46
|
sub_agg_key = Key.encode(parent_queries.map(&:name) + [aggregation_field_path])
|
47
47
|
sub_agg = Support::HashUtil.verbose_fetch(sub_aggs_by_agg_key, sub_agg_key)
|
@@ -20,12 +20,6 @@ module ElasticGraph
|
|
20
20
|
:max_page_size,
|
21
21
|
# Queries that take longer than this configured threshold will have a sanitized version logged.
|
22
22
|
:slow_query_latency_warning_threshold_in_ms,
|
23
|
-
# How to resolve nested relationships:
|
24
|
-
#
|
25
|
-
# - `optimized` (default): uses the new (in ElasticGraph 0.19.2.0) optimized resolver logic.
|
26
|
-
# - `original`: uses the resolver logic from ElasticGraph v0.19.1.1 and before.
|
27
|
-
# - `comparison`: runs both versions of the logic in serial, to compare them for correctness and performance. Results are logged.
|
28
|
-
:nested_relationship_resolver_mode,
|
29
23
|
# Object used to identify the client of a GraphQL query based on the HTTP request.
|
30
24
|
:client_resolver,
|
31
25
|
# Array of modules that will be extended onto the `GraphQL` instance to support extension libraries.
|
@@ -51,17 +45,10 @@ module ElasticGraph
|
|
51
45
|
end
|
52
46
|
end
|
53
47
|
|
54
|
-
nested_relationship_resolver_mode = parsed_yaml["nested_relationship_resolver_mode"]&.to_sym || :optimized
|
55
|
-
unless VALID_NESTED_RELATIONSHIP_RESOLVER_MODES.include?(nested_relationship_resolver_mode)
|
56
|
-
raise Errors::ConfigError, "Invalid value for `nested_relationship_resolver_mode`: #{nested_relationship_resolver_mode}. " \
|
57
|
-
"Valid values: #{VALID_NESTED_RELATIONSHIP_RESOLVER_MODES.join(", ")}."
|
58
|
-
end
|
59
|
-
|
60
48
|
new(
|
61
49
|
default_page_size: parsed_yaml.fetch("default_page_size"),
|
62
50
|
max_page_size: parsed_yaml.fetch("max_page_size"),
|
63
51
|
slow_query_latency_warning_threshold_in_ms: parsed_yaml["slow_query_latency_warning_threshold_in_ms"] || 5000,
|
64
|
-
nested_relationship_resolver_mode: nested_relationship_resolver_mode,
|
65
52
|
client_resolver: load_client_resolver(parsed_yaml),
|
66
53
|
extension_modules: extension_mods,
|
67
54
|
extension_settings: entire_parsed_yaml.except(*ELASTICGRAPH_CONFIG_KEYS)
|
@@ -74,8 +61,6 @@ module ElasticGraph
|
|
74
61
|
# The standard ElasticGraph root config setting keys; anything else is assumed to be extension settings.
|
75
62
|
ELASTICGRAPH_CONFIG_KEYS = %w[graphql indexer logger datastore schema_artifacts]
|
76
63
|
|
77
|
-
VALID_NESTED_RELATIONSHIP_RESOLVER_MODES = [:optimized, :original, :comparison]
|
78
|
-
|
79
64
|
private_class_method def self.load_client_resolver(parsed_yaml)
|
80
65
|
config = parsed_yaml.fetch("client_resolver") do
|
81
66
|
return Client::DefaultResolver.new({})
|
@@ -29,8 +29,9 @@ module ElasticGraph
|
|
29
29
|
class DatastoreQuery < Support::MemoizableData.define(
|
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
|
-
:
|
33
|
-
:
|
32
|
+
:client_filters, :internal_filters, :sort, :document_pagination,
|
33
|
+
:requested_fields, :request_all_fields, :requested_highlights, :request_all_highlights,
|
34
|
+
:individual_docs_needed, :size_multiplier, :monotonic_clock_deadline, :schema_element_names
|
34
35
|
)
|
35
36
|
# Load these files after the `Query` class has been defined, to avoid
|
36
37
|
# `TypeError: superclass mismatch for class Query`
|
@@ -96,20 +97,34 @@ module ElasticGraph
|
|
96
97
|
def merge_with(
|
97
98
|
individual_docs_needed: false,
|
98
99
|
total_document_count_needed: false,
|
99
|
-
|
100
|
+
client_filters: [],
|
101
|
+
internal_filters: [],
|
100
102
|
sort: [],
|
101
103
|
requested_fields: [],
|
104
|
+
request_all_fields: false,
|
105
|
+
requested_highlights: [],
|
106
|
+
request_all_highlights: false,
|
102
107
|
document_pagination: {},
|
103
108
|
size_multiplier: 1,
|
104
109
|
monotonic_clock_deadline: nil,
|
105
110
|
aggregations: {}
|
106
111
|
)
|
112
|
+
individual_docs_needed ||= self.individual_docs_needed ||
|
113
|
+
!requested_fields.empty? || request_all_fields ||
|
114
|
+
!requested_highlights.empty? || request_all_highlights
|
115
|
+
|
116
|
+
total_document_count_needed ||= self.total_document_count_needed || aggregations.values.any?(&:needs_total_doc_count?)
|
117
|
+
|
107
118
|
with(
|
108
|
-
individual_docs_needed:
|
109
|
-
total_document_count_needed:
|
110
|
-
|
119
|
+
individual_docs_needed: individual_docs_needed,
|
120
|
+
total_document_count_needed: total_document_count_needed,
|
121
|
+
client_filters: self.client_filters + client_filters,
|
122
|
+
internal_filters: self.internal_filters + internal_filters,
|
111
123
|
sort: merge_attribute(:sort, sort),
|
112
124
|
requested_fields: self.requested_fields + requested_fields,
|
125
|
+
request_all_fields: self.request_all_fields || request_all_fields,
|
126
|
+
requested_highlights: self.requested_highlights + requested_highlights,
|
127
|
+
request_all_highlights: self.request_all_highlights || request_all_highlights,
|
113
128
|
document_pagination: merge_attribute(:document_pagination, document_pagination),
|
114
129
|
size_multiplier: self.size_multiplier * size_multiplier,
|
115
130
|
monotonic_clock_deadline: [self.monotonic_clock_deadline, monotonic_clock_deadline].compact.min,
|
@@ -128,7 +143,7 @@ module ElasticGraph
|
|
128
143
|
# https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-index.html
|
129
144
|
def search_index_expression
|
130
145
|
@search_index_expression ||= index_expression_builder.determine_search_index_expression(
|
131
|
-
|
146
|
+
all_filters,
|
132
147
|
search_index_definitions,
|
133
148
|
# When we have aggregations, we must require indices to search. When we search no indices, the datastore does not return
|
134
149
|
# the standard aggregations response structure, which causes problems.
|
@@ -165,7 +180,7 @@ module ElasticGraph
|
|
165
180
|
# `[]` means that we are routing to no shards.
|
166
181
|
def shard_routing_values
|
167
182
|
return @shard_routing_values if defined?(@shard_routing_values)
|
168
|
-
routing_values = routing_picker.extract_eligible_routing_values(
|
183
|
+
routing_values = routing_picker.extract_eligible_routing_values(all_filters, route_with_field_paths)
|
169
184
|
|
170
185
|
@shard_routing_values ||=
|
171
186
|
if routing_values&.empty? && !aggregations_datastore_body.empty?
|
@@ -238,6 +253,10 @@ module ElasticGraph
|
|
238
253
|
document_paginator.effective_size
|
239
254
|
end
|
240
255
|
|
256
|
+
def all_filters
|
257
|
+
client_filters + internal_filters
|
258
|
+
end
|
259
|
+
|
241
260
|
private
|
242
261
|
|
243
262
|
def merge_attribute(attribute, other_value)
|
@@ -284,8 +303,7 @@ module ElasticGraph
|
|
284
303
|
def to_datastore_body
|
285
304
|
@to_datastore_body ||= aggregations_datastore_body
|
286
305
|
.merge(document_paginator.to_datastore_body)
|
287
|
-
.merge({query: filter_interpreter.build_query(
|
288
|
-
.merge({_source: source})
|
306
|
+
.merge({highlight: highlight, query: filter_interpreter.build_query(all_filters), _source: source}.compact)
|
289
307
|
end
|
290
308
|
|
291
309
|
def aggregations_datastore_body
|
@@ -304,6 +322,7 @@ module ElasticGraph
|
|
304
322
|
# at all--which means the datastore can avoid decompressing the _source field. Otherwise,
|
305
323
|
# we only ask for the fields we need to return.
|
306
324
|
def source
|
325
|
+
return true if request_all_fields
|
307
326
|
requested_source_fields = requested_fields - ["id"]
|
308
327
|
return false if requested_source_fields.empty?
|
309
328
|
# Merging in requested_fields as _source:{includes:} based on Elasticsearch documentation:
|
@@ -311,6 +330,19 @@ module ElasticGraph
|
|
311
330
|
{includes: requested_source_fields.to_a}
|
312
331
|
end
|
313
332
|
|
333
|
+
def highlight
|
334
|
+
return nil if !request_all_highlights && requested_highlights.empty?
|
335
|
+
|
336
|
+
# If there are no filters, there's nothing to highlight.
|
337
|
+
return nil if client_filters.empty?
|
338
|
+
|
339
|
+
field_paths = request_all_highlights ? ["*"] : requested_highlights
|
340
|
+
fields = field_paths.to_h { |field| [field, {}] }
|
341
|
+
highlight_query = filter_interpreter.build_query(client_filters) unless internal_filters.empty?
|
342
|
+
|
343
|
+
{fields:, highlight_query:}.compact
|
344
|
+
end
|
345
|
+
|
314
346
|
# Encapsulates dependencies of `Query`, giving us something we can expose off of `application`
|
315
347
|
# to build queries when desired.
|
316
348
|
class Builder < Support::MemoizableData.define(:runtime_metadata, :logger, :filter_interpreter, :filter_node_interpreter, :default_page_size, :max_page_size)
|
@@ -330,12 +362,16 @@ module ElasticGraph
|
|
330
362
|
|
331
363
|
def new_query(
|
332
364
|
search_index_definitions:,
|
333
|
-
|
365
|
+
client_filters: [],
|
366
|
+
internal_filters: [],
|
334
367
|
sort: [],
|
335
368
|
document_pagination: {},
|
336
369
|
size_multiplier: 1,
|
337
370
|
aggregations: {},
|
338
371
|
requested_fields: [],
|
372
|
+
request_all_fields: false,
|
373
|
+
requested_highlights: [],
|
374
|
+
request_all_highlights: false,
|
339
375
|
individual_docs_needed: false,
|
340
376
|
total_document_count_needed: false,
|
341
377
|
monotonic_clock_deadline: nil
|
@@ -344,20 +380,29 @@ module ElasticGraph
|
|
344
380
|
raise Errors::SearchFailedError, "Query is invalid, since it contains no `search_index_definitions`."
|
345
381
|
end
|
346
382
|
|
383
|
+
individual_docs_needed ||= !requested_fields.empty? || request_all_fields ||
|
384
|
+
!requested_highlights.empty? || request_all_highlights
|
385
|
+
|
386
|
+
total_document_count_needed ||= aggregations.values.any?(&:needs_total_doc_count?)
|
387
|
+
|
347
388
|
DatastoreQuery.new(
|
348
389
|
routing_picker: routing_picker,
|
349
390
|
index_expression_builder: index_expression_builder,
|
350
391
|
logger: logger,
|
351
392
|
schema_element_names: runtime_metadata.schema_element_names,
|
352
393
|
search_index_definitions: search_index_definitions,
|
353
|
-
|
394
|
+
client_filters: client_filters.to_set,
|
395
|
+
internal_filters: internal_filters.to_set,
|
354
396
|
sort: sort,
|
355
397
|
document_pagination: document_pagination,
|
356
398
|
size_multiplier: size_multiplier,
|
357
399
|
aggregations: aggregations,
|
358
400
|
requested_fields: requested_fields.to_set,
|
359
|
-
|
360
|
-
|
401
|
+
requested_highlights: requested_highlights.to_set,
|
402
|
+
request_all_fields: request_all_fields,
|
403
|
+
request_all_highlights: request_all_highlights,
|
404
|
+
individual_docs_needed: individual_docs_needed,
|
405
|
+
total_document_count_needed: total_document_count_needed,
|
361
406
|
monotonic_clock_deadline: monotonic_clock_deadline,
|
362
407
|
filter_interpreter: filter_interpreter,
|
363
408
|
default_page_size: default_page_size,
|
@@ -13,16 +13,33 @@ module ElasticGraph
|
|
13
13
|
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
|
14
14
|
#
|
15
15
|
# It is composed of:
|
16
|
-
# 1) The occurrence type (:
|
16
|
+
# 1) The occurrence type (:filter, :should, or :must_not)
|
17
17
|
# 2) A list of query clauses evaluated by the given occurrence type
|
18
18
|
# 3) An optional flag indicating whether the occurrence should be negated
|
19
|
+
#
|
20
|
+
# Note: since we never do anything with the score, we always prefer `filter` over `must`. If we ever
|
21
|
+
# decide to do something with the score (such as sorting by it), then we'll want to introduce `must`.
|
19
22
|
class BooleanQuery < ::Data.define(:occurrence, :clauses)
|
20
|
-
def self.must(*clauses)
|
21
|
-
new(:must, clauses)
|
22
|
-
end
|
23
|
-
|
24
23
|
def self.filter(*clauses)
|
25
|
-
|
24
|
+
unwrapped_clauses = clauses.map do |clause|
|
25
|
+
__skip__ = case clause
|
26
|
+
in {bool: {minimum_should_match: 1, should: [::Hash => single_should], **nil}, **nil}
|
27
|
+
# This case represents an `anyOf` with a single subfilter (`filter: {anyOf: [X]}`).
|
28
|
+
# Such an expression is semantically equivalent to `filter: X`, and we can unwrap the
|
29
|
+
# should clause in this case since there is only a single one.
|
30
|
+
#
|
31
|
+
# While it adds a bit of complexity to do this unwrapping, we believe it's worth it because
|
32
|
+
# it preserves the datastore's ability to apply caching. As the Elasticsearch documentation[^1]
|
33
|
+
# explains, the results of `filter` clauses can be cached, but not `should` clauses.
|
34
|
+
#
|
35
|
+
# [^1]: https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query
|
36
|
+
single_should
|
37
|
+
else
|
38
|
+
clause
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
new(:filter, unwrapped_clauses)
|
26
43
|
end
|
27
44
|
|
28
45
|
def self.should(*clauses)
|
@@ -90,33 +90,64 @@ module ElasticGraph
|
|
90
90
|
schema_names.gte => ->(field_name, value) { RangeQuery.new(field_name, :gte, value) },
|
91
91
|
schema_names.lt => ->(field_name, value) { RangeQuery.new(field_name, :lt, value) },
|
92
92
|
schema_names.lte => ->(field_name, value) { RangeQuery.new(field_name, :lte, value) },
|
93
|
-
|
93
|
+
|
94
94
|
schema_names.matches_query => ->(field_name, value) do
|
95
95
|
allowed_edits_per_term = value.fetch(schema_names.allowed_edits_per_term).runtime_metadata.datastore_abbreviation
|
96
96
|
|
97
|
-
BooleanQuery.
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
fuzziness: allowed_edits_per_term.to_s,
|
104
|
-
operator: value[schema_names.require_all_terms] ? "AND" : "OR"
|
105
|
-
}
|
106
|
-
}
|
107
|
-
}
|
108
|
-
)
|
97
|
+
BooleanQuery.filter({match: {field_name => {
|
98
|
+
query: value.fetch(schema_names.query),
|
99
|
+
# This is always a string field, even though the value is often an integer
|
100
|
+
fuzziness: allowed_edits_per_term.to_s,
|
101
|
+
operator: value[schema_names.require_all_terms] ? "AND" : "OR"
|
102
|
+
}}})
|
109
103
|
end,
|
104
|
+
|
110
105
|
schema_names.matches_phrase => ->(field_name, value) {
|
111
|
-
BooleanQuery.
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
106
|
+
BooleanQuery.filter({match_phrase_prefix: {field_name => {
|
107
|
+
query: value.fetch(schema_names.phrase)
|
108
|
+
}}})
|
109
|
+
},
|
110
|
+
|
111
|
+
schema_names.contains => ->(field_name, value) {
|
112
|
+
case_insensitive = value[schema_names.ignore_case] || false
|
113
|
+
anded_substrings = value[schema_names.all_substrings_of] || []
|
114
|
+
ored_substrings = value[schema_names.any_substring_of]
|
115
|
+
|
116
|
+
sub_expressions = anded_substrings.map do |substring|
|
117
|
+
substring_clause(field_name, substring, case_insensitive)
|
118
|
+
end
|
119
|
+
|
120
|
+
if ored_substrings
|
121
|
+
should_sub_expressions = ored_substrings.map do |substring|
|
122
|
+
substring_clause(field_name, substring, case_insensitive)
|
123
|
+
end
|
124
|
+
|
125
|
+
sub_expressions << {bool: {minimum_should_match: 1, should: should_sub_expressions}}
|
126
|
+
end
|
127
|
+
|
128
|
+
if ored_substrings&.empty?
|
129
|
+
BooleanQuery::ALWAYS_FALSE_FILTER
|
130
|
+
elsif sub_expressions.size > 0
|
131
|
+
BooleanQuery.filter(*sub_expressions)
|
132
|
+
end
|
133
|
+
},
|
134
|
+
|
135
|
+
schema_names.starts_with => ->(field_name, value) {
|
136
|
+
case_insensitive = value[schema_names.ignore_case] || false
|
137
|
+
ored_prefixes = value[schema_names.any_prefix_of]
|
138
|
+
|
139
|
+
sub_expressions = (ored_prefixes || []).map do |prefix|
|
140
|
+
{prefix: {field_name => {
|
141
|
+
value: prefix,
|
142
|
+
case_insensitive: case_insensitive
|
143
|
+
}}}
|
144
|
+
end
|
145
|
+
|
146
|
+
if ored_prefixes&.empty?
|
147
|
+
BooleanQuery::ALWAYS_FALSE_FILTER
|
148
|
+
elsif sub_expressions.size > 0
|
149
|
+
BooleanQuery.filter({bool: {minimum_should_match: 1, should: sub_expressions}})
|
150
|
+
end
|
120
151
|
},
|
121
152
|
|
122
153
|
# This filter operator wraps a geo distance query:
|
@@ -163,6 +194,15 @@ module ElasticGraph
|
|
163
194
|
}.freeze
|
164
195
|
end
|
165
196
|
|
197
|
+
def substring_clause(field_name, substring, case_insensitive)
|
198
|
+
{wildcard: {field_name => {
|
199
|
+
# We squeeze("*") to convert "**" to "*", which is not needed for correctness but is a bit simpler.
|
200
|
+
# There's no point in two consecutive "*" wildcards.
|
201
|
+
value: "*#{substring}*".squeeze("*"),
|
202
|
+
case_insensitive: case_insensitive
|
203
|
+
}}}
|
204
|
+
end
|
205
|
+
|
166
206
|
def to_datastore_value(value)
|
167
207
|
case value
|
168
208
|
when ::Array
|
@@ -15,31 +15,30 @@ module ElasticGraph
|
|
15
15
|
class QueryAdapter
|
16
16
|
class Filters < Support::MemoizableData.define(:schema_element_names, :filter_args_translator, :filter_node_interpreter)
|
17
17
|
def call(field:, query:, args:, lookahead:, context:)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
return query if filters.empty?
|
18
|
+
client_filter = filter_args_translator.translate_filter_args(field: field, args: args)
|
19
|
+
internal_filter = build_automatic_filter(client_filter: client_filter, query: query)
|
20
|
+
return query if client_filter.nil? && internal_filter.nil?
|
22
21
|
|
23
|
-
query.merge_with(
|
22
|
+
query.merge_with(client_filters: [client_filter].compact, internal_filters: [internal_filter].compact)
|
24
23
|
end
|
25
24
|
|
26
25
|
private
|
27
26
|
|
28
|
-
def build_automatic_filter(
|
27
|
+
def build_automatic_filter(client_filter:, query:)
|
29
28
|
# If an incomplete document could be hit by a search with our filters against any of the
|
30
29
|
# index definitions, we must add a filter that will exclude incomplete documents.
|
31
30
|
exclude_incomplete_docs_filter if query
|
32
31
|
.search_index_definitions
|
33
|
-
.any? { |index_def| search_could_hit_incomplete_docs?(index_def,
|
32
|
+
.any? { |index_def| search_could_hit_incomplete_docs?(index_def, client_filter || {}) }
|
34
33
|
end
|
35
34
|
|
36
35
|
def exclude_incomplete_docs_filter
|
37
36
|
{"__sources" => {schema_element_names.equal_to_any_of => [SELF_RELATIONSHIP_NAME]}}
|
38
37
|
end
|
39
38
|
|
40
|
-
# Indicates if a search against the given `index_def` using the given `
|
39
|
+
# Indicates if a search against the given `index_def` using the given `client_filter`
|
41
40
|
# could hit an incomplete document.
|
42
|
-
def search_could_hit_incomplete_docs?(index_def,
|
41
|
+
def search_could_hit_incomplete_docs?(index_def, client_filter)
|
43
42
|
# If the index definition doesn't allow any searches to hit incomplete documents, we
|
44
43
|
# can immediately return `false` without checking the filters.
|
45
44
|
return false unless index_def.searches_could_hit_incomplete_docs?
|
@@ -53,7 +52,7 @@ module ElasticGraph
|
|
53
52
|
#
|
54
53
|
# Here we determine what field paths we need to check (e.g. only those field paths that are against
|
55
54
|
# self-sourced fields).
|
56
|
-
paths_to_check = determine_paths_to_check(
|
55
|
+
paths_to_check = determine_paths_to_check(client_filter, index_def.fields_by_path)
|
57
56
|
|
58
57
|
# If we have no paths to check, then our filters don't exclude incomplete documents and we must return `true`.
|
59
58
|
return true if paths_to_check.empty?
|
@@ -61,7 +60,7 @@ module ElasticGraph
|
|
61
60
|
# Finally, we look over each path. If all our filters allow the search to match documents that have `nil`
|
62
61
|
# at that path, then the search can hit incomplete documents. But if even one path excludes documents
|
63
62
|
# that have a `null` value for the field, we can safely return `false` for a more efficient query.
|
64
|
-
paths_to_check.all? { |path| can_match_nil_values_at?(path,
|
63
|
+
paths_to_check.all? { |path| can_match_nil_values_at?(path, client_filter) }
|
65
64
|
end
|
66
65
|
|
67
66
|
# Figures out which field paths we need to check to see if a filter on it could match an incomplete document.
|
@@ -43,9 +43,15 @@ module ElasticGraph
|
|
43
43
|
def query_attributes_for(field:, lookahead:)
|
44
44
|
attributes =
|
45
45
|
if field.type.relay_connection?
|
46
|
+
highlights = lookahead
|
47
|
+
.selection(@schema.element_names.edges)
|
48
|
+
.selection(@schema.element_names.highlights)
|
49
|
+
|
46
50
|
{
|
47
51
|
individual_docs_needed: pagination_fields_need_individual_docs?(lookahead),
|
48
|
-
requested_fields: requested_fields_under(relay_connection_node_from(lookahead))
|
52
|
+
requested_fields: requested_fields_under(relay_connection_node_from(lookahead)),
|
53
|
+
request_all_highlights: requesting_all_highlights?(lookahead),
|
54
|
+
requested_highlights: requested_fields_under(highlights)
|
49
55
|
}
|
50
56
|
else
|
51
57
|
{
|
@@ -127,6 +133,15 @@ module ElasticGraph
|
|
127
133
|
lookahead.selects?(@schema.element_names.total_edge_count)
|
128
134
|
end
|
129
135
|
|
136
|
+
def requesting_all_highlights?(lookahead)
|
137
|
+
all_highlights =
|
138
|
+
lookahead
|
139
|
+
.selection(@schema.element_names.edges)
|
140
|
+
.selection(@schema.element_names.all_highlights)
|
141
|
+
|
142
|
+
all_highlights.selects?(@schema.element_names.path) || all_highlights.selects?(@schema.element_names.snippets)
|
143
|
+
end
|
144
|
+
|
130
145
|
def graphql_dynamic_field?(node)
|
131
146
|
# As per https://spec.graphql.org/October2021/#sec-Objects,
|
132
147
|
# > All fields defined within an Object type must not have a name which begins with "__"
|
@@ -16,7 +16,6 @@ module ElasticGraph
|
|
16
16
|
# Responsible for fetching a single field value from a document.
|
17
17
|
class GetRecordFieldValue
|
18
18
|
def initialize(elasticgraph_graphql:, config:)
|
19
|
-
@schema_element_names = elasticgraph_graphql.runtime_metadata.schema_element_names
|
20
19
|
end
|
21
20
|
|
22
21
|
def resolve(field:, object:, args:, context:)
|
@@ -32,7 +31,7 @@ module ElasticGraph
|
|
32
31
|
value = [] if value.nil? && field.type.list?
|
33
32
|
|
34
33
|
if field.type.relay_connection?
|
35
|
-
RelayConnection::ArrayAdapter.build(value, args,
|
34
|
+
RelayConnection::ArrayAdapter.build(value, args, context)
|
36
35
|
else
|
37
36
|
value
|
38
37
|
end
|
@@ -23,7 +23,6 @@ module ElasticGraph
|
|
23
23
|
@schema_element_names = elasticgraph_graphql.runtime_metadata.schema_element_names
|
24
24
|
@logger = elasticgraph_graphql.logger
|
25
25
|
@monotonic_clock = elasticgraph_graphql.monotonic_clock
|
26
|
-
@resolver_mode = elasticgraph_graphql.config.nested_relationship_resolver_mode
|
27
26
|
end
|
28
27
|
|
29
28
|
def resolve(field:, object:, args:, context:, lookahead:)
|
@@ -41,8 +40,7 @@ module ElasticGraph
|
|
41
40
|
NestedRelationshipsSource.execute_one(
|
42
41
|
Array(id_or_ids).to_set,
|
43
42
|
query:, join:, context:,
|
44
|
-
monotonic_clock: @monotonic_clock
|
45
|
-
mode: @resolver_mode
|
43
|
+
monotonic_clock: @monotonic_clock
|
46
44
|
)
|
47
45
|
|
48
46
|
join.normalize_documents(initial_response) do |problem|
|
@@ -32,7 +32,7 @@ module ElasticGraph
|
|
32
32
|
#
|
33
33
|
# This optimization, when we can apply it, results in much less load on the datastore. In addition, it also helps to reduce
|
34
34
|
# the amount of overhead imposed by ElasticGraph. Profiling has shown that significant overhead is incurred when we repeatedly
|
35
|
-
# merge filters into a query (e.g. `query.merge_with(
|
35
|
+
# merge filters into a query (e.g. `query.merge_with(internal_filters: [{id: {equal_to_any_of: [part.manufacturer_id]}}])` 10 times to
|
36
36
|
# produce 10 different queries). This optimization also avoids that overhead.
|
37
37
|
#
|
38
38
|
# Note: while the comments discuss the examples in terms of _parent objects_, in the implementation, we deal with id sets.
|
@@ -60,7 +60,7 @@ module ElasticGraph
|
|
60
60
|
# isn't particularly expensive, compared to needing to re-run an extra query.
|
61
61
|
EXTRA_SIZE_MULTIPLIER = 4
|
62
62
|
|
63
|
-
def initialize(query:, join:, context:, monotonic_clock
|
63
|
+
def initialize(query:, join:, context:, monotonic_clock:)
|
64
64
|
@query = query
|
65
65
|
@join = join
|
66
66
|
@filter_id_field_name_path = @join.filter_id_field_name.split(".")
|
@@ -69,24 +69,15 @@ module ElasticGraph
|
|
69
69
|
@schema_element_names = elastic_graph_schema.element_names
|
70
70
|
@logger = elastic_graph_schema.logger
|
71
71
|
@monotonic_clock = monotonic_clock
|
72
|
-
@mode = mode
|
73
72
|
end
|
74
73
|
|
75
74
|
def fetch(id_sets)
|
76
75
|
return fetch_original(id_sets) unless can_merge_filters?
|
77
|
-
|
78
|
-
case @mode
|
79
|
-
when :original
|
80
|
-
fetch_original(id_sets)
|
81
|
-
when :comparison
|
82
|
-
fetch_comparison(id_sets)
|
83
|
-
else
|
84
|
-
fetch_optimized(id_sets)
|
85
|
-
end
|
76
|
+
fetch_optimized(id_sets)
|
86
77
|
end
|
87
78
|
|
88
|
-
def self.execute_one(ids, query:, join:, context:, monotonic_clock
|
89
|
-
context.dataloader.with(self, query:, join:, context:, monotonic_clock
|
79
|
+
def self.execute_one(ids, query:, join:, context:, monotonic_clock:)
|
80
|
+
context.dataloader.with(self, query:, join:, context:, monotonic_clock:).load(ids)
|
90
81
|
end
|
91
82
|
|
92
83
|
private
|
@@ -116,52 +107,6 @@ module ElasticGraph
|
|
116
107
|
fetch_via_separate_queries(id_sets, requested_fields: requested_fields)
|
117
108
|
end
|
118
109
|
|
119
|
-
def fetch_comparison(id_sets)
|
120
|
-
# Note: we'd ideally run both versions of the logic in parallel, but our attempts to do that resulted in errors
|
121
|
-
# because of the fiber context in which dataloaders run.
|
122
|
-
original_duration_ms, original_results = time_duration do
|
123
|
-
# In the `fetch_optimized` implementation, we request this extra field. We don't need it for
|
124
|
-
# the original implementation (so `fetch_original` doesn't also request that field...) but for
|
125
|
-
# the purposes of comparison we need to request it so that the document payloads will have the
|
126
|
-
# same fields.
|
127
|
-
#
|
128
|
-
# Note: we don't add the requested field if we have only a single id set, in order to align with
|
129
|
-
# the short-circuiting logic in `fetch_via_single_query_with_merged_filters`. Otherwise, any time
|
130
|
-
# we have a single id set we always get reported differences which are not actually real!
|
131
|
-
requested_fields = (id_sets.size > 1) ? [@join.filter_id_field_name] : [] # : ::Array[::String]
|
132
|
-
fetch_original(id_sets, requested_fields: requested_fields)
|
133
|
-
end
|
134
|
-
|
135
|
-
optimized_duration_ms, optimized_results = time_duration do
|
136
|
-
fetch_optimized(id_sets)
|
137
|
-
end
|
138
|
-
|
139
|
-
# To see if we got the same results we only look at the documents, because we expect differences outside
|
140
|
-
# of the documents--for example, the `SearchResponse#metadata` will report different `took` values.
|
141
|
-
got_same_results = original_results.map(&:documents) == optimized_results.map(&:documents)
|
142
|
-
message = {
|
143
|
-
"message_type" => "NestedRelationshipsComparisonResults",
|
144
|
-
"field" => @join.field.description,
|
145
|
-
"original_duration_ms" => original_duration_ms,
|
146
|
-
"optimized_duration_ms" => optimized_duration_ms,
|
147
|
-
"optimized_faster" => (optimized_duration_ms < original_duration_ms),
|
148
|
-
"id_set_count" => id_sets.size,
|
149
|
-
"total_id_count" => id_sets.reduce(:union).size,
|
150
|
-
"got_same_results" => got_same_results
|
151
|
-
}
|
152
|
-
|
153
|
-
if got_same_results
|
154
|
-
@logger.info(message)
|
155
|
-
else
|
156
|
-
@logger.error(message.merge({
|
157
|
-
"original_documents" => loggable_results(original_results),
|
158
|
-
"optimized_documents" => loggable_results(optimized_results)
|
159
|
-
}))
|
160
|
-
end
|
161
|
-
|
162
|
-
original_results
|
163
|
-
end
|
164
|
-
|
165
110
|
# For "simple", document-based queries, we can safely merge filters. However, this cannot be done safely when the response
|
166
111
|
# cannot safely be "pulled part" into the bits that apply to a particular set of ids for a parent object. Specifically:
|
167
112
|
#
|
@@ -227,7 +172,7 @@ module ElasticGraph
|
|
227
172
|
|
228
173
|
# First, we build a combined query with filters that account for all ids we are filtering on from all `id_sets`.
|
229
174
|
filtered_query = @query.merge_with(
|
230
|
-
|
175
|
+
internal_filters: filters_for(id_sets.reduce(:union)),
|
231
176
|
requested_fields: [@join.filter_id_field_name],
|
232
177
|
# We need to request a larger size than `@query` originally had. If the original size was `10` and we have
|
233
178
|
# 5 sets of ids, then, at a minimum, we need to request 50 results (10 results for each id set).
|
@@ -271,7 +216,7 @@ module ElasticGraph
|
|
271
216
|
|
272
217
|
def fetch_via_separate_queries(id_sets, requested_fields: [])
|
273
218
|
queries = id_sets.map do |ids|
|
274
|
-
@query.merge_with(
|
219
|
+
@query.merge_with(internal_filters: filters_for(ids), requested_fields: requested_fields)
|
275
220
|
end
|
276
221
|
|
277
222
|
results = QuerySource.execute_many(queries, for_context: @context)
|
@@ -307,18 +252,6 @@ module ElasticGraph
|
|
307
252
|
stop_time = @monotonic_clock.now_in_ms
|
308
253
|
[stop_time - start_time, result]
|
309
254
|
end
|
310
|
-
|
311
|
-
# Converts the given list of responses into a format we can safely log when we are logging
|
312
|
-
# response differences. We include the `id` (to identify the document) and the `hash` (so
|
313
|
-
# we can tell if the payload of a document differed, without logging the contents of that
|
314
|
-
# payload).
|
315
|
-
def loggable_results(responses)
|
316
|
-
responses.map do |response|
|
317
|
-
response.documents.map do |doc|
|
318
|
-
"#{doc.id} (hash: #{doc.hash})"
|
319
|
-
end
|
320
|
-
end
|
321
|
-
end
|
322
255
|
end
|
323
256
|
end
|
324
257
|
end
|
@@ -17,14 +17,17 @@ module ElasticGraph
|
|
17
17
|
# here we just adapt it to the ElasticGraph internal resolver interface.
|
18
18
|
class ArrayAdapter < ResolvableValue.new(:graphql_impl)
|
19
19
|
# `ResolvableValue.new` provides the following methods:
|
20
|
-
# @dynamic initialize, graphql_impl,
|
20
|
+
# @dynamic initialize, graphql_impl, schema
|
21
21
|
|
22
22
|
# `def_delegators` provides the following methods:
|
23
23
|
# @dynamic start_cursor, end_cursor, has_next_page, has_previous_page
|
24
24
|
extend Forwardable
|
25
25
|
def_delegators :graphql_impl, :start_cursor, :end_cursor, :has_next_page, :has_previous_page
|
26
26
|
|
27
|
-
def self.build(nodes, args,
|
27
|
+
def self.build(nodes, args, context)
|
28
|
+
schema = context.fetch(:elastic_graph_schema)
|
29
|
+
schema_element_names = schema.element_names
|
30
|
+
|
28
31
|
# ElasticGraph supports any schema elements (like a `first` argument) being renamed,
|
29
32
|
# but `GraphQL::Relay::ArrayConnection` would not understand a renamed argument.
|
30
33
|
# Here we map the args back to the canonical relay args so `ArrayConnection` can
|
@@ -34,7 +37,7 @@ module ElasticGraph
|
|
34
37
|
end.compact
|
35
38
|
|
36
39
|
graphql_impl = ::GraphQL::Pagination::ArrayConnection.new(nodes || [], context: context, **relay_args)
|
37
|
-
new(
|
40
|
+
new(schema, graphql_impl)
|
38
41
|
end
|
39
42
|
|
40
43
|
def total_edge_count
|
@@ -47,7 +50,7 @@ module ElasticGraph
|
|
47
50
|
|
48
51
|
def edges
|
49
52
|
@edges ||= graphql_impl.nodes.map do |node|
|
50
|
-
Edge.new(
|
53
|
+
Edge.new(schema, graphql_impl, node)
|
51
54
|
end
|
52
55
|
end
|
53
56
|
|
@@ -58,7 +61,7 @@ module ElasticGraph
|
|
58
61
|
# Simple edge implementation for a node object.
|
59
62
|
class Edge < ResolvableValue.new(:graphql_impl, :node)
|
60
63
|
# `ResolvableValue.new` provides the following methods:
|
61
|
-
# @dynamic initialize, graphql_impl,
|
64
|
+
# @dynamic initialize, graphql_impl, schema, node
|
62
65
|
|
63
66
|
def cursor
|
64
67
|
graphql_impl.cursor_for(node)
|
@@ -21,13 +21,15 @@ module ElasticGraph
|
|
21
21
|
# Lambda that is used to convert a node to a sort value during truncation.
|
22
22
|
:to_sort_value,
|
23
23
|
# Gets an optional count of total edges.
|
24
|
-
:get_total_edge_count
|
24
|
+
:get_total_edge_count,
|
25
|
+
# The class used for edges
|
26
|
+
:edge_class
|
25
27
|
)
|
26
|
-
# @dynamic initialize, with,
|
28
|
+
# @dynamic initialize, with, schema, raw_nodes, paginator, to_sort_value, get_total_edge_count, edge_class
|
27
29
|
|
28
30
|
def page_info
|
29
31
|
@page_info ||= PageInfo.new(
|
30
|
-
|
32
|
+
schema: schema,
|
31
33
|
before_truncation_nodes: before_truncation_nodes,
|
32
34
|
edges: edges,
|
33
35
|
paginator: paginator
|
@@ -39,7 +41,7 @@ module ElasticGraph
|
|
39
41
|
end
|
40
42
|
|
41
43
|
def edges
|
42
|
-
@edges ||= nodes.map { |node|
|
44
|
+
@edges ||= nodes.map { |node| edge_class.new(schema, node) }
|
43
45
|
end
|
44
46
|
|
45
47
|
def nodes
|
data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb
CHANGED
@@ -14,14 +14,15 @@ module ElasticGraph
|
|
14
14
|
module RelayConnection
|
15
15
|
# Adapts an `DatastoreResponse::SearchResponse` to what the graphql gem expects for a relay connection.
|
16
16
|
class SearchResponseAdapterBuilder
|
17
|
-
def self.build_from(
|
17
|
+
def self.build_from(schema:, search_response:, query:)
|
18
18
|
document_paginator = query.document_paginator
|
19
19
|
|
20
20
|
GenericAdapter.new(
|
21
|
-
|
21
|
+
schema: schema,
|
22
22
|
raw_nodes: search_response.to_a,
|
23
23
|
paginator: document_paginator.paginator,
|
24
24
|
get_total_edge_count: -> { search_response.total_document_count },
|
25
|
+
edge_class: DocumentEdge,
|
25
26
|
to_sort_value: ->(document, decoded_cursor) do
|
26
27
|
(_ = document).sort.zip(decoded_cursor.sort_values.values, document_paginator.sort).map do |from_document, from_cursor, sort_clause|
|
27
28
|
DatastoreQuery::Paginator::SortValue.new(
|
@@ -34,6 +35,69 @@ module ElasticGraph
|
|
34
35
|
)
|
35
36
|
end
|
36
37
|
end
|
38
|
+
|
39
|
+
class DocumentEdge < GenericAdapter::Edge
|
40
|
+
def highlights
|
41
|
+
@highlights ||= node.highlights.each_with_object({}) do |(path_string, snippets), highlights| # $::Hash[::String, untyped]
|
42
|
+
*object_fields, leaf_field = path_string.split(".")
|
43
|
+
leaf_hash = object_fields.reduce(highlights) { |accum, field| accum[field] ||= {} }
|
44
|
+
leaf_hash[leaf_field.to_s] = snippets
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def all_highlights
|
49
|
+
@all_highlights ||= begin
|
50
|
+
document_type = schema.document_type_stored_in(node.index_definition_name)
|
51
|
+
|
52
|
+
node.highlights.filter_map do |path_string, snippets|
|
53
|
+
if (path = path_from(path_string, document_type))
|
54
|
+
SearchHighlight.new(schema, path, snippets)
|
55
|
+
end
|
56
|
+
end.sort_by(&:path)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def path_from(path_string, document_type)
|
63
|
+
type = document_type
|
64
|
+
|
65
|
+
# The GraphQL field name and `name_in_index` can be different. The datastore returns path segments using
|
66
|
+
# the `name_in_index` but we want to return the GraphQL field name, so here we translate.
|
67
|
+
path_string.split(".").map do |name_in_index|
|
68
|
+
fields = type.fields_by_name_in_index[name_in_index] || []
|
69
|
+
|
70
|
+
# It's possible (but pretty rare) for a single `name_in_index` to map to multiple GraphQL fields.
|
71
|
+
# We don't really have a basis for preferring one over another so we just use the first one here.
|
72
|
+
field = fields.first
|
73
|
+
|
74
|
+
# It's possible (but should be *very* rare) that `name_in_index` does not map to any GraphQL fields.
|
75
|
+
# Here's a situation where that could happen:
|
76
|
+
#
|
77
|
+
# * The schema has an `indexing_only: true` field.
|
78
|
+
# * A custom query interceptor (used via `elasticgraph-query_interceptor`) merges some `client_filters` into an intercepted
|
79
|
+
# query which filters on the indexing-only field.
|
80
|
+
#
|
81
|
+
# It would be more correct for the query interceptor to use `internal_filters` for that case, but in case we've
|
82
|
+
# run into this situation, logging a warning and hiding the highlight is the best we can do.
|
83
|
+
unless field
|
84
|
+
schema.logger.warn(
|
85
|
+
"Skipping SearchHighlight for #{document_type.name} #{node.id} which contains a path (#{path_string}) " \
|
86
|
+
"that does not map to any GraphQL field path."
|
87
|
+
)
|
88
|
+
|
89
|
+
return nil
|
90
|
+
end
|
91
|
+
|
92
|
+
type = field.type
|
93
|
+
field.name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class SearchHighlight < ResolvableValue.new(:path, :snippets)
|
99
|
+
# @dynamic initialize, path, snippets
|
100
|
+
end
|
37
101
|
end
|
38
102
|
end
|
39
103
|
end
|
@@ -20,11 +20,11 @@ module ElasticGraph
|
|
20
20
|
def self.maybe_wrap(search_response, field:, context:, lookahead:, query:)
|
21
21
|
return search_response unless field.type.relay_connection?
|
22
22
|
|
23
|
-
|
23
|
+
schema = context.fetch(:elastic_graph_schema)
|
24
24
|
|
25
25
|
unless field.type.unwrap_fully.indexed_aggregation?
|
26
26
|
return SearchResponseAdapterBuilder.build_from(
|
27
|
-
|
27
|
+
schema: schema,
|
28
28
|
search_response: search_response,
|
29
29
|
query: query
|
30
30
|
)
|
@@ -32,7 +32,7 @@ module ElasticGraph
|
|
32
32
|
|
33
33
|
agg_name = lookahead.ast_nodes.first&.alias || lookahead.name
|
34
34
|
Aggregation::Resolvers::RelayConnectionBuilder.build_from_search_response(
|
35
|
-
|
35
|
+
schema: schema,
|
36
36
|
search_response: search_response,
|
37
37
|
query: Support::HashUtil.verbose_fetch(query.aggregations, agg_name)
|
38
38
|
)
|
@@ -17,10 +17,10 @@ module ElasticGraph
|
|
17
17
|
# and also has a corresponding method definition.
|
18
18
|
module ResolvableValue
|
19
19
|
# `MemoizableData.define` provides the following methods:
|
20
|
-
# @dynamic
|
20
|
+
# @dynamic schema
|
21
21
|
|
22
22
|
def self.new(*fields, &block)
|
23
|
-
Support::MemoizableData.define(:
|
23
|
+
Support::MemoizableData.define(:schema, *fields) do
|
24
24
|
# @implements ResolvableValueClass
|
25
25
|
include ResolvableValue
|
26
26
|
class_exec(&block) if block
|
@@ -41,7 +41,7 @@ module ElasticGraph
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def canonical_name_for(name, element_type)
|
44
|
-
|
44
|
+
schema.element_names.canonical_name_for(name) ||
|
45
45
|
raise(Errors::SchemaError, "#{element_type} `#{name}` is not a defined schema element")
|
46
46
|
end
|
47
47
|
end
|
@@ -126,6 +126,10 @@ module ElasticGraph
|
|
126
126
|
raise Errors::NotFoundError, msg
|
127
127
|
end
|
128
128
|
|
129
|
+
def fields_by_name_in_index
|
130
|
+
@fields_by_name_in_index ||= @fields_by_name.values.group_by(&:name_in_index)
|
131
|
+
end
|
132
|
+
|
129
133
|
def enum_value_named(enum_value_name)
|
130
134
|
@enum_values_by_name[enum_value_name]
|
131
135
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elasticgraph-graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Myron Marston
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
- Block Engineering
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: base64
|
@@ -31,42 +31,42 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - '='
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: 0.
|
34
|
+
version: 1.0.0.rc1
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - '='
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: 0.
|
41
|
+
version: 1.0.0.rc1
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: elasticgraph-schema_artifacts
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
46
|
- - '='
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: 0.
|
48
|
+
version: 1.0.0.rc1
|
49
49
|
type: :runtime
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - '='
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: 0.
|
55
|
+
version: 1.0.0.rc1
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: graphql
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: 2.5.
|
62
|
+
version: 2.5.6
|
63
63
|
type: :runtime
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: 2.5.
|
69
|
+
version: 2.5.6
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: graphql-c_parser
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: '1.1'
|
77
77
|
- - ">="
|
78
78
|
- !ruby/object:Gem::Version
|
79
|
-
version: 1.1.
|
79
|
+
version: 1.1.3
|
80
80
|
type: :runtime
|
81
81
|
prerelease: false
|
82
82
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -86,77 +86,77 @@ dependencies:
|
|
86
86
|
version: '1.1'
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: 1.1.
|
89
|
+
version: 1.1.3
|
90
90
|
- !ruby/object:Gem::Dependency
|
91
91
|
name: elasticgraph-admin
|
92
92
|
requirement: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - '='
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
96
|
+
version: 1.0.0.rc1
|
97
97
|
type: :development
|
98
98
|
prerelease: false
|
99
99
|
version_requirements: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - '='
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 1.0.0.rc1
|
104
104
|
- !ruby/object:Gem::Dependency
|
105
105
|
name: elasticgraph-elasticsearch
|
106
106
|
requirement: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - '='
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 1.0.0.rc1
|
111
111
|
type: :development
|
112
112
|
prerelease: false
|
113
113
|
version_requirements: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - '='
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.
|
117
|
+
version: 1.0.0.rc1
|
118
118
|
- !ruby/object:Gem::Dependency
|
119
119
|
name: elasticgraph-opensearch
|
120
120
|
requirement: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - '='
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 0.
|
124
|
+
version: 1.0.0.rc1
|
125
125
|
type: :development
|
126
126
|
prerelease: false
|
127
127
|
version_requirements: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - '='
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: 0.
|
131
|
+
version: 1.0.0.rc1
|
132
132
|
- !ruby/object:Gem::Dependency
|
133
133
|
name: elasticgraph-indexer
|
134
134
|
requirement: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
136
|
- - '='
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: 0.
|
138
|
+
version: 1.0.0.rc1
|
139
139
|
type: :development
|
140
140
|
prerelease: false
|
141
141
|
version_requirements: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - '='
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: 0.
|
145
|
+
version: 1.0.0.rc1
|
146
146
|
- !ruby/object:Gem::Dependency
|
147
147
|
name: elasticgraph-schema_definition
|
148
148
|
requirement: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - '='
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 0.
|
152
|
+
version: 1.0.0.rc1
|
153
153
|
type: :development
|
154
154
|
prerelease: false
|
155
155
|
version_requirements: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - '='
|
158
158
|
- !ruby/object:Gem::Version
|
159
|
-
version: 0.
|
159
|
+
version: 1.0.0.rc1
|
160
160
|
email:
|
161
161
|
- myron@squareup.com
|
162
162
|
executables: []
|
@@ -247,10 +247,10 @@ licenses:
|
|
247
247
|
- MIT
|
248
248
|
metadata:
|
249
249
|
bug_tracker_uri: https://github.com/block/elasticgraph/issues
|
250
|
-
changelog_uri: https://github.com/block/elasticgraph/releases/tag/
|
251
|
-
documentation_uri: https://block.github.io/elasticgraph/api-docs/
|
250
|
+
changelog_uri: https://github.com/block/elasticgraph/releases/tag/v1.0.0.rc1
|
251
|
+
documentation_uri: https://block.github.io/elasticgraph/api-docs/v1.0.0.rc1/
|
252
252
|
homepage_uri: https://block.github.io/elasticgraph/
|
253
|
-
source_code_uri: https://github.com/block/elasticgraph/tree/
|
253
|
+
source_code_uri: https://github.com/block/elasticgraph/tree/v1.0.0.rc1/elasticgraph-graphql
|
254
254
|
gem_category: core
|
255
255
|
rdoc_options: []
|
256
256
|
require_paths:
|
@@ -259,7 +259,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
259
259
|
requirements:
|
260
260
|
- - ">="
|
261
261
|
- !ruby/object:Gem::Version
|
262
|
-
version: '3.
|
262
|
+
version: '3.4'
|
263
263
|
- - "<"
|
264
264
|
- !ruby/object:Gem::Version
|
265
265
|
version: '3.5'
|
@@ -269,7 +269,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
269
269
|
- !ruby/object:Gem::Version
|
270
270
|
version: '0'
|
271
271
|
requirements: []
|
272
|
-
rubygems_version: 3.6.
|
272
|
+
rubygems_version: 3.6.7
|
273
273
|
specification_version: 4
|
274
274
|
summary: The ElasticGraph GraphQL query engine.
|
275
275
|
test_files: []
|