Composite aggregation #
Paginated multi-source bucket aggregation: the composite key is a tuple
built from several sources (a terms field, a histogram interval, a
date_histogram, a geotile_grid), and pages are pulled with an
after cursor. It does what a single
terms cannot: group
by several dimensions at once, and stream all combinations without
oversizing size.
How it works #
Each entry in sources is named: { "day": { … } } binds day to one
dimension of the composite key. A bucket’s key maps each source name
to its value; buckets come back in ascending key order — first source
most significant, values compared as numbers when both are (numbers
sort before strings), as strings otherwise.
There is no doc_count ordering and no truncation: every combination
occurring in the matched documents is materialized, and size only
slices the sorted list into pages — the full set is always reachable
by cursor, where terms keeps its top-size buckets and drops the
rest.
A page carries an after_key, the key of its last bucket. Pass it
back as after and the next page holds only buckets sorting strictly
greater. A short final page still carries an after_key; the walk ends
when a page comes back empty.
Per document: a terms source over a multi-valued field yields one
value per element, so one document can land in several buckets; a
document missing a value for any source is dropped. histogram keys
are floor(v / interval) * interval, date_histogram keys are epoch
milliseconds (offset/time_zone shift boundaries), geotile_grid
keys are map tiles; sub-aggregations run only for the returned page.
SQL GROUP BY … LIMIT plans lower through composite — LIMIT becomes
size, one source per grouping key.
Examples #
Page through merchant combinations, five buckets at a time — the SQL
side is GROUP BY … LIMIT plus the envelope’s keyset cursor:
POST /sales/_search
{
"aggs": {
"matrix": {
"composite": {
"size": 100,
"sources": [
{ "merchant": { "terms": { "field": "merchant" } } },
{ "day": { "date_histogram": { "field": "date", "calendar_interval": "day" } } }
],
"after": { "merchant": "shop_b", "day": 1700000000000 }
}
}
}
}
Use cases #
- Cross-tab reports.
salescrossedtype × statuslands in 9 of 12 possible cells (mugwas neverpending,stickeronly everpending); nestsum(amount)for revenue per cell. - Export the full bucket space.
termscuts to its topsize; composite walks everything — loopafteruntil the empty page, five rows at a time over the 9 cells above. - Deep pagination.
afterwalks high-cardinality keys page by page. - Time × dimension matrices.
date_histogramby day plustermsontypegives one bucket per day-and-type that occurred across the 120 sales (2024-01-01 through 2025-04-21).
Parameters for composite
#
size
(Optional, integer, default:10) Maximum number of composite buckets to return per page.sources
(Required, array) Named sources, one per dimension of the composite key, as{ "<name>": { "<type>": { … } } };<type>is one of:terms— distinct values of afield(anorderis accepted; keys always come back ascending).histogram— numeric buckets of a fixed numericinterval.date_histogram—calendar_interval(minute…year) orfixed_interval(e.g."30m"; the legacyintervalstill works), plusoffset,time_zone,format. Keys are epoch milliseconds.geotile_grid— map tiles atprecision(default7, max29).
after
(Optional, object) Pagination cursor — the previous page’safter_key.aggs
(Optional, object) Nested sub-aggregations, computed per bucket.
Related #
terms— top-N buckets bydoc_count, no cursormulti_terms— tuple keys from several fieldsdate_histogram— the time source, standalonegeotile_grid— the map-tile source, standalone