CASE STUDY

Faceted Product Search at Large Scale (Amazon)

4 min read·624 words·Intermediate

How to use this case study

SDE-2 / Mid

Explain the inverted index for text, filters on price, brand and category, and how facet counts ("Brand: Nike (1,203)") are computed.

SDE-3 / Senior

Go deeper on nested categories, doc values for fast filtering and aggregations, sharding and replication, index updates from the catalog, and deterministic pagination.

Staff / Principal

Discuss relevance ranking and personalization, query latency at high QPS, caching, and consistency between price changes and search results.


0) Problem Restatement

Design product search for a large marketplace (asked at Amazon). Users type free text ("running shoes") and add filters: price range, brand, rating, and nested categories (Clothing → Shoes → Running). The results page also shows facets, lists of filter values with counts ("Nike (1,203)", "Adidas (987)"), which update as filters are applied. The catalog and traffic are huge, filters combine freely, and results need stable pagination.

Asked at: Amazon — 1 candidate report between Aug 2026 and Aug 2026.

1) Requirements

  • Full-text search with relevance ranking.
  • Filters: numeric ranges (price), exact values (brand, color), a category tree, availability.
  • Facet counts for the current query and filters.
  • Sorting: relevance, price, rating, newest.
  • Pagination that doesn't repeat or skip items.
  • Latency under ~200 ms at thousands of QPS, and catalog changes visible within minutes.

1.1 Scale

  • 500M products, 20K search QPS at peak.


2) Architecture

Architecture Diagram

flowchart LR
    CAT[("Catalog DB")] -->|"CDC changes"| K[("Kafka")]
    K --> IDXR["Indexer - build search documents"]
    IDXR --> ES[("Search cluster - shards x replicas")]
    U["Shoppers"] --> API["Search API"]
    API --> QC[("Query cache")]
    API --> ES
    API --> RR["Re-ranker - personalization"]
    PR["Price / stock updates"] --> K

A search engine like Elasticsearch/OpenSearch (or a custom Lucene-based system) holds the index.


3) Index Design

Each product becomes a document:

{ "product_id": 991, "title": "Men's Trail Running Shoe", "brand": "Nike",
  "category_path": ["clothing", "clothing/shoes", "clothing/shoes/running"],
  "price_cents": 8999, "rating": 4.4, "in_stock": true, "attributes": { "color": ["blue","black"], "size": ["9","10"] },
  "sales_rank": 1234, "created_at": "2026-08-01" }
  • Text fields (title, description) go into an inverted index: word → list of products.
  • Filter and facet fields (brand, color, price, category) are stored as doc values (column-style storage), which makes filtering and counting fast.
  • Nested categories: store every ancestor path (clothing, clothing/shoes, ...). Filtering on "Shoes" matches all subcategories with one term, and facet counts per level come from these terms.


4) Query Execution

  1. Parse the text, and match against the inverted index (with synonyms and typo tolerance).
  2. Apply filters as fast bitset operations (they don't affect scoring and are cacheable).
  3. Score matches (BM25 text relevance + business signals like sales and rating) and take the top N.
  4. Compute facets with aggregations over the matching set: terms counts for brand and color, range buckets for price, category counts.
  • Multi-select facets: when the user selects Brand = Nike, the brand facet should still show counts for other brands (computed with all filters except brand). This is done with "post filters" or separate aggregations per facet.
5. Each shard returns its top results and facet counts. The coordinator merges them.


5) Pagination and Consistency

  • Deep offset pagination is expensive and unstable. Use search_after (a cursor with the last item's sort values + product_id as a tie-breaker). This is deterministic, since ties are broken by ID.
  • Optionally pin a point-in-time snapshot for a browsing session, so the results don't shift while paging.
  • Price and stock freshness: updates stream from Kafka into the index within seconds or minutes. The product page and checkout always show the live price, so search is allowed to be slightly behind.


6) Performance

  • Shards split the 500M documents (e.g., 50 shards × ~10M docs), and replicas multiply query capacity.
  • Caching: a filter cache (bitsets for common filters like in_stock=true), and a query-result cache for popular searches (short TTL).
  • Routing by category or region can limit which shards a query hits.
  • Personalization happens as a re-rank of the top ~200 results, to keep the engine query fast.


7) Wrap-Up

Index each product with text fields in an inverted index and filter and facet fields as doc values, storing every category ancestor path for nested categories. Run queries as text match + bitset filters + scoring, compute facet counts with aggregations (excluding a facet's own filter for multi-select), merge shard results, and paginate with search_after cursors and ID tie-breakers. Keep the index fresh via CDC, and scale with shards, replicas, filter and query caches, and a light re-ranking layer.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →