Phase 1: Add grouping_missing_value_placeholder runtime metadata - #890
Conversation
b83c780 to
cbf8ece
Compare
cbf8ece to
56425a7
Compare
myronmarston
left a comment
There was a problem hiding this comment.
This is coming along nicely! Left some feedback.
|
@myronmarston I'm asking a couple high level questions here to make it easier to follow rather than spread out through all the stacked PRs
|
My instinct is "yes". One of the core ideas of ElasticGraph is to "shift" as much computation as possible to schema artifact generation time so that the GraphQL engine can be as dumb and agnostic as possible, often by reading and using runtime metadata values. Storing the placeholder value in runtime metadata artifacts aligns with that. At GraphQL query time we're dealing with GraphQL types and having the desired placeholder value available on the GraphQL type by reading it from runtime metadata feels very straightforward and aligned with ElasticGraph's existing patterns.
It feels simplest to me to just have the placeholder be a direct field in the runtime metadata, rather than it specifying an enum value which has to be dereferenced to get the actual placeholder value. Plus it's more flexible--it enables the placeholder to be anything. While we don't have any plans to use values beyond the 3 you mentioned, in my book it's a win-win when the simplest solution affords the most flexibility. I'm also not concerned about the duplication/redundancy of the same value showing up multiple times in
Yep, the expectation/requirement is that all schema artifacts are fully up-to-date. Besides the "were these artifacts dumped by the same version of ElasticGraph?" check I mentioned before, there's also a standard
Now that we've realized a conflict is impossible for enums, I don't think it's worth including it in the runtime metadata. Instead, the
I don't think we have to offer a way to control this (I can't really think of a reason a schema author would need to) but if it's easy/low-effort to enable this customization with the path we're taking, then I think it's great to offer the option--maybe there's a use case we haven't thought of.
I think the runtime metadata system we're talking about offers a pretty easy way to make this customizable, where opting out on a type is as easy as setting it to diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb
index 1769059..4eb44b2 100644
--- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb
+++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb
@@ -44,6 +44,7 @@ module ElasticGraph
class ScalarType < Struct.new(
:schema_def_state,
:type_ref,
+ :grouping_missing_value_placeholder_overridden,
:mapping_type,
:runtime_metadata,
:aggregated_values_customizations,
@@ -66,12 +67,13 @@ module ElasticGraph
# @private
def initialize(schema_def_state, name)
- super(schema_def_state, schema_def_state.type_ref(name).to_final_form)
+ super(schema_def_state, schema_def_state.type_ref(name).to_final_form, false)
# Default the runtime metadata before yielding, so it can be overridden as needed.
self.runtime_metadata = SchemaArtifacts::RuntimeMetadata::ScalarType.new(
coercion_adapter_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_COERCION_ADAPTER_REF,
- indexing_preparer_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_INDEXING_PREPARER_REF
+ indexing_preparer_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_INDEXING_PREPARER_REF,
+ grouping_missing_value_placeholder: nil
)
yield self
@@ -91,12 +93,26 @@ module ElasticGraph
type_ref.name
end
+ def grouping_missing_value_placeholder(placeholder)
+ self.grouping_missing_value_placeholder_overridden = true
+ self.runtime_metadata = runtime_metadata.with(grouping_missing_value_placeholder: placeholder)
+ end
+
# (see Mixins::HasTypeInfo#mapping)
def mapping(**options)
self.mapping_type = options.fetch(:type) do
raise Errors::SchemaError, "Must specify a mapping `type:` on custom scalars but was missing on the `#{name}` type."
end
+ unless grouping_missing_value_placeholder_overridden
+ case mapping_type
+ when *NUMERIC_TYPES
+ self.runtime_metadata = runtime_metadata.with(grouping_missing_value_placeholder: MISSING_NUMERIC_PLACEHOLDER)
+ when *STRING_TYPES
+ self.runtime_metadata = runtime_metadata.with(grouping_missing_value_placeholder: MISSING_STRING_PLACEHOLDER)
+ end
+ end
+
super
end
@@ -313,6 +329,7 @@ module ElasticGraph
# https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
# https://www.elastic.co/guide/en/elasticsearch/reference/7.13/number.html#number
NUMERIC_TYPES = %w[long integer short byte double float half_float scaled_float unsigned_long].to_set
+ STRING_TYPES = %w[keyword text]
DATE_TYPES = %w[date date_nanos].to_set
# The Elasticsearch/OpenSearch docs do not exhaustively give a list of types on which range queries are efficient,
# but the docs are clear that it is efficient on numeric and date types, and is inefficient on stringEssentially, expose a That approach has the properties I think we want:
How does that plan sound? |
|
Thanks for responding to my latest questions. They aligned with the direction I was already thinking but I wanted to put a few options out there. |
578fea1 to
6065481
Compare
|
I've updated phase 1 to address all comments. |
myronmarston
left a comment
There was a problem hiding this comment.
Almost ready to merge!
6065481 to
39650dd
Compare
- Add grouping_missing_value_placeholder attribute to ScalarType - Update searialization methods to include/exclude the new attribute appropriately
39650dd to
105c149
Compare
This is phase 1 in a series of new pull requests that are working toward a new approach to handling missing values in subaggregations as described in #882. This PR is the first step outlined by @myronmarston here. It only includes adding grouping_missing_value_placeholder as an attribute of ScalarType and Enum::Type. For scalar types, the placeholder value can be specified when the type is defined. For enum types there is no need to make schema authors define a placeholder value. We use a placeholder value of
**missing**that can't conflict with enum name (since GraphQL enums may only contain letters and underscores). If this value conflicts with a datastore_value then we fallback to not using a placeholder value.Context
Due to limitations in OpenSearch and ElasticSearch, composite aggregations cannot be used as subaggregations with in a composite aggregation. When grouping by multiple fields in a subaggregation, ElasticGraph works around this limitation by creating a hierarchy of subaggregations. To handle missing values, each group by field results in a terms subaggregation and a missing subaggregation. When there are many fields to group by this results in an exponential explosion in the number of subaggregations. For some of our queries this is resulting in poor performance, or worse, causing queries to fail because they exceed max clause count on the OpenSearch cluster.
The new approach still relies on a hierarchy of subaggregations, but at each level of the hierarchy it only uses one child subaggregation instead of two, so the number of subaggregations is linear with respect to the number of group by fields, instead of being exponential.
This approach relies on providing a the terms aggregations a missing value to use as a placeholder. This isn't ideal but all workarounds involve a balance of tradeoffs.
Other PRs
Phase 2: Infer grouping_missing_value_placeholder based on mapping_type
Phase 3: Wire up grouping_missing_value_placeholder in GraphQL layer
Phase 4: Use grouping_missing_value_placeholder in aggregation logic