Module 12 · Lesson 11
Design product search
An inverted index, a relevance function, and filters that must stay fast as the catalogue grows.
What you will be able to do
- Explain inverted index and analyzer basics
- Design faceted filtering without a full scan
- Choose ranking signals and evaluate them
- Keep the index consistent with the source of truth
Requirements and estimates
- Functional: keyword search over titles, descriptions and attributes; filters by category, price, brand; sort by relevance, price or rating.
- Non-functional: p99 under 200 ms, index freshness under 1 minute, typo tolerance.
- Scale: 100M products, 50k queries/s peak, 5M updates/day.
100M docs x ~500 B indexed fields = ~50 GB of terms+postings with 2x overhead and replicas: ~300 GB cluster queries 50k/s / 20 shards = 2.5k/s per shard, feasible updates 5M/day = ~60/s, batched into bulk refreshes
Index pipeline
Change capture
Product updates publish events from the catalog service; no direct writes to the index.
Enrich
Join category, brand, availability and rating into a search document.
Analyze
Lowercase, tokenise, apply synonyms and stemming per language field.
Bulk index
Batch writes on a short interval; an upsert keyed by product id keeps documents idempotent.
Refresh
Near-real-time refresh interval of a few seconds; rebuilding from source is always possible.
Never make the index authoritative
If the index is lost or corrupted, it must be rebuildable from the catalog database. Prices and stock shown in results should be validated at cart time, not trusted from a stale index.
Filtering and facets
Filters run on structured fields with doc values, so a category and price range narrow the candidate set before scoring. Facets are counts computed per filter dimension while the query executes — expensive if done naively for every combination, so real systems compute counts for the unfiltered dimensions selectively and cache popular queries.
- Filter before score. Cheap structured filters reduce the documents that need text scoring.
- Cache hot queries with a short TTL; search traffic is heavily skewed to a small set of terms.
- Bound pagination. Deep pages (10,000 results) force large scans; cap or use search-after cursors.
- Typo tolerance has a cost. Fuzzy matching enlarges the term set; limit edit distance and field scope.
Ranking
| Signal | Why it matters | Risk |
|---|---|---|
| Text relevance (BM25) | Query and document match | Rewards keyword stuffing |
| Popularity / sales | Reflects real demand | Entrenches incumbents |
| Conversion rate | Quality proxy | Noisy for new items |
| Price competitiveness | Commercial value | Can hurt margin |
| Recency / availability | Relevant and buyable | New items lack signals |
Ranking is a product decision expressed as a formula. Start with BM25 times a business multiplier, then evaluate with a fixed set of judged queries. Offline metrics before online A/B, always — otherwise every tweak is an opinion.
The closing line
"Catalog events feed a bulk-indexing pipeline into a sharded search cluster; filters run on structured fields before scoring; ranking is text relevance with a business multiplier; the index is derived and can be rebuilt from the catalog."
Worth remembering
- An inverted index maps tokens to documents; analyzers decide what a token is.
- Facets are pre-aggregated counts per filter value, not queries per filter.
- Ranking blends text relevance with business signals; measure with judgments, not intuition.
- The search index is derived — always rebuildable from the database, never the source of truth.
Checkpoint
Answer all 3 questions to finish the lesson.
1. Why must the search index be rebuildable from the database?
Derived stores cannot be the source of truth. Mapping changes alone often require reindexing every document.
2. What is the purpose of applying structured filters before scoring?
Scoring is the expensive part. Removing documents that cannot match the filter reduces the work before relevance is computed.
3. Why cap deep pagination?
Offset pagination must scan and discard preceding hits, so depth multiplies work. Cursors avoid the skip cost.