Build aggregation trees and identify each calculation’s document scope.
course-products examples use the teaching mapping and product from chapter 5, “Mapping and field types”. Exercise walkthroughs below use the trainer’s separate public dataset.
- Calculation scope
- Metrics and intervals
- Names and nesting
Build aggregation trees and identify each calculation’s document scope.
The course targets Elasticsearch 9.4.3. Exercise examples use the public products dataset; other requests illustrate separate teaching indices and may require setup or privileges unavailable in the trainer. Responses are shortened fragments; scores may vary. Check deployment and license requirements for advanced features.
Calculation scope
Aggregations operate on query-matching documents independently of returned hit count. size=0 suppresses hits, not calculations. Bucket aggregations group documents and metric aggregations calculate values. Sub-aggregations see their parent bucket’s documents, so a brand’s average price differs from the catalog average.
Check your understandingMetrics and intervals
sum, avg, min, max and stats operate on numeric values; missing and multivalued fields need care. Range uses explicit boundaries; histogram uses numeric intervals. date_histogram distinguishes calendar_interval from fixed_interval: a calendar day is not always 24 hours across clock changes. Time zone is part of the analytical definition, not a display detail.
Check your understandingNames and nesting
Aggregation names become response contract keys. Sorting hits by price does not sort buckets by average price. terms on brand creates groups and avg computes a metric within each. A nested aggregation specifically addresses nested documents; ordinary sub-aggregation nesting does not require nested mapping.
Average price is calculated independently for each selected brand bucket.
{
"size": 0,
"aggs": {
"brands": {
"terms": {
"field": "brand",
"size": 10
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
}
}
}
}
}How to read this result
Average price is calculated independently for each selected brand bucket.
Catalog reasoning example
For Sony/100, Sony/200 and Bose/50, terms on brand yields two groups. avg(price) gives Sony 150 and Bose 50. If query first restricts price<150, Sony’s average becomes 100. Setting size=1 changes hits, not those averages. Read analytical trees by identifying the document scope, grouping and metric in that order.
Check your understandingCount products by brand
A terms aggregation creates buckets for exact indexed values. Here we group by category instead of brand. Each bucket has a key and doc_count. The terms size controls the number of returned buckets; the root size controls returned documents. These are unrelated parameters with the same name.
Use keyword fields for whole-value groups, such as category or brand.keyword. Default terms ordering favors larger counts, with deterministic tie handling by key. On multi-shard production data, top terms can have approximation considerations; this trainer uses one primary shard and a size large enough to include every value in these examples.
Try a different scenario
{
"size": 0,
"aggs": {
"categories": {
"terms": {
"field": "category",
"size": 10
}
}
}
}Elasticsearch response
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"categories": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Accessories",
"doc_count": 75
},
{
"key": "Audio",
"doc_count": 75
},
{
"key": "Computers",
"doc_count": 75
},
{
"key": "Smartphones",
"doc_count": 75
}
]
}
}
}How to read this result
No hits are returned. categories.buckets lists keys and counts for the matching population, while hits.total still reports the total number of matched documents.
Average price
avg computes the mean of indexed numeric values in its current scope. The example calculates average price only for category Audio using a top-level query. The metric response contains value, not a list of products or a bucket count. Without a query it would describe the whole index.
Documents missing the metric field do not contribute a value to a standard average. If there are no usable numeric values, value is null rather than zero. Treat decimal results as numeric calculations: insignificant floating-point differences are not a change in meaning.
Try a different scenario
{
"size": 0,
"query": {
"term": {
"category": "Audio"
}
},
"aggs": {
"audio_average": {
"avg": {
"field": "price"
}
}
}
}Elasticsearch response
{
"hits": {
"total": {
"value": 75,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"audio_average": {
"value": 859.4133333333333
}
}
}How to read this result
audio_average.value summarizes every matching Audio product, not merely a first page. The metric name is chosen by this example.
Price boundaries
min and max are independent metric aggregations, so place them as siblings in aggs. The example limits the population to Accessories and asks for both price endpoints. A metric always inherits the current scope: neither sibling changes the document set of the other.
These endpoints are observed values, not the boundaries of a range filter. If a query allows price below 800, the maximum observed price can be lower than 800. Empty populations produce null metric values, so do not invent an extreme zero for an empty group.
Try a different scenario
{
"size": 0,
"query": {
"term": {
"category": "Accessories"
}
},
"aggs": {
"lowest": {
"min": {
"field": "price"
}
},
"highest": {
"max": {
"field": "price"
}
}
}
}Elasticsearch response
{
"hits": {
"total": {
"value": 75,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"lowest": {
"value": 99
},
"highest": {
"value": 2499
}
}
}How to read this result
The two value fields describe the same Accessories population. They can be read without requesting or sorting individual products.
Metrics per brand
A bucket can contain child aggs. Group categories, then calculate average_price for each category. This computes several separate averages rather than one global average copied next to every bucket. The response shape mirrors the request tree, so read it from the parent key toward its descendants.
Nesting aggregations is not the same feature as a nested query on a nested-mapped field. Our products contain ordinary scalar fields; no nested field mapping is needed for a category-to-metric tree. Keep the hierarchy within the trainer limits and estimate bucket multiplication before adding another grouping.
Try a different scenario
{
"size": 0,
"aggs": {
"categories": {
"terms": {
"field": "category",
"size": 10
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
}
}
}
}
}Elasticsearch response
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"categories": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Accessories",
"doc_count": 75,
"average_price": {
"value": 859.4
}
},
{
"key": "Audio",
"doc_count": 75,
"average_price": {
"value": 859.4133333333333
}
},
{
"key": "Computers",
"doc_count": 75,
"average_price": {
"value": 589.2
}
},
{
"key": "Smartphones",
"doc_count": 75,
"average_price": {
"value": 589.88
}
}
]
}
}
}How to read this result
Every category bucket contains its own average_price.value. The metric sits inside that bucket, which identifies its population.
Category and brand analytics
Use three levels: category buckets, then brand buckets, then numeric metric leaves. Our example analyzes active products, groups them by category and brand.keyword and computes three price metrics per pair. All metrics under a brand inherit both the category restriction and the brand restriction.
The query is outside aggs and restricts all groups. Each terms size is local to that level. At sizes 10 and 20, the conservative bucket estimate is 10 + 10 × 20 = 210, within the trainer budget of 1000. The metric leaves occupy the third level but do not multiply the number of buckets.
Read the response as categories.buckets → brands.buckets → metric.value. Never combine the metrics of different category-brand pairs unless the business question explicitly asks for a new calculation. A mean of group means is generally not the same as the overall document mean.
- query: active
- categories (level 1)
- brands (level 2)
- avg / min / max (level 3)
- brands (level 2)
- categories (level 1)
Each indentation level narrows the document population before the metrics are calculated.
Try a different scenario
{
"size": 0,
"query": {
"term": {
"status": "active"
}
},
"aggs": {
"categories": {
"terms": {
"field": "category",
"size": 10
},
"aggs": {
"brands": {
"terms": {
"field": "brand.keyword",
"size": 20
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
},
"max_price": {
"max": {
"field": "price"
}
}
}
}
}
}
}
}Elasticsearch response
{
"hits": {
"total": {
"value": 100,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"categories": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Accessories",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "apple",
"doc_count": 25,
"average_price": {
"value": 859.4
},
"min_price": {
"value": 99
},
"max_price": {
"value": 2499
}
}
]
}
},
{
"key": "Audio",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "apple",
"doc_count": 25,
"average_price": {
"value": 859.4
},
"min_price": {
"value": 99
},
"max_price": {
"value": 2499
}
}
]
}
},
{
"key": "Computers",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Sony",
"doc_count": 25,
"average_price": {
"value": 589.2
},
"min_price": {
"value": 49
},
"max_price": {
"value": 1299
}
}
]
}
},
{
"key": "Smartphones",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Sony",
"doc_count": 25,
"average_price": {
"value": 589.2
},
"min_price": {
"value": 49
},
"max_price": {
"value": 1299
}
}
]
}
}
]
}
}
}How to read this result
Every brand bucket is inside one category. Its metrics summarize only active products in that pair, even if the same brand appears elsewhere in the response.
Check your understanding
Answer before expanding each explanation.
1. Does size=0 disable aggregations?
No, it suppresses returned hits.
2. Which documents reach a sub-aggregation?
Its parent bucket’s documents.
3. Does search sort order buckets?
No, bucket ordering is configured in the aggregation.
4. Is a calendar day always 24 hours?
No, clock changes can alter its duration.
5. Do sub-aggregations require nested mapping?
No, only the dedicated nested aggregation uses that scope.
Key takeaways
- No, it suppresses returned hits.
- Its parent bucket’s documents.
- No, bucket ordering is configured in the aggregation.
- No, clock changes can alter its duration.
- No, only the dedicated nested aggregation uses that scope.
Official reference
Ready to put this into practice?
Confirm reading to save your place. Reading and exercise progress are tracked separately in this browser using your guest cookie.