Handbooks / System Design Case Studies / Chapter 4
Design Search Autocomplete
12 pages · ~16 min✓ Reviewed
Builds on Design a Distributed Key-Value Store. Next up: Design a Notification System.
Problem Statement, Scope and Requirements
1. Problem and Scope
Running example: Search Autocomplete (typeahead) for a search box handling 10B searches/day.
- Input: a prefix typed so far, e.g.
wor - Output: the top 5 ranked completions, refreshed on every keystroke
- Ranked by popularity, not alphabetical order
- Prefix match on the typed text
- Return 5 suggestions per prefix
- English only
- No personalization in v1
- Offensive terms filtered out of results
2. Non-Functional Targets
| Property | Target | Meaning |
|---|---|---|
| Latency | p99 under 100 ms | End to end, keystroke to dropdown |
| Availability | 99.99% | About 52 min of downtime per year |
| Consistency | Eventual | New trends visible within 15 min |
- Stale suggestions are acceptable; a slow or missing dropdown is not
- Strong consistency is not needed for suggestions
High-Level Architecture
1. Two Paths (Search Autocomplete)
Search Autocomplete splits into a read path that serves prefixes and a write path that rebuilds the trie. They meet only at the snapshot.
| Read path | Write path | |
|---|---|---|
| Parts | query service, cache, trie | log, aggregate, build trie |
| Job | prefix → top 5 | searches → new trie |
Trie with Top-K Suggestions
1. Responsibility and the Fix
Maps a typed prefix to its 5 best completions in O(L), L = prefix length (Search Autocomplete).
- Naive: walk to the prefix node, then DFS every descendant: O(L + N)
- Too slow at ~1M peak QPS: N can be millions of nodes
- Fix: store the top-k list on every node, so lookup is O(L) and k = 5 is constant
- Cost: ~k x memory per node, paid once at build time
Data Collection Pipeline
1. Responsibility and Flow
- Turn raw search events into aggregated query counts
- Output feeds the trie builder for Search Autocomplete
- Kafka is partitioned by query hash, so one query always lands on one aggregator
- 1Client logone event per search
- 2Kafkapartition by hash(query)
- 3Stream aggregator1-min windows
- 4Hourly / weekly tablescounts per query
- 5Trie builderrebuild every 15 min
Ranking: Frequency and Recency
Running example: Search Autocomplete at 10B searches/day, top 5 completions per prefix.
1. Score and Decay
- Score = frequency over a decay window, not a lifetime total
- Raw all-time count buries new trends under old giants
- Exponential decay:
score = sum(count_i x 0.5^(age_i / half-life)) - Half-life = 7 days: a search from 7 days ago counts half, 14 days ago a quarter
2. The Blend
Suggestion Query Service
1. Responsibility
Running example: Search Autocomplete, ~460K QPS avg, ~1M QPS peak, p99 under 100 ms end to end.
- Stateless service: any instance can answer any prefix
- Normalizes the prefix: lowercase, trim, collapse spaces
" World CUP "becomesworld cup- Queries the right trie shard and returns the top 5
2. Request Flow
- Requestraw prefix
- Normalizelower, trim, collapse
- Local LRUhit: return now
- Routefirst 2 characters
- Top 5from trie shard
Caching at the Edge
1. Responsibility and Layers
Search Autocomplete at ~1M peak QPS: the edge answers the hottest prefixes, so the origin sees only ~150K QPS.
- 1Browser cachemax-age 300
- 2CDN edgeTTL 5 min
- 3Service local LRUin-process
- 4Trielookup under 1 ms
- Browser TTL is capped at 300 s: a CDN purge cannot clear browsers
- Most requests stop at the browser or CDN and never reach the origin
- Only a miss at every layer reaches the trie
Sharding the Trie
Sharding the Trie (Search Autocomplete): split a ~50 GB trie across 10 shards, each served by 3 replicas.
1. Why Shard
- Split the ~50 GB trie when one node's RAM or QPS cannot hold it
- Memory: ~50 GB / 10 shards = ~5 GB per shard
- QPS (est.): ~150K origin QPS / 10 shards = ~15K per shard
- One prefix is served by one shard, so its top-5 is precomputed there
2. Picking the Split
Filtering Offensive Terms
1. Responsibility and Layers
- Search Autocomplete never suggests slurs, adult or harmful terms
- A flagged term must disappear within minutes (under 5 min)
- Two layers: build-time filter, then serve-time filter on the output
- Build-time = blocklist + ML classifier, before queries enter the trie
- Search logsraw queries
- Build filterblocklist + ML
- Trietop-5 per node
- Serve filterversioned blocklist
- Clientsafe top 5
Scaling, Failure Handling, Trade-offs and Interview Questions
1. Scaling and Degradation
Search Autocomplete at ~1M peak QPS: fix each bottleneck with its own lever, and fail soft.
| Bottleneck | Lever | Example |
|---|---|---|
| Read QPS | Add replicas | 3 replicas per shard |
| Memory | Add shards | ~50 GB / 10 shards = ~5 GB each |
Cheatsheet: Typeahead Numbers and Rules
1. Scale and Trie (Search Autocomplete)
| Number | Value |
|---|---|
| Peak load | ~1M QPS (~460K avg x ~2.2) |
| Latency | p99 under 100 ms end to end |
| Trie size | ~50 GB in memory |
| Shards | 10 shards x 3 replicas = 30 servers |
| Per shard | 50 GB / 10 = ~5 GB each |
Check yourself
Both 'earth day' (weekly decayed count 500K, 0 in the last hour) and the new 'earthquake today' (weekly 200K, 200K in the last hour) match the prefix 'earth'. With score = 0.7 x weekly + 0.3 x last hour, which is suggested first?
- 'earth day' scores 0.7 x 500K + 0.3 x 0 = 350K
- 'earthquake today' scores 0.7 x 200K + 0.3 x 200K = 200K
- So 'earth day' ranks first, even though 'earthquake today' is the one trending
- The new query overtakes only if its traffic is sustained, since its weekly part keeps growing with each hour