Chapter 08 · Aggregations: Metrics, Buckets, Pipeline Aggregations, Cardinality, and Analytical Search
Metric Aggregations: min / max / avg / sum / stats / percentiles / cardinality and Approximation Tradeoffs
Learn which metric aggregations are exact, which are approximate, how percentiles and cardinality trade memory for accuracy, and how to validate analytical answers against a hand-computable AtlasMart fixture.
Learning outcomes
AtlasMart’s search page now needs revenue summaries,
unique-customer counts and latency percentiles next to search
results. The dangerous shortcut is to read every number in an
aggregation response as equally exact. Metric aggregations have
different mathematical contracts: min,
max, sum, avg and
stats reduce exact numeric values for the matched
documents, while cardinality deliberately estimates
distinct counts and percentile algorithms deliberately trade
bounded memory for quantile accuracy.
Distinguish exact simple metric reductions from approximate cardinality and percentile computations.
Calculate a hand-computable AtlasMart baseline before trusting the distributed response.
Explain precision_threshold and percentile compression as resource/accuracy controls rather than “make exact” switches.
Identify how shards, missing values, field mappings and scripts change the population being measured.
Design acceptance tests that compare invariants and tolerated approximation instead of freezing arbitrary decimals.
The reproducible examples target self-managed Elasticsearch 9.5.3 and OpenSearch 3.8.0. The mandatory aggregation exercises use free/local REST APIs and portable field types. Aggregation names often look similar across products, but exact algorithms, defaults, error metadata, scripting behavior, circuit-breaker accounting and managed-service limits must be verified per target version. Elasticsearch and OpenSearch both document cardinality as approximate and cap precision_threshold at 40,000. OpenSearch 3.8 also documents a hybrid cardinality collector whose runtime strategy can vary with memory conditions; this is not a portable tuning knob to copy into Elasticsearch.
The generation environment does not provide live Elasticsearch/OpenSearch clusters, so requests are specified as deterministic labs and expected invariants rather than represented as captured output. Run them against the pinned local course clusters, record actual response sizes/timings/error metadata, and remove only the dedicated AtlasMart Chapter 08 indices. Do not use production data, production scripts, or unbounded bucket trees for the failure exercises.
1. Start from the population, not the aggregation name
Every metric answers a question over the documents that survive the query and all enclosing bucket scopes. Before asking for an average, define the population: all orders, only paid orders, one tenant, one date range, or one nested item category. If the population is wrong, an exact sum is still analytically wrong.
| Metric | Contract | Typical risk |
|---|---|---|
| min / max | Exact extrema over collected numeric values | Missing/null semantics or wrong query population. |
| sum / avg / stats | Exact arithmetic reduction over collected values | Floating-point representation, multi-valued fields, scripts, or unexpected coercion. |
| percentiles | Estimated quantiles | Algorithm/compression choice and small-sample interpretation. |
| cardinality | Approximate distinct count | Treating the estimate as exact billing/compliance truth. |
| value_count | Count of values, not unique values | Confusing “number of values” with “number of documents” or distinct entities. |
DELETE atlasmart-orders-agg-v1
PUT atlasmart-orders-agg-v1
{
"settings": {"number_of_shards": 2, "number_of_replicas": 0},
"mappings": {
"properties": {
"order_id": {"type":"keyword"},
"customer_id": {"type":"keyword"},
"order_date": {"type":"date"},
"region": {"type":"keyword"},
"channel": {"type":"keyword"},
"status": {"type":"keyword"},
"total": {"type":"double"},
"latency_ms": {"type":"long"},
"store": {"properties":{"location":{"type":"geo_point"}}},
"items": {
"type":"nested",
"properties": {
"sku":{"type":"keyword"},
"category":{"type":"keyword"},
"qty":{"type":"integer"},
"line_total":{"type":"double"}
}
}
}
}
}
POST atlasmart-orders-agg-v1/_bulk?refresh=true
{ "index": { "_id": "o1" } }
{ "order_id":"o1","customer_id":"c1","order_date":"2026-09-01T10:00:00Z","region":"north","channel":"web","status":"paid","total":120.0,"latency_ms":80,"store":{"location":{"lat":35.72,"lon":51.41}},"items":[{"sku":"p1","category":"audio","qty":1,"line_total":100.0},{"sku":"p5","category":"office","qty":1,"line_total":20.0}] }
{ "index": { "_id": "o2" } }
{ "order_id":"o2","customer_id":"c2","order_date":"2026-09-01T12:00:00Z","region":"south","channel":"mobile","status":"paid","total":80.0,"latency_ms":160,"store":{"location":{"lat":29.61,"lon":52.53}},"items":[{"sku":"p3","category":"audio","qty":1,"line_total":80.0}] }
{ "index": { "_id": "o3" } }
{ "order_id":"o3","customer_id":"c1","order_date":"2026-09-02T08:30:00Z","region":"north","channel":"web","status":"refunded","total":60.0,"latency_ms":250,"store":{"location":{"lat":35.69,"lon":51.39}},"items":[{"sku":"p6","category":"sports","qty":1,"line_total":60.0}] }
{ "index": { "_id": "o4" } }
{ "order_id":"o4","customer_id":"c3","order_date":"2026-09-02T13:20:00Z","region":"west","channel":"store","status":"paid","total":200.0,"latency_ms":110,"store":{"location":{"lat":34.80,"lon":48.51}},"items":[{"sku":"p8","category":"audio","qty":1,"line_total":160.0},{"sku":"p7","category":"accessories","qty":2,"line_total":40.0}] }
{ "index": { "_id": "o5" } }
{ "order_id":"o5","customer_id":"c4","order_date":"2026-09-03T09:15:00Z","region":"north","channel":"mobile","status":"paid","total":150.0,"latency_ms":95,"store":{"location":{"lat":36.26,"lon":59.62}},"items":[{"sku":"p4","category":"audio","qty":1,"line_total":130.0},{"sku":"p5","category":"office","qty":1,"line_total":20.0}] }
{ "index": { "_id": "o6" } }
{ "order_id":"o6","customer_id":"c2","order_date":"2026-09-03T18:40:00Z","region":"south","channel":"web","status":"cancelled","total":40.0,"latency_ms":310,"store":{"location":{"lat":31.90,"lon":54.36}},"items":[{"sku":"p7","category":"accessories","qty":2,"line_total":40.0}] }
2. Prove the exact metrics first
GET atlasmart-orders-agg-v1/_search
{
"size": 0,
"aggs": {
"min_total": {"min":{"field":"total"}},
"max_total": {"max":{"field":"total"}},
"avg_total": {"avg":{"field":"total"}},
"sum_total": {"sum":{"field":"total"}},
"total_stats": {"stats":{"field":"total"}},
"latency_pct": {"percentiles":{"field":"latency_ms","percents":[50,95,99]}},
"unique_customers": {"cardinality":{"field":"customer_id","precision_threshold":100}}
}
}
For the six-document fixture, calculate these BEFORE running the API:
- total values: 120, 80, 60, 200, 150, 40
- min(total) = 40
- max(total) = 200
- sum(total) = 650
- avg(total) = 650 / 6 = 108.333...
- stats.count = 6
- exact unique customers = 4 (c1,c2,c3,c4)
Then compare API output.
The simple metrics should match exactly for this fixture.
The cardinality result may also happen to equal 4, but the aggregation's contract is approximate.
Percentiles are estimates: validate ordering/range properties, not a hand-invented frozen decimal.
For the simple metrics, compare the response to the hand calculation. For cardinality, record both the exact fixture answer and the returned estimate. A matching value on six documents proves the fixture, not that the algorithm becomes exact on large production sets.
3. Cardinality is intentionally approximate
Both products use a HyperLogLog++-style cardinality estimator.
The point is bounded memory as distinct values grow, not exact
set materialization. precision_threshold raises the
range over which counts are expected to remain close to
accurate, but it does not convert the algorithm into an exact
DISTINCT operation. The documented maximum is 40,000; larger
values do not keep buying precision.
“The fixture returned 4, so cardinality is exact.” That is a property of this tiny test instance, not the API contract.
For invoices, quotas, legal counts, uniqueness enforcement or money movement, use a system/process with the required exactness semantics. For faceting, dashboards and exploratory analytics, an approximate distinct count is often a better resource trade.
4. Percentiles summarize a distribution; they do not reveal it
p50, p95 and p99 are quantile estimates. They answer “roughly
what value bounds this percentage of observations?” They do not
tell you the number of samples, multimodality, outliers above
p99, or service-level error budget. Always retain
count and, for operational latency work, consider
histograms or raw telemetry systems when stronger quantile
semantics are required.
GET atlasmart-orders-agg-v1/_search
{
"size": 0,
"aggs": {
"default_pct": {"percentiles":{"field":"latency_ms","percents":[50,95,99]}},
"higher_compression": {
"percentiles": {
"field":"latency_ms",
"percents":[50,95,99],
"tdigest":{"compression":200}
}
}
}
}
Do not assert that the second request is “more correct” for every workload. It spends a different resource budget. Evaluate on representative data and record the algorithm/settings with the result.
5. Missing, multi-valued and scripted inputs change the measurement
An aggregation reads field values, not your business vocabulary.
A missing total, a multi-valued numeric field, or a
runtime/scripted value can change count and cost. Verify
mappings with _field_caps/_mapping,
define missing-value policy explicitly, and avoid turning
expensive per-document scripts into the default analytics path
merely because they make a prototype convenient.
GET atlasmart-orders-agg-v1/_field_caps?fields=total,latency_ms,customer_id,order_date
GET atlasmart-orders-agg-v1/_mapping
6. Acceptance tests: exact where exact, bounded where approximate
Exact fixtures:
assert min_total == 40
assert max_total == 200
assert sum_total == 650
assert abs(avg_total - 108.3333333333) < numeric_tolerance
assert stats.count == 6
Approximate fixtures:
exact_unique_customers = 4
observed_cardinality = response.aggregations.unique_customers.value
assert abs(observed_cardinality - exact_unique_customers) <= approved_small_fixture_tolerance
Percentiles:
assert p50 <= p95 <= p99
assert min_latency <= p50 <= p99 <= max_latency
record algorithm/settings/version
All analytics:
assert response._shards.failed == 0
record took, response bytes, shard count, dataset size
Check your understanding
- Why can cardinality return the exact-looking answer on a small fixture and still be approximate?
- What must be defined before computing avg(total)?
- Does raising precision_threshold make cardinality exact?
- Why is p99 alone insufficient for an SLO investigation?
- What should exact metric tests use as an oracle?
Review the answers
1. The estimator can coincide with the true count on a given dataset; the API contract still uses an approximate algorithm designed for bounded memory.
2. The document population/query/bucket scope and the field/missing-value semantics.
3. No. It changes the memory/accuracy tradeoff within documented limits; it is still an estimator.
4. It hides sample count, tail values beyond p99, distribution shape and failure context.
5. A small deterministic fixture with independently hand-computed values.
Production judgment
Use metric aggregations when their mathematical contract matches the decision. Keep exact contractual accounting in systems designed for exact accounting; use search-cluster metrics for interactive analysis when freshness and approximate summaries are acceptable. Measure query latency, heap/circuit-breaker pressure and response size with realistic cardinality rather than extrapolating from six documents.
Summary and next step
You can now separate exact reductions from approximate
estimators and validate both deliberately. The next lesson
groups documents into buckets, where distributed candidate
selection, size/shard_size and
response-tree growth create a second class of analytical
accuracy and resource tradeoffs.
Authoritative references
- Elastic aggregations overview — Official aggregation concepts and API entry point.
- Elastic terms aggregation — Shard candidate collection, shard_size and document-count error behavior.
- Elastic cardinality aggregation — HyperLogLog++ approximation and precision_threshold tradeoffs.
- Elastic percentiles aggregation — Approximate percentile calculation and algorithm controls.
- Elastic composite aggregation — Deterministic bucket pagination with after_key and early-termination guidance.
- Elastic pipeline aggregations — Pipeline categories, bucket paths, derivatives and scripts.
- OpenSearch aggregations overview — Metric, bucket and pipeline aggregation structure and resource considerations.
- OpenSearch terms aggregation — Terms size/shard_size behavior and warnings about inaccurate ascending-count ordering.
- OpenSearch cardinality aggregation — Approximate distinct counts, precision_threshold and collector behavior.
- OpenSearch percentile aggregation — Approximate percentiles and TDigest controls.
- OpenSearch composite aggregation — Composite sources and after-key pagination.
- OpenSearch pipeline aggregations — Supported pipeline aggregations including moving_fn, derivative and bucket_script.