How do you sort and page Elasticsearch aggregation buckets by a computed value?
answer
- Compute first, then order
- terms order cannot see pipeline values
- One pipeline both sorts and truncates
- It only reorders what came back
- No sort clause means pure truncation
basics
~20 sCompute the value per bucket with bucket_script, then order the buckets with bucket_sort, which also takes from and size. A terms aggregation's own order clause cannot reference a pipeline aggregation, so bucket_sort is the mechanism.
solid answer
~50 sCompute the value with a `bucket_script` parent pipeline — a `buckets_path` map plus a Painless script, giving each bucket a derived number such as a conversion or error rate. Then order by it with `bucket_sort`, which takes a `sort` array over `buckets_path` values (plus `_key` and `_count`), and optionally `from` and `size`. A `terms` aggregation's own `order` clause cannot reference a pipeline aggregation, which is precisely why `bucket_sort` exists; with no `sort` at all it degenerates into a pure truncation of the parent's bucket list. The caveat is the same one that bites `bucket_selector`: `bucket_sort` reorders only the buckets the parent already returned. If `terms` picked its top 10 by document count, sorting those 10 by error rate gives the worst of *those ten*, not the worst in the index. Widening `size`/`shard_size` widens the candidate set at a memory cost; `from`/`size` here is windowing within one response, not deep paging over all buckets.
code
json · 25 lines{
"size": 0,
"aggs": {
"by_seller": {
"terms": { "field": "seller_id", "size": 500 },
"aggs": {
"orders": { "sum": { "field": "order_count" } },
"returns": { "sum": { "field": "return_count" } },
"return_rate": {
"bucket_script": {
"buckets_path": { "o": "orders", "r": "returns" },
"script": "params.o == 0 ? 0 : params.r / params.o"
}
},
"worst_first": {
"bucket_sort": {
"sort": [ { "return_rate": { "order": "desc" } } ],
"from": 0,
"size": 10
}
}
}
}
}
}go deeper
Recall that bucket_script adds a computed number to each bucket and bucket_sort orders the buckets by it, including an optional from and size.
Explain why the terms order clause cannot reference a pipeline value, and that bucket_sort without a sort clause is a pure truncation of the parent's bucket list.
Show the correctness judgment: bucket_sort ranks only the candidate set terms already chose, so a top-N by computed rate is approximate unless the candidate set is widened deliberately.
Own the framing for the business: state the report as 'worst rate among the N busiest' or pay for a wider candidate set or an out-of-cluster ranking, and make that choice explicit rather than implicit.
## The two pieces **`bucket_script`** is a parent pipeline that gives each bucket a new numeric value computed from other values in that bucket. It takes `buckets_path` as a map of variable names to paths and a Painless `script` that reads them as `params`: ```json "return_rate": { "bucket_script": { "buckets_path": { "o": "orders", "r": "returns" }, "script": "params.o == 0 ? 0 : params.r / params.o" } } ``` Guarding against a zero denominator matters: an unguarded division produces infinity or a null and then sorts to a corner of the result. **`bucket_sort`** is a parent pipeline that reorders — and optionally truncates — the bucket list of the aggregation it sits in. It takes a `sort` array whose entries are paths to bucket values (a metric, a `bucket_script` output, `_key`, `_count`), each with an `order`, plus `from`, `size` and `gap_policy`. ## Why terms `order` is not enough A `terms` aggregation can order its buckets by `_count`, by `_key`, or by a single-value metric sub-aggregation. It **cannot** order by a pipeline aggregation. The reason is structural: `terms` selects and orders its buckets while the aggregation is being computed and merged, whereas pipeline aggregations run afterwards over the finished result. A value that does not exist yet cannot drive the selection that produces it. `bucket_sort` closes the loop by re-sorting after the fact. ## Ordering versus selecting: the correctness caveat This is the part interviewers are really testing. `bucket_sort` **re-sorts what it was given**. The pipeline runs after the source aggregation has already decided which buckets exist. With a `terms` aggregation, that decision was made by the aggregation's own `order` and `size`: 1. Each shard returns its top `shard_size` terms by the terms ordering. 2. The coordinating node merges and keeps the top `size`. 3. `bucket_script` computes the rate for those buckets. 4. `bucket_sort` ranks those buckets by the rate. A seller with a catastrophic return rate and a modest order count never enters at step 1 or 2, so no amount of sorting at step 4 finds them. The honest phrasing for a report is "the worst return rate among the 500 busiest sellers", and if the business needs the true worst, the request must change: raise `size` and `shard_size` until the candidate set plausibly contains the answer (paying memory and reduce cost), order the `terms` aggregation by a metric correlated with the target where one exists, or enumerate every bucket with a paging aggregation such as `composite` and rank outside Elasticsearch. ## from and size `bucket_sort` accepts `from` and `size`, which window the sorted bucket list. This looks like paging and is useful for serving page 2 of a table, but it is not deep paging: every page still requires the source aggregation to build the same candidate set, and moving `from` deeper does not reach buckets outside it. For genuine enumeration of a large bucket space, a paging bucket aggregation is the right tool. Used with `size` alone and no `sort`, `bucket_sort` is a pure truncation — a compact way to say "keep the first N buckets of this histogram" without changing their order. That idiom is common for trimming long time series before returning them. ## Cost and placement Both aggregations are parent pipelines and must be declared inside the multi-bucket aggregation whose buckets they operate on. Both run in the reduce phase on the coordinating node, so their cost is per bucket, not per document: script compilation once, then a cheap evaluation per bucket, then a sort. On a bucket set of a few hundred this is negligible; on tens of thousands of buckets across many script stages it becomes real coordinating-node CPU, which is a reason to keep bucket counts bounded rather than to avoid the pipelines. ## Putting it together A typical "worst offenders" panel is: a `terms` aggregation with a deliberately generous `size`, two `sum` metrics inside it, a `bucket_script` turning them into a ratio, optionally a `bucket_selector` dropping buckets with too few documents to be meaningful, and a `bucket_sort` ranking by the ratio and keeping the top ten. Every stage runs after the previous one on the merged result, which is why the whole chain inherits the candidate-set limitation of the very first step.
- Why can't a terms aggregation's order clause sort by a bucket_script value?Because `terms` orders and truncates its buckets while the aggregation is computed and merged, and pipeline aggregations run afterwards on the finished result. The value simply does not exist at the moment selection happens. `terms` can order by `_count`, `_key` or a single-value metric sub-aggregation; ordering by a computed value requires `bucket_sort` after the fact.
- Is bucket_sort's from and size a way to page through all buckets of a large terms aggregation?No. It windows the bucket list the parent already produced, so every page rebuilds the same candidate set and no page reaches beyond it. It is fine for serving page two of a small table, but genuine enumeration of a large bucket space needs a paging bucket aggregation rather than a pipeline window.
- How do you keep a ratio computed by bucket_script from being dominated by tiny buckets?Guard the script against a zero denominator, then add a `bucket_selector` requiring a minimum volume — for example `params.orders >= 100` — before the `bucket_sort` runs, or use the `terms` aggregation's own `min_doc_count`. Without a floor, a seller with two orders and one return outranks everyone at a 50% rate, which is noise rather than signal.
saying these in an interview costs you the question
- Puts a pipeline aggregation path in the terms order clause
- Believes bucket_sort finds the global top by the computed value
- Treats bucket_sort from/size as deep paging over all buckets
- Divides in bucket_script without guarding a zero denominator
- Ranks by a ratio with no minimum-volume floor