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 amonthbucket is 28—31 days and honours daylight-saving shifts. Only a single unit is allowed (1d, not2d). -
fixed_interval— an exact multiple of a SI unit:ms,s,m,h,d.30dis 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 slidingwindowof 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.