elasticgraph-graphql 0.19.3.0 → 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 +53 -13
- 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 +21 -21
- data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +1 -2
- data/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb +5 -9
- 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 +17 -16
- 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/field.rb +1 -1
- data/lib/elastic_graph/graphql/schema/type.rb +4 -0
- data/lib/elastic_graph/graphql.rb +1 -7
- metadata +25 -25
@@ -41,22 +41,21 @@ module ElasticGraph
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def query_attributes_for(field:, lookahead:)
|
44
|
-
index_field_paths =
|
45
|
-
field.type
|
46
|
-
.unwrap_fully
|
47
|
-
.search_index_definitions
|
48
|
-
.flat_map { |index_def| index_def.fields_by_path.keys }
|
49
|
-
.to_set
|
50
|
-
|
51
44
|
attributes =
|
52
45
|
if field.type.relay_connection?
|
46
|
+
highlights = lookahead
|
47
|
+
.selection(@schema.element_names.edges)
|
48
|
+
.selection(@schema.element_names.highlights)
|
49
|
+
|
53
50
|
{
|
54
51
|
individual_docs_needed: pagination_fields_need_individual_docs?(lookahead),
|
55
|
-
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)
|
56
55
|
}
|
57
56
|
else
|
58
57
|
{
|
59
|
-
requested_fields: requested_fields_under(lookahead
|
58
|
+
requested_fields: requested_fields_under(lookahead)
|
60
59
|
}
|
61
60
|
end
|
62
61
|
|
@@ -75,9 +74,9 @@ module ElasticGraph
|
|
75
74
|
# can ignore its foreign key; but when we are determining requested fields for a parent type,
|
76
75
|
# we need to identify the foreign key to request from the datastore, without recursing into
|
77
76
|
# its children.
|
78
|
-
def requested_fields_under(node,
|
77
|
+
def requested_fields_under(node, path_prefix: "")
|
79
78
|
fields = node.selections.flat_map do |child|
|
80
|
-
requested_fields_for(child,
|
79
|
+
requested_fields_for(child, path_prefix: path_prefix)
|
81
80
|
end
|
82
81
|
|
83
82
|
fields << "#{path_prefix}__typename" if field_for(node.field)&.type&.abstract?
|
@@ -86,22 +85,14 @@ module ElasticGraph
|
|
86
85
|
|
87
86
|
# Identifies the fields we need to fetch from the datastore for the given node,
|
88
87
|
# and recursing into the fields under it as needed.
|
89
|
-
def requested_fields_for(node,
|
88
|
+
def requested_fields_for(node, path_prefix:)
|
90
89
|
return [] if graphql_dynamic_field?(node)
|
91
90
|
|
92
91
|
# @type var field: Schema::Field
|
93
92
|
field = _ = field_for(node.field)
|
94
93
|
|
95
94
|
if field.type.embedded_object?
|
96
|
-
|
97
|
-
if index_field_paths.include?(field_path)
|
98
|
-
# If we've reached a path to a scalar field, we should just return it instead of recursing.
|
99
|
-
# This allows an object field to use a `name_in_index` of a scalar field which can be
|
100
|
-
# necessary for some custom resolvers.
|
101
|
-
[field_path]
|
102
|
-
else
|
103
|
-
requested_fields_under(node, index_field_paths, path_prefix: "#{field_path}.")
|
104
|
-
end
|
95
|
+
requested_fields_under(node, path_prefix: "#{path_prefix}#{field.name_in_index}.")
|
105
96
|
else
|
106
97
|
field.index_field_names_for_resolution.map do |name|
|
107
98
|
"#{path_prefix}#{name}"
|
@@ -142,6 +133,15 @@ module ElasticGraph
|
|
142
133
|
lookahead.selects?(@schema.element_names.total_edge_count)
|
143
134
|
end
|
144
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
|
+
|
145
145
|
def graphql_dynamic_field?(node)
|
146
146
|
# As per https://spec.graphql.org/October2021/#sec-Objects,
|
147
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
|
@@ -17,11 +17,7 @@ module ElasticGraph
|
|
17
17
|
class GraphQLAdapterBuilder
|
18
18
|
def initialize(runtime_metadata:, named_resolvers:, query_adapter:)
|
19
19
|
@runtime_metadata = runtime_metadata
|
20
|
-
@
|
21
|
-
::Hash.new do |hash, field_config|
|
22
|
-
hash[field_config] = resolver_constructor.call(field_config)
|
23
|
-
end
|
24
|
-
end
|
20
|
+
@named_resolvers = named_resolvers
|
25
21
|
@query_adapter = query_adapter
|
26
22
|
end
|
27
23
|
|
@@ -46,10 +42,10 @@ module ElasticGraph
|
|
46
42
|
def object_type_hash
|
47
43
|
@runtime_metadata.object_types_by_name.filter_map do |type_name, type|
|
48
44
|
fields_hash = type.graphql_fields_by_name.filter_map do |field_name, field|
|
49
|
-
if (
|
50
|
-
resolver = @
|
51
|
-
raise Errors::SchemaError, "Resolver `#{
|
52
|
-
end
|
45
|
+
if (resolver_name = field.resolver)
|
46
|
+
resolver = @named_resolvers.fetch(resolver_name) do
|
47
|
+
raise Errors::SchemaError, "Resolver `#{resolver_name}` (for `#{type_name}.#{field_name}`) cannot be found."
|
48
|
+
end
|
53
49
|
|
54
50
|
resolver_lambda =
|
55
51
|
if resolver.method(:resolve).parameters.include?([:keyreq, :lookahead])
|
@@ -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
|
@@ -15,32 +15,33 @@ module ElasticGraph
|
|
15
15
|
module RelayConnection
|
16
16
|
# Relay connection adapter for an array. Implemented primarily by `GraphQL::Relay::ArrayConnection`;
|
17
17
|
# here we just adapt it to the ElasticGraph internal resolver interface.
|
18
|
-
class ArrayAdapter < ResolvableValue.new(:graphql_impl
|
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,
|
28
|
-
|
27
|
+
def self.build(nodes, args, context)
|
28
|
+
schema = context.fetch(:elastic_graph_schema)
|
29
|
+
schema_element_names = schema.element_names
|
30
|
+
|
29
31
|
# ElasticGraph supports any schema elements (like a `first` argument) being renamed,
|
30
32
|
# but `GraphQL::Relay::ArrayConnection` would not understand a renamed argument.
|
31
33
|
# Here we map the args back to the canonical relay args so `ArrayConnection` can
|
32
34
|
# understand them.
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
}.compact
|
35
|
+
relay_args = [:first, :after, :last, :before].to_h do |arg_name|
|
36
|
+
[arg_name, args[schema_element_names.public_send(arg_name)]]
|
37
|
+
end.compact
|
38
|
+
|
39
|
+
graphql_impl = ::GraphQL::Pagination::ArrayConnection.new(nodes || [], context: context, **relay_args)
|
40
|
+
new(schema, graphql_impl)
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
def total_edge_count
|
44
|
+
graphql_impl.nodes.size
|
44
45
|
end
|
45
46
|
|
46
47
|
def page_info
|
@@ -49,7 +50,7 @@ module ElasticGraph
|
|
49
50
|
|
50
51
|
def edges
|
51
52
|
@edges ||= graphql_impl.nodes.map do |node|
|
52
|
-
Edge.new(
|
53
|
+
Edge.new(schema, graphql_impl, node)
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
@@ -60,7 +61,7 @@ module ElasticGraph
|
|
60
61
|
# Simple edge implementation for a node object.
|
61
62
|
class Edge < ResolvableValue.new(:graphql_impl, :node)
|
62
63
|
# `ResolvableValue.new` provides the following methods:
|
63
|
-
# @dynamic initialize, graphql_impl,
|
64
|
+
# @dynamic initialize, graphql_impl, schema, node
|
64
65
|
|
65
66
|
def cursor
|
66
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
|
@@ -29,7 +29,7 @@ module ElasticGraph
|
|
29
29
|
@computation_detail = runtime_metadata&.computation_detail
|
30
30
|
@resolver = runtime_metadata&.resolver
|
31
31
|
@name_in_index = runtime_metadata&.name_in_index || name
|
32
|
-
@graphql_field.extras([:lookahead]) if resolvers_needing_lookahead.include?(@resolver
|
32
|
+
@graphql_field.extras([:lookahead]) if resolvers_needing_lookahead.include?(@resolver)
|
33
33
|
end
|
34
34
|
|
35
35
|
def type
|
@@ -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
|
@@ -183,13 +183,7 @@ module ElasticGraph
|
|
183
183
|
def named_graphql_resolvers
|
184
184
|
@named_graphql_resolvers ||= runtime_metadata.graphql_resolvers_by_name.transform_values do |resolver|
|
185
185
|
ext = resolver.load_resolver
|
186
|
-
|
187
|
-
->(field_config) do
|
188
|
-
(_ = ext.extension_class).new(
|
189
|
-
elasticgraph_graphql: self,
|
190
|
-
config: ext.config.merge(field_config)
|
191
|
-
)
|
192
|
-
end
|
186
|
+
(_ = ext.extension_class).new(elasticgraph_graphql: self, config: ext.config)
|
193
187
|
end
|
194
188
|
end
|
195
189
|
|