Search Autocomplete / Typeahead
Problem statement
Design a system that returns the top 5–10 query suggestions as the user types, ranked by popularity, updated as search trends change.
Requirements
Functional
Given a prefix, return the top-k most popular completions.
Suggestions reflect recent popularity (updated hourly/daily).
Support prefix lengths 1–50; case-insensitive; optionally personalised.
Non-functional
p99 latency < 100 ms end-to-end (users type ~5 chars/s).
Highly available — degrade to stale suggestions rather than none.
10B search queries/day feeding the popularity data.
Storage efficient: hundreds of millions of distinct queries.
Back-of-the-envelope estimates
- 10B queries/day → ~120k QPS for the search backend; autocomplete calls ≈ 5–8× that → ~700k QPS, mostly cached.
- Distinct queries: 200M × avg 20 chars ≈ 4 GB raw; a trie with top-k cached per node ≈ 3–4× → ~15 GB, fits in memory on a few nodes, sharded by prefix.
- Popularity aggregation: 10B events/day = 116k/s into a stream processor; hourly batch produces ~200M counts.
API
GET /v1/autocomplete?q=how+to&limit=8&locale=en-IN
→ { "suggestions": ["how to tie a tie", "how to cook rice", ...] }
Client debounces (~100 ms) and cancels in-flight requests when the prefix changes.
Data model
Trie node: {children, topK: [(query, score)]}, topK precomputed. Persisted as a serialised snapshot in object storage; served from memory. Popularity source: query_counts(query, window, count) in a wide-column store.
High-level design
- Data collection: search service logs queries to Kafka; a stream/batch job aggregates counts per query per hour and writes to
query_counts. Filter out low-frequency and unsafe queries. - Trie builder (offline, hourly/daily): reads aggregated counts, builds a trie where every node stores its top-k completions (computed bottom-up by merging children). Serialised and shipped to serving nodes.
- Serving: nodes hold the trie in memory; sharded by first 1–2 characters with hot-prefix awareness (e.g. 'a…' gets more shards than 'x…'). A request routes to the shard, walks to the prefix node, returns its cached top-k — O(prefix length).
- Caching: CDN/edge cache and browser cache for short popular prefixes (TTL minutes). Most requests never reach the trie.
- Personalisation (optional): merge global top-k with the user's recent searches client-side or in a thin layer.
Architecture diagram
Deep dives
Why precompute top-k: walking a subtree at query time is O(subtree) — too slow; storing top-k per node makes lookup O(L). The cost is rebuild time and memory, paid offline.
Freshness vs cost: hourly rebuilds for trending terms; a small 'trending' overlay merged at serve time gives minute-level freshness for breaking news.
Sharding: naive first-letter sharding is skewed; use a shard map built from prefix frequency so each shard carries similar QPS.
Filtering: profanity/PII filter applied at build time; a blocklist checked at serve time for urgent removals.
Bottlenecks & mitigations
Hot prefixes (single letters) → cache at edge; serve from every node (replicate the top level of the trie).
Trie rebuild time as data grows → build incrementally per shard; blue-green swap snapshots.
Long-tail prefixes with no data → return empty quickly; don't fall back to expensive search.
Failure modes
- Serving node down: LB routes to replicas of the shard; snapshots reload in minutes.
- Aggregation pipeline delay: serve the last good snapshot; alert if a snapshot is > 2 hours old.
- Bad snapshot (corrupt/offensive): keep the previous snapshot and roll back atomically.
- Edge cache serving stale after a takedown: purge by prefix key; keep TTLs short for sensitive locales.
Follow-up questions interviewers ask
How would you support fuzzy matching (typos)?
How would you do multi-language / transliterated (Hinglish) suggestions?
How would you personalise without leaking other users' queries?