A facet looks simple from the outside: mark a field facetable, and every search response comes back with counts for each of its values. The engineering problem shows up once a collection passes a few hundred thousand documents and a category page facets on five fields at once — brand, price bucket, size, colour, availability — each of which has to be recomputed against the current query and every other active filter, on every keystroke of an autocomplete-driven search box.
Why facet counts get expensive
A naive implementation recomputes every facet's full value distribution from scratch on every request: scan the matching documents, group by field, count. That is fine at ten thousand documents and unusable at ten million, because the cost scales with the size of the result set, not with the number of distinct facet values a shopper will actually see.
Two techniques keep it fast without changing what the shopper sees:
- Facet computation shares the query's own retrieval pass. Counting happens on the same posting-list intersection used to find matching documents, not as a second pass over the result set — so facets add a constant overhead per query rather than a second full scan.
- High-cardinality facets cap the values returned, not the accuracy of the count for the values shown. A brand facet with four thousand distinct values doesn't need four thousand counts back — it needs the top handful sorted by count, plus the ability to page into the rest if a shopper searches within the facet.
Facets compose with filters, not replace them
The subtlety that breaks a lot of DIY facet implementations: when a shopper has already filtered to brand:nike, the price facet's counts should reflect only Nike products — but the brand facet's own counts should still show every brand, each with the count it would produce if selected, so the shopper can switch brands without clearing the filter first. That means each facet is computed against the query filtered by every other active facet, not the query filtered by all of them including itself. Get this wrong and a shopper who filters by brand sees the brand facet collapse to one value with no way back.
What this looks like on a real catalogue
AACSearch's facet engine runs this filtered-per-facet computation inline with retrieval, so a category page with five active facets and a few hundred thousand candidate documents still answers inside the same latency budget as an unfiltered query. The trade-off it makes explicit rather than hiding: facets on very high-cardinality fields (a free-text tag field with tens of thousands of unique values, say) are capped to the most relevant values per request, with an explicit "load more" rather than silently truncating a small facet the same way. A field that shouldn't be exhaustively faceted at that cardinality is a schema decision, not a performance surprise discovered in production.
The result for the storefront: facet counts that always agree with what clicking through actually returns, on a catalogue too large for a naive "recompute everything from scratch" approach to survive.
Похожие посты
Webhook delivery: signatures, retries and safe replays
How AACSearch signs outbound webhooks, what happens when your endpoint is briefly down, and how an Idempotency-Key keeps a retried write from landing twice.
Multi-tenant search architecture: how tenant isolation works in AACSearch
Collection namespacing on the engine proxy, full versus search key scopes, short-lived browser keys, per-organization quotas in a Durable Object and organization-scoped events — what a platform operator gets when embedding search for many customers.
What is a Search OS?
A search box answers one query. A DIY engine gives you a retrieval primitive. A Search OS is the layered system around it — query understanding, indexing, synonyms, ranking rules, curation, results and analytics — that a tenant actually controls. Here's how AACSearch implements each layer.