Module: ElasticGraph::GraphQL::Aggregation

Defined in:
lib/elastic_graph/graphql/aggregation/key.rb,
lib/elastic_graph/graphql/aggregation/query.rb,
lib/elastic_graph/graphql/aggregation/computation.rb,
lib/elastic_graph/graphql/aggregation/path_segment.rb,
lib/elastic_graph/graphql/aggregation/query_adapter.rb,
lib/elastic_graph/graphql/aggregation/term_grouping.rb,
lib/elastic_graph/graphql/aggregation/resolvers/node.rb,
lib/elastic_graph/graphql/aggregation/query_optimizer.rb,
lib/elastic_graph/graphql/aggregation/field_path_encoder.rb,
lib/elastic_graph/graphql/aggregation/field_term_grouping.rb,
lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb,
lib/elastic_graph/graphql/aggregation/script_term_grouping.rb,
lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb,
lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb,
lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb,
lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb,
lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb,
lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb,
lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb,
lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb

Defined Under Namespace

Modules: CompositeGroupingAdapter, FieldPathEncoder, Key, NonCompositeGroupingAdapter, Resolvers, TermGrouping Classes: DateHistogramGrouping, FieldTermGrouping, NestedSubAggregation, Query, QueryAdapter, QueryOptimizer, ScriptTermGrouping

Constant Summary collapse

AggregationDetail =

The details of an aggregation level, including the ‘aggs` clauses themselves and `meta` that we want echoed back to us in the response for the aggregation level.

::Data.define(
  # Aggregation clauses that would go under `aggs.
  :clauses,
  # Custom metadata that will be echoed back to us in the response.
  # https://www.elastic.co/guide/en/elasticsearch/reference/8.11/search-aggregations.html#add-metadata-to-an-agg
  :meta
) do
  # @implements AggregationDetail

  # Wraps this aggregation detail in another aggregation layer for the given `grouping`,
  # so that we can easily build up the necessary multi-level aggregation structure.
  def wrap_with_grouping(grouping, query:)
    agg_key = grouping.key
    extra_inner_meta = grouping.inner_meta.merge({
      # The response just includes tuples of values for the key of each bucket. We need to know what fields those
      # values come from, and this `meta` field  indicates that.
      "grouping_fields" => [agg_key]
    })

    inner_agg_hash = {
      "aggs" => (clauses unless (clauses || {}).empty?),
      "meta" => meta.merge(extra_inner_meta)
    }.compact

    missing_bucket_inner_agg_hash = inner_agg_hash.key?("aggs") ? inner_agg_hash : {} # : ::Hash[::String, untyped]

    AggregationDetail.new(
      {
        agg_key => grouping.non_composite_clause_for(query).merge(inner_agg_hash),

        # Here we include a `missing` aggregation as a sibling to the main grouping aggregation. We do this
        # so that we get a bucket of documents that have `null` values for the field we are grouping on, in
        # order to provide the same behavior as the `CompositeGroupingAdapter` (which uses the built-in
        # `missing_bucket` option).
        #
        # To work correctly, we need to include this `missing` aggregation as a sibling at _every_ level of
        # the aggregation structure, and the `missing` aggregation needs the same child aggregations as the
        # main grouping aggregation has. Given the recursive nature of how this is applied, this results in
        # a fairly complex structure, even though conceptually the idea behind this isn't _too_ bad.
        Key.missing_value_bucket_key(agg_key) => {
          "missing" => {"field" => grouping.encoded_index_field_path}
        }.merge(missing_bucket_inner_agg_hash)
      },
      {"buckets_path" => [agg_key]}
    )
  end
end
Computation =
::Data.define(:source_field_path, :computed_index_field_name, :detail) do
  # @implements Computation

  def key(aggregation_name:)
    Key::AggregatedValue.new(
      aggregation_name: aggregation_name,
      field_path: source_field_path.map(&:name_in_graphql_query),
      function_name: computed_index_field_name
    ).encode
  end

  def clause
    encoded_path = FieldPathEncoder.join(source_field_path.filter_map(&:name_in_index))
    {detail.function.to_s => {"field" => encoded_path}}
  end
end
PathSegment =
::Data.define(
  # The name of this segment's field in the GraphQL query. If it's an aliased field, this
  # will be the alias name.
  :name_in_graphql_query,
  # The name of this segment's field in the datastore index.
  :name_in_index
) do
  # Factory method that aids in building a `PathSegment` for a given `field` and `lookahead` node.
  def self.for(lookahead:, field: nil)
    ast_node = lookahead.ast_nodes.first # : ::GraphQL::Language::Nodes::Field

    new(
      name_in_graphql_query: ast_node.alias || ast_node.name,
      name_in_index: field&.name_in_index&.to_s
    )
  end
end