Course contents
Theory course · 5 / 6

Aggregations

Aggregations summarize matching documents. Bucket aggregations form groups; metric aggregations calculate values. Use size: 0 when only statistics are needed.

5 sectionsElasticsearch 9.4.3Not read yet

What you will learn

  • Distinguish document pages, buckets and metrics.
  • Calculate counts and numeric summaries over the correct population.
  • Understand global filters and nested aggregation scope.

Aggregations answer questions about groups of matching documents. A bucket aggregation defines groups; a metric aggregation computes a value inside its current group. The top-level query restricts the population used by all ordinary aggregations. size: 0 suppresses returned hits but does not stop aggregation calculation.

Aggregation names are chosen by the request author and appear as keys in the response. Use the names required by a task because they are part of the answer contract. In this trainer both aggs and aggregations are supported, but the maximum nesting is three aggregation levels and the bucket budget is 1000.

Each metric has a population
  • Query matches
    • Category A
      • avg(price) in A
    • Category B
      • avg(price) in B

A child metric sees only documents inherited from its parent bucket. Parallel siblings do not filter one another.

Examples use the public products dataset on Elasticsearch 9.4.3. Responses are shortened JSON fragments: timing, shard and index metadata are omitted. Numeric scores are illustrative and can vary. Examples teach the method; exercise values may differ.

01 / 05

Count 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

POST /products/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "category",
        "size": 10
      }
    }
  }
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 21
02 / 05

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

POST /products/_search
{
  "size": 0,
  "query": {
    "term": {
      "category": "Audio"
    }
  },
  "aggs": {
    "audio_average": {
      "avg": {
        "field": "price"
      }
    }
  }
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 22
03 / 05

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

POST /products/_search
{
  "size": 0,
  "query": {
    "term": {
      "category": "Accessories"
    }
  },
  "aggs": {
    "lowest": {
      "min": {
        "field": "price"
      }
    },
    "highest": {
      "max": {
        "field": "price"
      }
    }
  }
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 23
04 / 05

Aggregate in-stock products

A filter aggregation creates a single bucket containing documents that pass its query. Only its child aggregations inherit that restriction. In this example total hits still includes every product, but out_of_stock contains unavailable products and its average describes only those products.

A top-level query would restrict all aggregations and the search result itself. Choose the scope from the business question: one local comparison needs a filter bucket; one shared restriction for everything belongs in query. filter buckets expose doc_count, which helps verify that a metric used the intended population.

Try a different scenario

POST /products/_search
{
  "size": 0,
  "aggs": {
    "out_of_stock": {
      "filter": {
        "term": {
          "in_stock": false
        }
      },
      "aggs": {
        "average_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
Elasticsearch response
Shortened response · JSON
{
  "hits": {
    "total": {
      "value": 300,
      "relation": "eq"
    },
    "hits": []
  },
  "aggregations": {
    "out_of_stock": {
      "doc_count": 100,
      "average_price": {
        "value": 724.3
      }
    }
  }
}

How to read this result

Compare hits.total.value with out_of_stock.doc_count. They describe different populations because the restriction is local to the aggregation.

Apply this in exercise 24
05 / 05

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

POST /products/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "average_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
Elasticsearch response
Shortened response · JSON
{
  "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.

Apply this in exercise 25

Key takeaways

  • Root size controls hits; terms.size controls buckets.
  • Use keyword for exact-value buckets and numeric fields for numeric metrics.
  • query restricts every ordinary aggregation; filter aggregation restricts its descendants.
  • A metric belongs inside the bucket whose documents it summarizes.
  • An empty numeric metric is null, not zero.

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.