Aggregations
Aggregations run over the whole match set, not the page of hits you asked for. Set size: 0 when you want the summary and not the documents.
Group by a field
A terms aggregation needs a field it can group on: keyword, numeric, date or boolean. On a text field use the .keyword sub-field — see indexing.
await es.search({
index: 'shop.products', size: 0,
aggs: { by_brand: { terms: { field: 'brand' } } }
})Metrics
avg, sum, min, max, value_count, stats and percentiles are answered, on their own or inside a bucket.
aggs: {
avg_price: { avg: { field: 'price' } },
spread: { percentiles: { field: 'price' } },
by_brand: { terms: { field: 'brand' },
aggs: { avg_price: { avg: { field: 'price' } } } }
}missing on a metric aggregation asks us to substitute a value for documents that do not have the field. We refuse it rather than fold in a number you did not choose, because the result would look like data and be an assumption.
Bucket by time
A date_histogram groups documents into calendar buckets. The field can hold an ISO-8601 string or epoch milliseconds — both bucket the same way. Buckets are UTC and calendar-aligned, so pass calendar_interval.
aggs: { per_day: { date_histogram: {
field: 'placed_at',
calendar_interval: 'day', // minute hour day week month quarter year
time_zone: 'UTC'
} } }fixed_interval is refused: an arbitrary fixed width has no calendar-aligned equivalent here, and approximating it would put your documents in the wrong buckets.
Named filter buckets
One bucket per named query, counting the documents that match both your search and that filter, with sub-aggregations over exactly those documents. other_bucket_key adds a bucket for everything that matched none of them.
aggs: { by_price: {
filters: {
other_bucket_key: 'mid_range',
filters: {
clearance: { range: { price: { lt: 50 } } },
premium: { range: { price: { gte: 200 } } }
}
},
aggs: { avg_price: { avg: { field: 'price' } } }
} }A filters aggregation is answered with one search per filter, which is a different shape of work from the single pass every other aggregation takes. Combining it with other top-level aggregations in one request is refused, so that a request that looks like one search cannot quietly become several.
The documents behind a bucket
top_hits returns the best matching documents with their _source. It works at the top level, or nested so each bucket carries its own examples.
// three best matches overall
aggs: { best: { top_hits: { size: 3 } } }
// one example per brand, most expensive first
aggs: { by_brand: { terms: { field: 'brand' },
aggs: { top: { top_hits: { size: 1, sort: [{ price: 'desc' }] } } } } }Pipelines
cumulative_sum and derivative post-process the buckets of the aggregation they sit in — a running total or a rate of change over a date histogram.
aggs: { per_day: { date_histogram: { field: 'placed_at', calendar_interval: 'day' },
aggs: {
revenue: { sum: { field: 'total' } },
running: { cumulative_sum: { buckets_path: 'revenue' } }
} } }What is not answered
- extended_stats, composite and nested have no executor yet and are refused rather than partially answered.
- An aggregation that names no field is refused — except a bare top_hits, which legitimately needs none.
- A field that was never indexed comes back as an empty aggregation with your hits intact, not as a failed search.
Elasticsearch is a trademark of Elasticsearch B.V., registered in the U.S. and in other countries. OriginChainDB is not affiliated with, endorsed by, or sponsored by Elasticsearch B.V. OriginChainDB implements a compatible HTTP API so that existing Elasticsearch clients can talk to it; it does not distribute Elasticsearch software.