elasticgraph-graphql 0.19.3.0 → 1.0.0.rc2
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/README.md +53 -0
- data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +1 -1
- data/lib/elastic_graph/graphql/aggregation/query.rb +9 -3
- 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/datastore_search_router.rb +14 -4
- 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/query_source.rb +0 -1
- 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 +27 -27
@@ -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.
|
@@ -50,9 +50,15 @@ module ElasticGraph
|
|
50
50
|
|
51
51
|
attributes =
|
52
52
|
if field.type.relay_connection?
|
53
|
+
highlights = lookahead
|
54
|
+
.selection(@schema.element_names.edges)
|
55
|
+
.selection(@schema.element_names.highlights)
|
56
|
+
|
53
57
|
{
|
54
58
|
individual_docs_needed: pagination_fields_need_individual_docs?(lookahead),
|
55
|
-
requested_fields: requested_fields_under(relay_connection_node_from(lookahead), index_field_paths)
|
59
|
+
requested_fields: requested_fields_under(relay_connection_node_from(lookahead), index_field_paths),
|
60
|
+
request_all_highlights: requesting_all_highlights?(lookahead),
|
61
|
+
requested_highlights: requested_fields_under(highlights, index_field_paths)
|
56
62
|
}
|
57
63
|
else
|
58
64
|
{
|
@@ -142,6 +148,15 @@ module ElasticGraph
|
|
142
148
|
lookahead.selects?(@schema.element_names.total_edge_count)
|
143
149
|
end
|
144
150
|
|
151
|
+
def requesting_all_highlights?(lookahead)
|
152
|
+
all_highlights =
|
153
|
+
lookahead
|
154
|
+
.selection(@schema.element_names.edges)
|
155
|
+
.selection(@schema.element_names.all_highlights)
|
156
|
+
|
157
|
+
all_highlights.selects?(@schema.element_names.path) || all_highlights.selects?(@schema.element_names.snippets)
|
158
|
+
end
|
159
|
+
|
145
160
|
def graphql_dynamic_field?(node)
|
146
161
|
# As per https://spec.graphql.org/October2021/#sec-Objects,
|
147
162
|
# > 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
|
@@ -27,7 +27,6 @@ module ElasticGraph
|
|
27
27
|
|
28
28
|
def fetch(queries)
|
29
29
|
responses_by_query = @datastore_router.msearch(queries, query_tracker: @query_tracker)
|
30
|
-
@query_tracker.record_datastore_queries_for_single_request(queries)
|
31
30
|
queries.map { |q| responses_by_query.fetch(q) }
|
32
31
|
end
|
33
32
|
|
@@ -17,15 +17,18 @@ module ElasticGraph
|
|
17
17
|
# here we just adapt it to the ElasticGraph internal resolver interface.
|
18
18
|
class ArrayAdapter < ResolvableValue.new(:graphql_impl, :total_edge_count)
|
19
19
|
# `ResolvableValue.new` provides the following methods:
|
20
|
-
# @dynamic initialize, graphql_impl, total_edge_count,
|
20
|
+
# @dynamic initialize, graphql_impl, total_edge_count, 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
|
28
30
|
nodes ||= [] # : ::Array[untyped]
|
31
|
+
|
29
32
|
# ElasticGraph supports any schema elements (like a `first` argument) being renamed,
|
30
33
|
# but `GraphQL::Relay::ArrayConnection` would not understand a renamed argument.
|
31
34
|
# Here we map the args back to the canonical relay args so `ArrayConnection` can
|
@@ -40,7 +43,7 @@ module ElasticGraph
|
|
40
43
|
}.compact
|
41
44
|
|
42
45
|
graphql_impl = ::GraphQL::Pagination::ArrayConnection.new(nodes, context: context, **relay_args)
|
43
|
-
new(
|
46
|
+
new(schema, graphql_impl, nodes.size)
|
44
47
|
end
|
45
48
|
|
46
49
|
def page_info
|
@@ -49,7 +52,7 @@ module ElasticGraph
|
|
49
52
|
|
50
53
|
def edges
|
51
54
|
@edges ||= graphql_impl.nodes.map do |node|
|
52
|
-
Edge.new(
|
55
|
+
Edge.new(schema, graphql_impl, node)
|
53
56
|
end
|
54
57
|
end
|
55
58
|
|
@@ -60,7 +63,7 @@ module ElasticGraph
|
|
60
63
|
# Simple edge implementation for a node object.
|
61
64
|
class Edge < ResolvableValue.new(:graphql_impl, :node)
|
62
65
|
# `ResolvableValue.new` provides the following methods:
|
63
|
-
# @dynamic initialize, graphql_impl,
|
66
|
+
# @dynamic initialize, graphql_impl, schema, node
|
64
67
|
|
65
68
|
def cursor
|
66
69
|
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.unwrap_fully.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
|