For AI agents: this page is also available as Markdown at /docs/facets-and-aggregations.md. Every docs page is: add .md to its URL. llms.txt lists them all.
Facets and aggregations
Facets group the documents a query matches into buckets and count them: "Science fiction (31)", "Under $20 (18)", "May (240)". Metrics reduce them to numbers: average price, highest latency. Both see every matching document, not just the page of hits you asked for, and both come back in the same response, from the same consistent view of the collection. Counts are exact.
You can describe them in the native request shown here, or in the language you already use:
Elasticsearch aggs, Solr facets, SQL GROUP BY, or LogQL metric queries. They all produce the same
kinds of buckets. See Query languages.
#Facets in a request
Add a facets object to any search request. Each key is a name you choose, and it comes back
unchanged in the response.
curl -X POST https://api.warpsearch.io/v1/collections/books/search \
-H "Authorization: Bearer $WARPSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"language": "lucene",
"query": "title:dune",
"limit": 5,
"facets": {
"by_genre": {"type": "terms", "field": "genre", "size": 10,
"metrics": {"avg_price": "avg(price)"},
"order": {"by": "avg_price", "desc": true}},
"per_decade": {"type": "range", "field": "year", "start": 1900, "end": 2030, "interval": 10},
"price_tiers": {"type": "query",
"buckets": {"cheap": "price:<10", "pricey": "price:>=10"}}
},
"metrics": {"max_price": "max(price)"}
}'
{
"total": 42,
"hits": [],
"facets": {
"by_genre": {
"type": "terms",
"field": "genre",
"missing": 2,
"buckets": [
{"value": "scifi", "count": 31, "metrics": {"avg_price": 11.7}},
{"value": "classic", "count": 9, "metrics": {"avg_price": 8.2}}
]
}
},
"metrics": {"max_price": 24.5}
}
Hits arrive as usual, so a results page and its sidebar need one request. Set limit to 0 when you
only want the analytics.
#Facet types
type |
Groups by | Main options |
|---|---|---|
terms |
Each distinct value of a field. | size (number of buckets), min_count, missing (also count documents with no value), order |
range |
Fixed-width numeric buckets. | field, start, end, interval |
date_histogram |
Time buckets. | interval (a number, a duration such as 5m, or auto) or calendar_interval (day, week, month, quarter, year), time_zone, format |
query |
Named buckets that you define with queries. | buckets: a map of bucket name to query |
Every facet also accepts metrics and facets, which run once per bucket. That is how you ask for
"the average price and the top publishers of every category" in one request: a terms facet with a
metrics object and a nested facets object.
"by_category": {
"type": "terms", "field": "category", "size": 10,
"metrics": {"avg_price": "avg(price)"},
"facets": {"publishers": {"type": "terms", "field": "publisher", "size": 5}}
}
Notes on terms:
- Use it on
keywordand number fields. On atextfield it returns individual analyzed words, not whole values, so facet on akeywordsub-field instead. See Schemas and field types. - A multi-valued field counts a document in each of its values, never twice in the same bucket.
orderis by count by default. Set{"by": "<metric name>", "desc": true}to order by a metric of the facet, or usebucket_order(key_desc,count_desc,count_asc).- Buckets you did not ask for are not returned, so
sizedecides how many you see.
Each bucket in the response has a value and a count, plus metrics and facets when you asked
for them. A range or date_histogram bucket also has from and to (the lower bound is included,
the upper one is not), and a date bucket has key_as_string, the formatted lower bound.
#Metrics
A metric is "function(field)" or an object. They sit at the top of the request (one number over all
matches) or inside a facet (one number per bucket).
| Function | Result | Notes |
|---|---|---|
avg, sum, min, max |
A number | Missing values are skipped. An empty set returns null. |
count |
A number | Documents with a value. |
stats |
{count, min, max, avg, sum} |
|
cardinality |
A number ({value} in Elasticsearch shape) |
Approximate distinct count. Exact up to a threshold, then about 1.6% error. Not on text fields. |
percentiles |
A map keyed by percent, such as "99.0" |
Approximate, from a t-digest. Numeric and date fields. |
percentile_ranks |
A map keyed by value | Approximate. Needs values. |
The string form uses the defaults. The three approximate functions take parameters in the object form:
"metrics": {
"p": {"func": "percentiles", "field": "latency_ms", "percents": [50, 99], "keyed": false},
"users": {"func": "cardinality", "field": "user_id", "precision_threshold": 1000},
"slow": {"func": "percentile_ranks", "field": "latency_ms", "values": [500]}
}
precision_threshold is capped at 40000. percents default to 1, 5, 25, 50, 75, 95 and 99.
compression (20 to 1000) tunes accuracy. Metric names are free-form strings and come back unchanged.
Status by API:
cardinality,percentilesandpercentile_ranksare available through the/v1metricsobject and through Elasticsearch and OpenSearch aggregations.statsand the basic functions work everywhere.
#Date histograms
"facets": {
"per_day": {"type": "date_histogram", "field": "published",
"calendar_interval": "day", "time_zone": "America/Denver"}
}
Calendar intervals follow the civil calendar: months vary in length and a daylight-saving day has 23
or 25 hours. Fixed intervals such as 5m or 1h are exact durations. By default the histogram runs
from the first non-empty bucket to the last, with empty buckets between. To widen it, set
extended_start and extended_end, and trim_empty to drop empty edge buckets outside them.
Giving the histogram explicit bounds, for example through a date range in the query or
time_range, saves the engine a pass to find them.
Limits: a histogram returns at most 5000 buckets, and at most 1024 when it has metrics or nested
facets. A wider span fails with limit_exceeded, and the fix is a bigger interval or a narrower
range.
#Faceted navigation
For a sidebar where users tick several values and still see their alternatives, count each facet without its own selection:
- In the Elasticsearch language, put the user's choices in
post_filter. It narrows the hits and leaves the aggregations untouched. - In Solr, tag the filter query and exclude it in the facet:
fq={!tag=g}genre:scifiwithfacet.field={!ex=g}genre. - In the native request, send the selected values in
filtersfor the hits and run a second request without them for the counts of the facet in question.
#Other languages
| You write | You get |
|---|---|
Elasticsearch aggs: terms, histogram, date_histogram, range, date_range, filters, filter, global, top_hits, and the metrics avg, sum, min, max, value_count, stats, extended_stats, cardinality, percentiles, percentile_ranks |
The same buckets, in Elasticsearch shape. |
Solr facet.field, facet.range, facet.query, facet.pivot, stats, and the JSON Facet API |
Solr-shaped facet results. |
SQL GROUP BY a, b, COUNT(*), AVG, SUM, MIN, MAX, HAVING COUNT(*) >= n, SELECT DISTINCT col |
Nested terms facets. |
LogQL count_over_time, sum by (...), avg_over_time |
A date histogram per series. |
Terms ordering by a cardinality or percentiles sub-aggregation, scripts, terms include and
exclude, and hdr percentiles are not supported. Unsupported pieces answer 400 unsupported_feature, and approximations come back as warnings; see
Limits and errors.
#Plan limits and cost
Your plan caps how many facets a request may carry and how many buckets each returns, in addition
to the fixed histogram limits above. A size of -1 is capped at your plan's limit rather than
unbounded. Facets over a high-cardinality field with nested facets are the most expensive shape, so
filter first and keep size small. Bound date histograms with a time range; see
Pricing.
#Gotchas
- Counts are of documents, except on multi-valued fields, where each value is counted.
- A facet on a field that no document has returns no buckets, and
missingcounts the rest. - Numeric
termsfacets enumerate distinct values. For bands of numbers, userange.