Design search typeahead
Type "how to" into a search box and ten suggestions appear before your next keystroke. Every keystroke is a request, so a search engine serves far more suggestion lookups than searches, and each must answer in a few milliseconds. The trick is to do almost no work at query time: precompute the answer for every popular prefix in advance, and serve it from memory. It follows the interview framework phase by phase.
1. The prompt
"Design the autocomplete for a search box: as the user types, show the top 10 most popular queries that start with what they've typed."
The read path must be almost free, so the design moves the work offline: a pipeline that counts queries and precomputes answers, and a serving fleet that only looks them up. The interesting parts are the data structure, freshness for trending queries, and taking load off the servers altogether with the client and the CDN.
2. Frame the problem
Suggestions from what: past searches, or a product catalogue?
Popular past search queries.
We need a pipeline from search logs to counts to precomputed lists.
How fresh must popularity be?
Daily for most; trending queries within minutes.
A batch build plus a small streaming layer.
How many suggestions, and how fast?
Up to 10, within ~50 ms end to end.
A few milliseconds on the server leaves room for the network.
Personalised?
Lightly: the user's own recent searches mixed in.
A small per-user layer on top of global lists, done on the device or a small store.
Must anything be filtered?
Yes: offensive or unsafe suggestions must never appear.
Filtering at build time, plus an emergency block list at serving time.
Languages and regions?
Per locale; we'll treat each locale as its own table.
The tables partition naturally by locale.
3. What it must do
- For a prefix, the system returns up to 10 suggestions, ranked by popularity.
- Popularity reflects recent searches: trending queries appear within minutes.
- Operators can block offensive or unsafe suggestions immediately.
Not now:
- Spelling correction: a different model.
- Deep personalisation: mentioned in follow-ups.
- Tokenisation for every language.
4. How well it must do it
| Target | Why this number |
|---|---|
| Very fast: p99 under 50 ms end to end; a few ms at the server speed | Suggestions are useless if they arrive after the next keystroke. |
| Scale: ~75,000 lookups a second at peak before caching load shape | Every keystroke is a request. |
| Fresh: trending queries within ~10 minutes; everything else daily speed | People search for breaking news immediately. |
| Available: stale suggestions are fine; no suggestions is an acceptable fallback consistency or availability | Search still works without autocomplete. |
| Safe: a blocked suggestion disappears everywhere within minutes security · rules | An offensive suggestion is a public embarrassment. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Lookups | 500M searches a day × 5 keystrokes ÷ 10⁵ s | ~25,000/s, ~75,000/s at peak → so cut requests at the client and CDN first |
| Distinct queries worth suggesting | Queries seen at least ~10 times in the window | ~100M queries |
| Prefix table | Prefixes up to 20 characters, ~10 suggestions each, heavily shared | Tens of GB → so it fits in memory across a few servers, replicated |
| Short prefixes | "h", "ho", "how" are requested by everyone | → so they cache superbly at the edge |
6. The nouns
- Query log event.
- Query count: time-decayed popularity.
- Prefix entry: a prefix and its top 10.
- Table version: one build of all prefix entries.
- Block list.
query log text, time, locale (stream → data lake) query counts (locale, query) → decayed score (batch job output) prefix table (locale, prefix) → [(query, score) × 10] versioned, loaded whole into memory block list terms and exact queries, applied at build and at serve time
7. The contract
GET /v1/suggest?q=how%20to%20m&locale=en-IN&limit=10
→ 200 {"suggestions": ["how to make pancakes", "how to meditate", …]}
Cache-Control: public, max-age=300
Anonymous and cacheable: personal suggestions (the user's own history) are mixed in on the device, so the shared response stays cacheable.
Data flow: (1) searches are logged → (2) a batch job counts and builds the prefix table → (3) servers load the new version → (4) a stream job adds trending queries → (5) the client debounces and caches; the CDN answers popular prefixes; misses hit a server's memory.
8. First working version
A queries table with counts, updated from the search logs, and a suggestion service that queries it.
8.1 Suggesting
For each keystroke, the service runs a prefix query sorted by count.
8.2 Counting
A nightly job aggregates the day's search log into the counts table.
8.3 Blocking
A blocked flag on the queries row hides it from the query.
Parked for later (section 9 fixes each)
- A short prefix like "h" matches millions of rows; sorting them on every keystroke is far too slow.
- How to rebuild without serving a half-built table.
- 75,000 requests a second, most of them for the same short prefixes.
- Nightly counts miss breaking news.
- Personal history and safety.
9. Make it hold up
9.1 Finding the top suggestions for a prefix very fast
| Approach | Why | |
|---|---|---|
| ✗ Bad | SELECT query FROM queries WHERE query LIKE 'how to m%' ORDER BY count DESC LIMIT 10 | The prefix match can use an index, but sorting all matches by count on every keystroke is far too slow for short prefixes like "h", which match millions of rows. |
| ~ Good | A trie (prefix tree): walk down to the prefix's node, then search its subtree for the most popular completions | Walking down is fast (one step per character), but searching a big subtree at query time is still slow for short prefixes. |
| ✓ Great | A trie where every node stores its own top 10, computed when the trie is built; or simply a hash map from prefix to its top 10 | A lookup is one walk (or one hash lookup) and returns a ready list. All the work moves into the offline build. Try it in section 13. |
9.2 Building the tables, and swapping safely fresh · available
- Search events stream into a log (Kafka) and land in the data lake.
- A batch job (hourly or daily) counts queries over a window, with more weight for recent searches (exponential decay), drops rare queries, and filters out blocked terms.
- It builds the prefix → top-10 table for every prefix of every kept query.
- The new table is loaded into the serving fleet as a whole new version, and servers switch to it atomically. Serving never sees a half-built table; a bad build is rolled back by switching back.
9.3 Serving it fast, and mostly not serving it at all very fast · scale
- Suggestion servers hold the table in memory, partitioned if needed by the first characters of the prefix (with care: "s" is far busier than "x", so balance partitions by traffic, not letters) and replicated for throughput.
- Client side: wait ~100 ms after the last keystroke before sending (debounce), cancel requests that are out of date, and cache answers in the browser, since users often type and delete.
- CDN: short prefixes are requested by everyone and change slowly, so they cache very well at the edge for a few minutes.
9.4 Trending queries fresh
A daily rebuild misses a story breaking right now. Add a stream job that counts queries over the last few minutes (Module 23) and detects sudden rises; a small "trending" table is merged into results at query time, or into the top lists of affected prefixes. Keep CDN lifetimes short (a few minutes) so trends reach users.
9.5 Personalisation and safety safe
Blend global suggestions with the user's own recent searches, kept on their device or in a small per-user store, so the shared response stays cacheable. Filter suggestions through a block list and safety classifiers at build time, plus an emergency block list applied at serving time for anything that slipped through. An emergency block also purges affected prefixes from the CDN.
9.6 The whole design after the deep dives
Building, swapping and serving suggestions in the final design.
10. Interviewer follow-ups
Traffic grows 10×.
Most of it lands on the CDN and client caches. Add server replicas for the misses. The build job grows with data volume, but it's offline.
Support typos ("hwo to").
At build time, also index common misspellings that lead to the same query (from logs where people corrected themselves), or run a fuzzy match on short edit distances for prefixes with few results.
Match words in the middle ("pancakes" → "how to make pancakes").
Index every word position, not just the start: each query is added under the prefixes of each of its words. The table grows several times, still within memory for popular queries.
A bad build ships offensive suggestions.
Roll back to the previous version (an atomic switch), purge the CDN, add the terms to the emergency list, and add a pre-swap check that compares the new version's top prefixes with the old one.
How do you measure quality?
Acceptance rate (how often a suggestion is clicked), keystrokes saved, and latency. A/B test scoring changes.
Trie or hash map?
A hash map of prefix → list is simplest to serve and shard; a trie saves memory by sharing prefixes. With precomputed lists, either works; pick the hash map unless memory is tight.
Different results per country.
Build a table per locale (or region), and pass the locale in the request. CDN caches key on it.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Build job fails | Slightly stale suggestions | Table age alert | Servers keep the last good version |
| Bad build | Odd or offensive suggestions | Pre-swap checks, acceptance rate drop, reports | Atomic rollback, CDN purge, emergency block list |
| Suggest servers overloaded | Slow or missing suggestions | Latency, error rate | CDN and client caches absorb most; the client hides suggestions on timeout |
| Trending stream lags | New trends appear later | Consumer lag | Daily build still covers the rest |
12. Wrap-up and the clock
Trade-offs we made:
- Precompute everything offline (trivial lookups, freshness limited by build frequency).
- A trending stream to close the gap.
- Atomic versioned swaps.
- Heavy client and CDN caching (fewer requests, minutes of staleness).
- Personalisation on the device to keep responses shareable.
What we'd watch:
- Suggest latency p99.
- CDN hit rate.
- Table age.
- Acceptance rate.
- Trending lag.
- Block-list hits.
At 10×:
- More replicas.
- Per-locale tables.
- More edge caching.
A 45-minute interview
| 0–4 | Frame the problem (source, freshness, safety) |
| 4–9 | Must do, how well, numbers |
| 9–12 | Nouns, contract, data flow |
| 12–18 | First working version, parked list |
| 18–40 | Deep dives: trie with top-k (9.1), build and swap (9.2), client/CDN (9.3), trending (9.4) |
| 40–45 | Follow-ups and wrap-up |
A 60-minute interview
| 0–5 | Frame the problem |
| 5–11 | Must do, how well, numbers |
| 11–15 | Nouns, contract, data flow |
| 15–23 | First working version |
| 23–50 | Deep dives 9.1–9.5 and the final picture |
| 50–60 | Follow-ups: typos, mid-word matching, bad builds; wrap-up |
| Level | What a strong answer shows |
|---|---|
| Mid-level | A trie, why the naive query is slow, serving from memory, a periodic rebuild from logs. |
| Senior | Top-k stored per node, an atomic swap of new versions, client debouncing and caching, CDN for short prefixes, balanced partitioning. |
| Staff+ | Time-decayed scoring, the trending stream, personalisation blend, safety filtering, and the cost trade-off between build frequency and freshness. |
Common mistakes:
- Querying the database per keystroke.
- Searching the trie subtree at query time.
- No client debouncing.
- Ignoring the CDN.
- Rebuilding in place.
- No answer for trending queries or offensive suggestions.
13. Try it, check yourself, read more
A real trie built from about 40 sample queries with counts. Each node stores its top 5. Type a prefix: the lookup walks one node per character and returns the stored list.
How do you make a trie lookup fast even for a one-letter prefix?
What does debouncing do?
How do breaking-news queries appear quickly?
How do servers move to a new table without ever serving a half-built one?
| Resource | Why |
|---|---|
| Design a search autocomplete system (ByteByteGo) | A full chapter on the same design. |
| Trie (Wikipedia) | The data structure, with variants such as compressed (radix) tries. |