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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -0
  3. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +1 -1
  4. data/lib/elastic_graph/graphql/aggregation/query.rb +9 -3
  5. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +6 -38
  6. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +2 -2
  7. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +8 -7
  8. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +3 -3
  9. data/lib/elastic_graph/graphql/config.rb +0 -15
  10. data/lib/elastic_graph/graphql/datastore_query.rb +53 -13
  11. data/lib/elastic_graph/graphql/datastore_response/document.rb +4 -0
  12. data/lib/elastic_graph/graphql/datastore_search_router.rb +14 -4
  13. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +23 -6
  14. data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +62 -22
  15. data/lib/elastic_graph/graphql/query_adapter/filters.rb +10 -11
  16. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +16 -1
  17. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +1 -2
  18. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +1 -3
  19. data/lib/elastic_graph/graphql/resolvers/nested_relationships_source.rb +7 -74
  20. data/lib/elastic_graph/graphql/resolvers/query_source.rb +0 -1
  21. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +8 -5
  22. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +6 -4
  23. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +66 -2
  24. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +3 -3
  25. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +3 -3
  26. data/lib/elastic_graph/graphql/schema/type.rb +4 -0
  27. 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
- filter_from_args = filter_args_translator.translate_filter_args(field: field, args: args)
19
- automatic_filter = build_automatic_filter(filter_from_args: filter_from_args, query: query)
20
- filters = [filter_from_args, automatic_filter].compact
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(filters: filters)
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(filter_from_args:, query:)
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, filter_from_args || {}) }
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 `filter_from_args`
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, filter_from_args)
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(filter_from_args, index_def.fields_by_path)
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, filter_from_args) }
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, @schema_element_names, context)
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(filters: [{id: {equal_to_any_of: [part.manufacturer_id]}}])` 10 times to
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:, mode:)
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:, mode:)
89
- context.dataloader.with(self, query:, join:, context:, monotonic_clock:, mode:).load(ids)
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
- filters: filters_for(id_sets.reduce(:union)),
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(filters: filters_for(ids), requested_fields: requested_fields)
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, schema_element_names
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, schema_element_names, context)
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(schema_element_names, graphql_impl, nodes.size)
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(schema_element_names, graphql_impl, node)
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, schema_element_names, node
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, schema_element_names, raw_nodes, paginator, to_sort_value, get_total_edge_count
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
- schema_element_names: schema_element_names,
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| Edge.new(schema_element_names, node) }
44
+ @edges ||= nodes.map { |node| edge_class.new(schema, node) }
43
45
  end
44
46
 
45
47
  def nodes
@@ -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(schema_element_names:, search_response:, query:)
17
+ def self.build_from(schema:, search_response:, query:)
18
18
  document_paginator = query.document_paginator
19
19
 
20
20
  GenericAdapter.new(
21
- schema_element_names: schema_element_names,
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
- schema_element_names = context.fetch(:elastic_graph_schema).element_names
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
- schema_element_names: schema_element_names,
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
- schema_element_names: schema_element_names,
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 schema_element_names
20
+ # @dynamic schema
21
21
 
22
22
  def self.new(*fields, &block)
23
- Support::MemoizableData.define(:schema_element_names, *fields) do
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
- schema_element_names.canonical_name_for(name) ||
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