Aggregations

This section documents the current Elasticsearch 9.x line (with 8.19 as the final 8.x release) as published at the Elasticsearch documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Kibana-only UIs, the ML/NLP model-management workflow, cross-cluster replication, and parts of the paid / serverless-only surface) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Elasticsearch iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

An aggregation turns a set of matching documents into counts, statistics, and grouped buckets in the same request that runs the query. This page covers how aggregations relate to the query, the metric and bucket families, how they nest, and the pipeline aggregations that compute over the buckets other aggregations produced. See Aggregations.

Running an aggregation alongside a query

Aggregations go in an aggs (or aggregations) object at the top level of the search body. Every named aggregation runs over the documents the query matched — narrow the query and every aggregation narrows with it. Set size: 0 to skip the hit list entirely when you only want the aggregated numbers; this also lets the shards skip fetching _source and scoring is not needed.

GET /orders/_search
{
  "size": 0,
  "query": { "term": { "status": "shipped" } },
  "aggs": {
    "revenue": { "sum": { "field": "amount" } },
    "by_region": { "terms": { "field": "region" } }
  }
}

post_filter runs after the aggregations are computed but before the hits are returned, so the buckets reflect the broader query while the hit list is narrowed. This is the standard pattern for faceted search: show counts for every colour, but only list the documents for the colour the user clicked.

GET /products/_search
{
  "query": { "match": { "name": "running shoe" } },
  "aggs": {
    "colors": { "terms": { "field": "color" } }
  },
  "post_filter": { "term": { "color": "red" } }
}
// "colors" still counts every colour matched by the query; hits are only red.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#post-filter

A filter inside the query (or a filter sub-aggregation) narrows the aggregation too; post_filter is the only knob that separates the two. For the query side of the request see Search API & pagination.

Metric aggregations

Metric aggregations compute a single value (or a small set of related values) over a numeric, date, or keyword field. See Metrics aggregations.

Single-value and multi-value statistics

avg, sum, min, max, and value_count each return one number. stats returns count, min, max, avg, and sum together in one pass; extended_stats adds variance, standard deviation, and the standard-deviation bounds.

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "order_stats":      { "stats":          { "field": "amount" } },
    "amount_spread":     { "extended_stats": { "field": "amount", "sigma": 2 } },
    "distinct_line_qty": { "value_count":    { "field": "quantity" } }
  }
}
// value_count counts non-null values, not distinct values -- use cardinality for that.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html

cardinality — approximate distinct count

cardinality estimates the number of distinct values using the HyperLogLog++ algorithm. It is approximate: memory and accuracy are bounded by precision_threshold (default 3000, max 40000), below which counts are usually exact and above which a small error appears. It exists because an exact distinct count would need to hold every value in memory.

GET /logs/_search
{
  "size": 0,
  "aggs": {
    "unique_visitors": {
      "cardinality": { "field": "visitor_id", "precision_threshold": 3000 }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html

percentiles and percentile_ranks — also approximate

percentiles reports the value below which a given percentage of the data falls (p50, p95, p99 …); percentile_ranks is the inverse — given a value, what percentage falls below it. Both use a t-digest sketch (tunable with tdigest.compression) or the fixed-memory HDR histogram, so results near the extremes carry a small error.

GET /requests/_search
{
  "size": 0,
  "aggs": {
    "latency_pct":  { "percentiles":      { "field": "took_ms", "percents": [50, 95, 99] } },
    "latency_ranks":{ "percentile_ranks": { "field": "took_ms", "values": [250, 500] } }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html

top_hits — sample documents per bucket

top_hits is a metric aggregation that returns the actual top documents for its enclosing bucket, with their own sort, size, _source filtering, and highlighting. Nested under a terms bucket it yields the classic "top 3 results per group" (field collapsing via collapse is the cheaper option when you need exactly one group level on the hit list).

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_brand": {
      "terms": { "field": "brand", "size": 10 },
      "aggs": {
        "cheapest": {
          "top_hits": {
            "size": 3,
            "sort": [ { "price": "asc" } ],
            "_source": [ "name", "price" ]
          }
        }
      }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html

Bucket aggregations

Bucket aggregations sort documents into buckets, each with a doc_count, and each able to hold sub-aggregations. See Bucket aggregations.

terms — group by field value

terms builds one bucket per distinct value, ordered by descending doc_count by default and capped at size (default 10). order can sort by _key, by _count, or by a sub-aggregation’s value ("order": \{ "revenue": "desc" }).

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "top_skus": {
      "terms": {
        "field": "sku",
        "size": 20,
        "order": { "revenue": "desc" }
      },
      "aggs": { "revenue": { "sum": { "field": "amount" } } }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html

Accuracy caveat. Each shard returns only its own top shard_size terms, so a term that is just outside the top on every shard can be missed or have an under-counted doc_count. The response reports doc_count_error_upper_bound (worst-case count that could be missing) and sum_other_doc_count (documents in no returned bucket). Raise shard_size, or use composite when you must enumerate every term exactly.

range and date_range — explicit numeric or date bands

range defines buckets by half-open intervals (from inclusive, to exclusive). date_range is the same for date fields and accepts date math and a format.

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "price_bands": {
      "range": {
        "field": "amount",
        "ranges": [
          { "to": 50 },
          { "from": 50, "to": 200 },
          { "from": 200 }
        ]
      }
    },
    "recent": {
      "date_range": {
        "field": "created_at",
        "format": "yyyy-MM-dd",
        "ranges": [
          { "from": "now-7d/d", "to": "now/d" },
          { "from": "now-30d/d", "to": "now-7d/d" }
        ]
      }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html

histogram and date_histogram — fixed-width buckets

histogram buckets a numeric field at a fixed interval. date_histogram buckets a date field and distinguishes two interval kinds:

  • calendar_interval — minute, hour, day, week, month, quarter, year. These follow the calendar, so a month bucket is 28—​31 days and honours daylight-saving shifts. Only a single unit is allowed (1d, not 2d).

  • fixed_interval — an exact multiple of a SI unit: ms, s, m, h, d. 30d is always exactly 30 * 24h, regardless of month boundaries or DST.

time_zone (e.g. "Europe/Madrid") shifts bucket boundaries to local midnight and applies DST offsets; without it, buckets align to UTC.

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "per_day": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "day",
        "time_zone": "Europe/Madrid",
        "min_doc_count": 0,
        "extended_bounds": { "min": "2026-01-01", "max": "2026-01-31" }
      },
      "aggs": { "revenue": { "sum": { "field": "amount" } } }
    }
  }
}
// min_doc_count: 0 + extended_bounds fills empty days so a time series has no gaps.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html

filter and filters — buckets from queries

filter creates a single sub-bucket of documents matching a query, to scope a metric. filters creates one named bucket per query, plus an optional other_bucket for the rest.

GET /logs/_search
{
  "size": 0,
  "aggs": {
    "by_severity": {
      "filters": {
        "other_bucket_key": "info",
        "filters": {
          "errors":   { "term": { "level": "error" } },
          "warnings": { "term": { "level": "warn" } }
        }
      },
      "aggs": { "hosts": { "cardinality": { "field": "host" } } }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html

nested and reverse_nested — aggregate inside nested objects

A nested field type stores each sub-object as a hidden Lucene document; a nested aggregation steps into that scope so metrics see one entry per sub-object rather than one per parent. reverse_nested climbs back out to the parent to aggregate a parent field from within a nested context.

GET /products/_search
{
  "size": 0,
  "aggs": {
    "reviews": {
      "nested": { "path": "reviews" },
      "aggs": {
        "by_rating": {
          "terms": { "field": "reviews.rating" },
          "aggs": {
            "back_to_product": {
              "reverse_nested": {},
              "aggs": { "products": { "cardinality": { "field": "sku" } } }
            }
          }
        }
      }
    }
  }
}
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html

The nested/reverse_nested pair and the join field’s children aggregation are covered together in Joins & relationships.

composite — page through every bucket

terms cannot be paginated. composite builds buckets from one or more value sources, returns them in sorted order in pages of size, and reports an after_key; feed that back as after to get the next page. It walks the whole cardinality without the top-N accuracy problem, at the cost of no order-by-metric.

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "sku_day": {
      "composite": {
        "size": 1000,
        "sources": [
          { "sku": { "terms": { "field": "sku" } } },
          { "day": { "date_histogram": { "field": "created_at", "calendar_interval": "day" } } }
        ],
        "after": { "sku": "SKU-4821", "day": 1767225600000 }
      },
      "aggs": { "revenue": { "sum": { "field": "amount" } } }
    }
  }
}
// Omit "after" on the first request; pass the response's after_key on each subsequent one.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html

Sub-aggregations and nesting

Any bucket aggregation can carry an aggs block. A metric inside a bucket computes that metric per bucket; a bucket inside a bucket subdivides each parent bucket, and the tree can go several levels deep (watch the bucket count — the product of the levels' cardinalities). Metric aggregations are always leaves.

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "by_region": {
      "terms": { "field": "region" },
      "aggs": {
        "per_month": {
          "date_histogram": { "field": "created_at", "calendar_interval": "month" },
          "aggs": {
            "revenue":     { "sum": { "field": "amount" } },
            "avg_order":   { "avg": { "field": "amount" } }
          }
        }
      }
    }
  }
}
// region -> month -> (revenue, avg_order)

Pipeline aggregations

Pipeline aggregations take the output of other aggregations (via buckets_path) rather than documents, so they are declared as siblings of the aggregation they consume, usually inside the parent bucket. parent pipelines (e.g. derivative, cumulative_sum, moving_fn) run on sibling buckets of a histogram; sibling pipelines (e.g. bucket_script, bucket_selector) run once per bucket. See Pipeline aggregations.

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "per_month": {
      "date_histogram": { "field": "created_at", "calendar_interval": "month" },
      "aggs": {
        "revenue":       { "sum": { "field": "amount" } },
        "cost":          { "sum": { "field": "cost" } },

        "revenue_delta": { "derivative":     { "buckets_path": "revenue" } },
        "revenue_ytd":   { "cumulative_sum": { "buckets_path": "revenue" } },
        "revenue_3m_avg":{ "moving_fn":      { "buckets_path": "revenue", "window": 3,
                                               "script": "MovingFunctions.unweightedAvg(values)" } },
        "margin":        { "bucket_script":  { "buckets_path": { "r": "revenue", "c": "cost" },
                                               "script": "(params.r - params.c) / params.r" } },
        "keep_profitable": { "bucket_selector": { "buckets_path": { "m": "margin" },
                                                  "script": "params.m > 0.2" } }
      }
    }
  }
}
  • derivative — change from the previous bucket. derivative

  • cumulative_sum — running total across buckets. cumulative_sum

  • moving_fn — a script (MovingFunctions.unweightedAvg, linearWeightedAvg, ewma, holt …) over a sliding window of prior buckets. moving_fn

  • bucket_script — a per-bucket Painless expression combining sibling metrics into a new value. bucket_script

  • bucket_selector — a per-bucket boolean filter that drops buckets whose script returns false (a HAVING clause). bucket_selector

bucket_sort (sort/paginate buckets in memory) and stats_bucket / max_bucket and friends (summarise a whole sibling series) round out the family.

Contrast with SQL and MongoDB

The terms + sub-metric shape is a GROUP BY; bucket_selector is HAVING; date_histogram is GROUP BY date_trunc(…​); the pipeline aggregations (derivative, cumulative_sum, moving_fn) are window functions. The differences: aggregations always run against the current query’s result set, terms counts are approximate under sharding unless you use composite, and top_hits returns whole documents per group with no join. For those equivalents see SQL Aggregate & Window Functions and MongoDB Aggregation Pipeline.

ES|QL expresses many of the same summaries with a pipe syntax (STATS …​ BY …​); see Query languages & scripting.