Skip to content
ferroseekdocumentation homemain, packages at version v0.1.0
Ferroseek on GitHub

Aggregations and facets

Aggregations go in aggs (or aggregations) of a _search body. They are computed over the documents that match query, and they ignore post_filter, exactly as in Elasticsearch. That one rule is what makes multi-select facets possible; the pattern is below.

Aggregation Parameters
terms field, size, order, min_doc_count
range field, ranges (each with from, to, key), keyed
histogram field, interval, offset, min_doc_count, keyed, extended_bounds
date_histogram field, calendar_interval or fixed_interval, format, min_doc_count, keyed
filter A query
filters filters (an object of named queries, or an array), other_bucket, other_bucket_key
global None: all documents, ignoring query
missing field: documents without a value

Every bucket aggregation can hold sub-aggregations in its own aggs. terms buckets can be ordered by _count, by _key, or by a single-value metric sub-aggregation ({"order": {"avg_price": "desc"}}).

terms and the other bucket aggregations work on keyword, numeric, boolean and date fields. A terms aggregation on a text field is an error, with the same message Elasticsearch gives.

min, max, avg, sum, stats, value_count and cardinality, each with a field. Floating-point results match Elasticsearch to within 1e-9.

{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category", "size": 5, "order": { "avg_price": "desc" } },
"aggs": { "avg_price": { "avg": { "field": "price" } } }
}
}
}

Rejected with [ferroseek] unsupported:: the aggregation types date_range, extended_stats, percentiles, percentile_ranks, top_hits, significant_terms, composite, nested, reverse_nested, sampler, weighted_avg, multi_terms, rare_terms, auto_date_histogram, adjacency_matrix, geo_bounds, scripted_metric, bucket_sort, bucket_selector, median_absolute_deviation and variable_width_histogram; aggregation meta; on terms, include, exclude, execution_hint, collect_mode and shard_min_doc_count; on metrics, script, missing, format and value_type; on histogram, order and hard_bounds.

A listing page with facets usually wants this behaviour: choosing a brand narrows the products, but the brand facet still shows every brand (so the user can tick a second one), while the colour and price facets update to reflect the chosen brand.

In one request:

  1. Put the user’s text search and any fixed scope (such as the category being browsed) in query.
  2. Put the facet selections in post_filter. It filters the hits but not the aggregations.
  3. Wrap each facet’s aggregation in a filter aggregation that applies every other facet’s selection.

This is the request the example shop sends for “jacket”, with red and blue selected and no brand or price chosen:

examples/shop-worker/src/search-request.ts (output)
{
"query": {
"bool": {
"must": [{ "multi_match": { "query": "jacket", "fields": ["name^3", "description"], "operator": "and" } }],
"filter": []
}
},
"post_filter": { "bool": { "filter": [{ "terms": { "color": ["red", "blue"] } }] } },
"aggs": {
"brand": {
"filter": { "bool": { "filter": [{ "terms": { "color": ["red", "blue"] } }] } },
"aggs": { "values": { "terms": { "field": "brand", "size": 30 } } }
},
"color": {
"filter": { "match_all": {} },
"aggs": { "values": { "terms": { "field": "color", "size": 30 } } }
},
"price": {
"filter": { "bool": { "filter": [{ "terms": { "color": ["red", "blue"] } }] } },
"aggs": {
"ranges": {
"range": {
"field": "price",
"ranges": [
{ "key": "under-25", "to": 25 },
{ "key": "25-50", "from": 25, "to": 50 },
{ "key": "50-100", "from": 50, "to": 100 },
{ "key": "100-250", "from": 100, "to": 250 },
{ "key": "250-plus", "from": 250 }
]
}
}
}
},
"category": {
"filter": { "bool": { "filter": [{ "terms": { "color": ["red", "blue"] } }] } },
"aggs": { "values": { "terms": { "field": "category", "size": 30 } } }
}
},
"sort": [{ "_score": { "order": "desc" } }, { "sku": { "order": "asc" } }],
"from": 24,
"size": 24,
"track_total_hits": true
}

(Shown in a different key order from the code, and with _source left out.) The colour facet’s filter applies no colour selection, so it keeps showing all colours that match “jacket”; the brand, price and category facets count only red and blue jackets. The function that builds it, buildListingSearch, is about 25 lines and is a reasonable starting point for your own.

A response may contain at most 65,536 buckets across all bucket aggregations at every level (Elasticsearch’s search.max_buckets, answered with 503 too_many_buckets_exception). A range aggregation takes at most 1,024 ranges, and a request at most 256 queries across all filter and filters aggregations. See Limits and differences.