Search & Retrieval

Faceted Search

Aggregation-based filtering that lets users iteratively narrow search results using dynamic facet counts across categories, price ranges, ratings, and more.

⏱ 9 min read

What it is

Faceted search is a navigation paradigm that combines free-text search with a set of facets — pre-computed, dynamically updated filter dimensions derived from the current result set. Each facet shows the distinct values for a field (e.g., Brand: Apple (42), Samsung (31)) along with the document count that would remain if that value were selected. Users can progressively refine results by checking multiple facet values, and the counts update in real time to reflect only the still-reachable combinations.

Under the hood, facets are implemented as aggregations in Elasticsearch and OpenSearch. A terms aggregation groups documents by a keyword field and counts occurrences; a range aggregation buckets numeric or date fields into user-defined intervals; a histogram aggregation produces uniform-width buckets. The critical UX distinction is that facet counts reflect the results after all active filters are applied, except for the facet being counted itself (a technique called post-filter or multi-select faceting), so users always see non-zero counts for available refinements.

Why it exists

Users rarely know exactly what they want when they start browsing. Faceted search exploits this by guiding discovery: rather than requiring users to write precise queries, they can start broad and iteratively narrow. This is especially valuable in e-commerce, where a shopper might start with "laptop" and then filter by brand, screen size, price range, and in-stock status without ever reformulating their query. The live facet counts act as a map of the result space, preventing dead-end selections.

From a technical perspective, faceted search emerged because databases cannot efficiently perform simultaneous GROUP BY operations across multiple high-cardinality columns on large datasets within a latency budget acceptable for interactive use. Search engines pre-build data structures (doc values, field data) that make these aggregations fast — typically under 100ms for millions of documents — enabling the real-time count updates that make the UX compelling.

When to use

  • E-commerce product listings where users need to filter by brand, price, rating, colour, size, and availability simultaneously.
  • Job boards where candidates filter by location, salary range, remote/on-site, contract type, and required skills.
  • Real estate search with facets for price bracket, property type, bedrooms, and neighbourhood.
  • Media libraries (images, videos, documents) with metadata facets like format, date, author, and licence type.
  • Enterprise knowledge bases where documents are tagged with department, document type, date range, and author.

When not to use

  • High-cardinality unique fields: Running a terms aggregation on a user ID or UUID field that has millions of unique values wastes heap and produces useless facet counts.
  • Real-time data with write-heavy workloads: Aggregations are expensive on indices that are constantly being updated; consider a separate analytics index refreshed periodically.
  • Pure full-text discovery: If the query domain is long-form documents without structured metadata, facets have nothing useful to aggregate on; use categories and taxonomy tagging first.
  • Very small result sets: When a query already returns fewer than 10 results, facets with counts of 1–2 add visual noise rather than help.

Typical architecture

Client Request:
  { query: "laptop", filters: { brand: "Dell", price: [500,1000] } }
         │
         ▼
┌─────────────────────────────────────────┐
│  Search Service / API Layer             │
│                                         │
│  1. Build bool query:                   │
│     - must: multi_match "laptop"        │
│     - filter: term brand=Dell           │
│     - filter: range price 500-1000      │
│                                         │
│  2. Add post_filter for multi-select:   │
│     (applied after aggs computed)       │
│                                         │
│  3. Add aggregations:                   │
│     - terms: brand (top 20)             │
│     - range: price buckets              │
│     - terms: screen_size                │
│     - avg: rating                       │
└─────────────────────────────────────────┘
         │
         ▼
   Elasticsearch Index
   (keyword fields with doc_values)
         │
         ▼
┌─────────────────────────────────────────┐
│  Response:                              │
│  hits: [doc1, doc2, ...]  (page 1)      │
│  aggregations:                          │
│    brand: [{Apple:12},{Lenovo:8}...]    │
│    price: [{0-500:5},{500-1k:23}...]    │
│    screen_size: [{15":14},{13":9}...]   │
└─────────────────────────────────────────┘

Pros and cons

Pros

  • Dramatically improves discovery UX — users can explore without knowing precise terminology.
  • Live document counts prevent dead-end filter combinations, reducing frustration.
  • Composable: any combination of facet values can be applied without re-architecting the query.
  • Elasticsearch aggregations are highly optimised using doc_values (columnar storage), keeping latency low.
  • Provides implicit analytics: aggregation results reveal the shape of your data over time.

Cons

  • Aggregations add latency and memory pressure proportional to the number of buckets and result cardinality.
  • Multi-select faceting (post_filter) adds complexity: different parts of the query must be scoped differently.
  • Nested facets (e.g., faceting on nested objects) require nested aggregations, which are much more expensive.
  • Facet ordering (by count vs. alphabetical vs. curated) is a UI/UX decision that requires product input.
  • Caching invalidation is tricky: facet counts change whenever documents are indexed or updated.

Implementation notes

Always map facet fields as keyword type (or integer/float for numeric facets) with doc_values: true (the default). Never run terms aggregations on text fields — this requires loading field data into heap, which is slow and memory-intensive. For fields that need both full-text search and faceting (e.g., product category), use Elasticsearch's multi-field mapping: a text sub-field for search and a keyword sub-field for aggregations. Control the number of buckets returned per aggregation with the size parameter; returning thousands of buckets per request is a common source of performance problems.

For multi-select faceting (where selecting one brand value should not filter out other brands from the brand facet), use the post_filter pattern: compute all aggregations on the unfiltered query, then apply the brand filter as a post_filter that affects only the returned hits. Alternatively, use a filter aggregation as a parent per facet to exclude that facet's own filter from its count computation. Cache aggressively: Elasticsearch's shard request cache stores aggregation results keyed by the query hash; set size: 0 if you only need aggregations with no hits to maximise cache hit rates.

Common failure modes

  • fielddata enabled on text fields: Loading field data for text aggregations causes heap exhaustion under load; always use keyword sub-fields instead.
  • Too many buckets: Setting size: 10000 on a terms aggregation forces the engine to collect 10,000 buckets per shard, multiplied across all shards.
  • Nested aggregation explosion: Deeply nested aggregations (terms inside terms inside nested) scale multiplicatively and quickly exceed memory limits.
  • Stale facet counts: Without refresh: true (not recommended in production) or sufficient refresh_interval, facet counts lag behind document ingestion.
  • UI dead ends from missing post_filter: Without post_filter for multi-select, selecting two values for the same facet shows zero results because both filters apply simultaneously via AND logic.

Decision checklist

  • Are all facet fields mapped as keyword (or numeric) with doc_values enabled?
  • Have you determined which facets need multi-select and implemented post_filter accordingly?
  • Have you capped aggregation bucket sizes to a reasonable limit (e.g., top 20)?
  • Have you considered aggregation caching and set size: 0 for count-only requests?
  • Have you validated that facet counts remain accurate after document updates?
  • Is the facet order (by count vs. alphabetical vs. manual) defined by product requirements?

Example use cases

  • E-commerce marketplace: Brand, price range, customer rating, shipping speed, and seller facets on a product search page; multi-select brand facet allows comparing Apple and Samsung simultaneously.
  • B2B parts catalogue: Facets on specification attributes (voltage, current rating, tolerance) derived from structured product data; range facets for dimensional attributes.
  • Legal document repository: Facets on practice area, jurisdiction, document type, date range, and author; helps lawyers narrow thousands of precedents to relevant cases.

Further reading