Elasticsearch Aggregations

Besides finding documents, Elasticsearch can summarize them. Aggregations compute analytics over the documents matching a query: counts per category, revenue per day, the 99th percentile latency per service, the number of unique users. They power faceted navigation in e-commerce search ("Brand: Acme (42)"), dashboards in Kibana, and log and metrics analytics, all computed in near real time over the same index you search.

Aggregations resemble SQL's GROUP BY (see SQL GROUP BY), but they're built from composable bucket and metric aggregations that can nest arbitrarily. Some of them, like cardinality, percentiles, and terms across shards, are approximate by design, trading exactness for speed at scale.

TL;DR

Quick Example

A faceted product search, and an analytics query:

Core Concepts

Bucket Aggregations

Buckets can contain sub-aggregations, for example revenue per day per country.

Metric Aggregations

avg, sum, min, max, value_count, stats/extended_stats, percentiles, percentile_ranks, cardinality (distinct count), top_hits (sample documents per bucket), weighted_avg, geo_bounds, and more.

Pipeline Aggregations

These operate on other aggregations' outputs: derivative (change between buckets), cumulative_sum, moving_fn (moving averages), bucket_script (per-bucket formulas like conversion rate), bucket_selector (filter buckets, like SQL HAVING), and bucket_sort (sort or truncate buckets).

Doc Values and Field Types

Aggregations read doc values, a columnar on-disk structure built for keyword, numeric, date, boolean, IP, and geo fields. Aggregating on text fields requires fielddata (in-memory and expensive) and aggregates individual tokens, which is almost never what you want. Map aggregatable strings as keyword. See Elasticsearch mappings.

Faceted Search With post_filter

When a user selects "Brand: Acme", you want the result list filtered, but the brand facet should still show counts for other brands so the user can switch. post_filter applies after aggregations are computed:

For multiple facet groups, use per-facet filter aggregations that apply all other selected facets, to get correct counts.

Approximation and Accuracy

Paging Through Buckets

terms returns the top N only. To iterate through all buckets (for example to export every customer's totals), use the composite aggregation with after_key pagination.

Best Practices

Set size: 0 for Pure Analytics

When you only need aggregation results, "size": 0 skips fetching and scoring hits. It's faster and lets aggregation results be cached (the shard request cache).

Filter Before Aggregating

Narrow the document set with filters (time range, tenant, status) in filter context. Aggregations over fewer documents are faster and more meaningful.

Pre-Aggregate High-Volume Data

For dashboards over billions of events, use transforms, rollups, or downsampling of time-series data streams to maintain summary indices, rather than aggregating raw events on every page load.

Watch Memory and Bucket Counts

High-cardinality terms aggregations with deep nesting can create enormous numbers of buckets. Elasticsearch caps it with search.max_buckets. Limit sizes, avoid nesting high-cardinality fields, and use composite for exhaustive iteration.

Common Mistakes

Aggregating a text Field

Using terms for Exact Unique Counts

The number of buckets from a terms aggregation isn't a distinct count, since it's truncated to size. Use cardinality for approximate distinct counts, or composite to enumerate them exactly.

Time Zone Mismatches in Date Histograms

Daily buckets default to UTC, so "sales per day" for a European shop splits days at the wrong hour. Set time_zone on date histograms and date ranges.

FAQ

How do Elasticsearch aggregations compare to SQL GROUP BY?

Bucket aggregations are like GROUP BY (and more flexible: ranges, histograms, filters), and metric aggregations are like aggregate functions (SUM, AVG, COUNT DISTINCT). They nest to arbitrary depth, and they can run in the same request as a full-text search. Elasticsearch also offers a SQL interface that translates into aggregations.

Why are my terms aggregation counts slightly wrong?

On multi-shard indices, each shard returns its own top terms, and the coordinating node merges them, so terms that were just below a shard's cutoff can be undercounted. Increase shard_size, check doc_count_error_upper_bound, or use fewer shards for small datasets.

How do I build facet counts that don't disappear when selected?

Run aggregations against the query without the facet's own selection, and apply selected facets through post_filter (for hits) plus filter aggregations for each facet's cross-filtering. That keeps counts for alternative options visible.

Are aggregations fast enough for real-time dashboards?

Generally yes for filtered time ranges and moderate cardinality: doc values and caching make them efficient. For very large datasets or high-cardinality breakdowns, use transforms or downsampled indices, or push heavy analytics to an OLAP store such as ClickHouse.

Related Topics

References