Lesson 0007 · Query DSL

Aggregations fundamentals

From “which documents match?” to “what is the shape of my data?”

~11 minWarm-up + quiz + labCheat sheet: here

Warm-up — recall from 0003

From memory: a string mapped as text is analyzed into many tokens; a keyword is stored as one exact term. Which of the two do you sort and aggregate on?keyword. Hold that: it's the number-one aggregation footgun, and it comes straight from Lesson 0003.

A second question over the same result set

Everything so far answered “which documents match?”. Aggregations answer a different question — “what are the counts, averages, and groupings across those documents?” — in the same request. The query selects a document set; the aggs summarize that set. Aggregations reference

Because aggregations run over the query-matched documents, you compose them with everything from Lesson 0006: filter to a subset with bool, then summarize it. Set the top-level "size": 0 to skip the hits and return only the aggregations — the common shape for dashboards. Aggregations reference

Two families you'll use constantly

FamilyAnswersProducesExamples
metric“what's the number?”a value over the docsavg, sum, min/max, stats, cardinality
bucket“what are the groups?”buckets, each with a doc_countterms, range, histogram, date_histogram

A metric “calculates metrics, such as a sum or average, from field values.” A bucket “groups documents into buckets… based on field values, ranges, or other criteria,” each bucket being “a collection of documents that meet certain criteria.” Aggregations reference · Definitive Guide: Buckets (A third family, pipeline, feeds on other aggregations' output rather than documents — park it for later.)

The power move: nest a metric inside a bucket

Buckets alone give you counts. The real analysis comes from putting an aggregation inside a bucket's aggs“buckets can also be nested inside other buckets, giving you a hierarchy,” and metrics can be computed per bucket. This is drill-down: Definitive Guide: Buckets

"aggs": {
  "by_status": { // bucket: one bucket per status
    "terms": { "field": "status" },
    "aggs": {
      "avg_views": { "avg": { "field": "views" } } // metric, per bucket
    }
  }
}

Read it as: “group by status; within each group, average views.” The same nesting continues — a bucket inside a bucket inside a bucket — to slice by status, then by year, then average views, all in one pass.

The terms aggregation: two things that trip people up

terms is the aggregation you'll reach for most — one bucket per unique value, each with a doc_count. Two realities to internalize:

1 · Aggregate on keyword, not text

“By default, you cannot run a terms aggregation on a text field. Use a keyword sub-field instead.” A text field is analyzed into tokens (0003), so bucketing it would count word fragments, not values — and it's disabled unless you turn on costly fielddata. terms aggregation

2 · The counts are approximate

Default size is 10 (top ten buckets). Because each shard picks its own top terms before results merge, doc_count values for a terms aggregation may be approximate.” The response tells you how much to worry via doc_count_error_upper_bound and sum_other_doc_count. terms aggregation (Same per-shard-then-merge shape as BM25's IDF in 0002 — distribution makes exactness expensive.)

Check yourself

From memory — effortful recall is the point. Feedback is immediate.

A metric aggregation produces:

Metric aggs (avg, sum, cardinality…) compute a value from field values across the docs. Grouping into buckets is the bucket family's job.

A bucket aggregation groups documents by:

A bucket is a collection of documents meeting a criterion — a value, a range, a date interval. Each bucket carries a doc_count.

Aggregations are computed over:

The query selects the scope; aggs summarize exactly that set. With no query it's all docs; with a bool it's the filtered subset.

To compute a metric per bucket, you:

Put the metric in the bucket aggregation's own aggs. Nesting is what turns counts into drill-down analysis.

A terms aggregation should run on:

Aggregate on keyword (one exact term). A text field is tokenized, so it's disabled for aggs by default and would bucket word fragments.

Hands-on: scope, bucket, nest — then hit the keyword wall

On your live cluster. Seed a tiny index:

PUT /lab_aggs
{ "mappings": { "properties": {
  "title":  { "type": "text" },
  "status": { "type": "keyword" },
  "year":   { "type": "integer" },
  "views":  { "type": "integer" }
}}}

POST /lab_aggs/_bulk
{"index":{"_id":1}}
{"title":"elasticsearch tuning","status":"published","year":2021,"views":900}
{"index":{"_id":2}}
{"title":"elasticsearch scaling","status":"published","year":2022,"views":1500}
{"index":{"_id":3}}
{"title":"elasticsearch basics","status":"draft","year":2023,"views":300}
{"index":{"_id":4}}
{"title":"postgres tuning","status":"published","year":2019,"views":200}

1 · A metric over a query scope (with size: 0)

GET /lab_aggs/_search
{
  "size": 0,
  "query": { "match": { "title": "elasticsearch" } },
  "aggs": { "avg_views": { "avg": { "field": "views" } } }
}

Predict first: the query scopes to docs 1–3, so avg_views = (900+1500+300)/3 = 900, and hits is empty because size:0.

2 · Bucket + nested metric (drill-down)

GET /lab_aggs/_search
{
  "size": 0,
  "aggs": {
    "by_status": {
      "terms": { "field": "status" },
      "aggs": { "avg_views": { "avg": { "field": "views" } } }
    }
  }
}

Predict the buckets: published (doc_count 3, avg of 900/1500/200 ≈ 867) and draft (doc_count 1, avg 300). Note doc_count_error_upper_bound and sum_other_doc_count in the response — both 0 on this tiny index, but that's where approximation would show.

3 · Hit the wall on purpose

GET /lab_aggs/_search
{ "size": 0, "aggs": { "by_title": { "terms": { "field": "title" } } } }   // title is text → error

Read the error — “Fielddata is disabled… Text fields are not optimised for operations that require per-document field data like aggregations and sorting.” That's the 0003 rule enforced by the engine. Then DELETE /lab_aggs and bring the response to your teacher.

Primary source — read this next

Aggregations reference (Elastic Docs, current) for the family overview, and the terms aggregation page for the approximation and size/shard_size mechanics. Current-version — syntax is safe to copy.