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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0966dcde797efc0d2e1f22901440b313e803a1db5cce33f79b515078901560c
4
- data.tar.gz: 8508a1aafcbf466c4664dc95aaf2a112193df1a3f2edc92c40427ed8f96cd5a0
3
+ metadata.gz: 24f8c6670bb21bcf8936b9bd55cc2e3d6964e78ef9e3742881e720dc0e212404
4
+ data.tar.gz: 8b1de56fd5b6732905fb07fc4d2ece0480784104e6b860d42b0f62027463cef2
5
5
  SHA512:
6
- metadata.gz: ccf09ea0494543be1b95d241d5592c1e35e4680b506c9776b1c823a83023af641c1ce4f71a305e8b733a72ef39d229f797aa8f1e21bfc2921a34cfb95d3c265b
7
- data.tar.gz: c4df4204da85ff79eda7ecfefde640a6f9344c52426148074e95cbc9330328f8e526cb874d9aa3e869797953b4fe04fb31c7fe276119de767ead86068375ad53
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
- else
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, interval_unit_key: element_names.truncation_unit),
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, interval_unit_key: element_names.truncation_unit),
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
- # Until `legacy_grouping_schema` is removed, we need to check both `granularity` and `truncation_unit`.
296
- def interval_from(node, schema_args, interval_unit_key:)
297
- enum_type_name = node.field.arguments.fetch(interval_unit_key).type.unwrap.graphql_name
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
- schema_element_names,
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(schema_element_names, bucket)
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:, schema_element_names:)
19
- build_from_buckets(query: query, parent_queries: [], schema_element_names: schema_element_names) do
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:, schema_element_names:, field_path: [], &build_buckets)
24
+ def self.build_from_buckets(query:, parent_queries:, schema:, field_path: [], &build_buckets)
25
25
  GraphQL::Resolvers::RelayConnection::GenericAdapter.new(
26
- schema_element_names: schema_element_names,
27
- raw_nodes: raw_nodes_for(query, parent_queries, schema_element_names, field_path, &build_buckets),
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, schema_element_names, field_path)
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
- schema_element_names: schema_element_names,
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(:schema_element_names, :sub_aggregations, :parent_queries, :sub_aggs_by_agg_key, :field_path)
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
- schema_element_names: schema_element_names,
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[schema_element_names.first] == 0
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
- :filters, :sort, :document_pagination, :requested_fields, :individual_docs_needed,
33
- :size_multiplier, :monotonic_clock_deadline, :schema_element_names
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
- filters: [],
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: self.individual_docs_needed || individual_docs_needed || !requested_fields.empty?,
109
- total_document_count_needed: self.total_document_count_needed || total_document_count_needed || aggregations.values.any?(&:needs_total_doc_count?),
110
- filters: self.filters + filters,
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
- filters,
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(filters, route_with_field_paths)
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(filters)}.compact)
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
- filters: [],
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
- filters: filters.to_set,
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
- individual_docs_needed: individual_docs_needed || !requested_fields.empty?,
360
- total_document_count_needed: total_document_count_needed || aggregations.values.any?(&:needs_total_doc_count?),
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,
@@ -58,6 +58,10 @@ module ElasticGraph
58
58
  payload["version"]
59
59
  end
60
60
 
61
+ def highlights
62
+ raw_data["highlight"] || {}
63
+ end
64
+
61
65
  def cursor
62
66
  @cursor ||= decoded_cursor_factory.build(raw_data.fetch("sort"))
63
67
  end
@@ -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 (:must, :filter, :should, or :must_not)
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
- new(:filter, clauses)
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
- schema_names.matches => ->(field_name, value) { BooleanQuery.must({match: {field_name => value}}) },
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.must(
98
- {
99
- match: {
100
- field_name => {
101
- query: value.fetch(schema_names.query),
102
- # This is always a string field, even though the value is often an integer
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.must(
112
- {
113
- match_phrase_prefix: {
114
- field_name => {
115
- query: value.fetch(schema_names.phrase)
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
- 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.
@@ -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, @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
@@ -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, schema_element_names
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, 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
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(schema_element_names, graphql_impl)
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(schema_element_names, graphql_impl, node)
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, schema_element_names, node
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, 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.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
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.19.2.2
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: 2025-05-05 00:00:00.000000000 Z
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.4
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.4
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.2
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.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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.19.2.2
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/v0.19.2.2
251
- documentation_uri: https://block.github.io/elasticgraph/api-docs/v0.19.2.2/
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/v0.19.2.2/elasticgraph-graphql
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.2'
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.2
272
+ rubygems_version: 3.6.7
273
273
  specification_version: 4
274
274
  summary: The ElasticGraph GraphQL query engine.
275
275
  test_files: []