Autocomplete, where the latency budget is a keystroke
Every design choice here is downstream of one constraint: the answer must arrive before the user types the next character.
The brief
As a user types into a search box, suggest completions ranked by popularity.
Suggestions come from what people actually searched for, so the corpus changes continuously.
Requirements
Functional
- Return the top suggestions for a prefix
- Rank by popularity, not alphabetically
- Reflect newly popular queries within a reasonable delay
- Never suggest something that must not be suggested
Non-functional
- A suggestion arriving after the next keystroke is a suggestion nobody sees
- Slightly stale rankings are fine; a slow response is not
- The write path is enormous and entirely offline; the read path is tiny and entirely online
Back-of-envelope
Assume
- 10 million searches a day
- 20 characters typed per search, with a request per keystroke after the third
- Top 10 suggestions per prefix
Therefore
- Prefix requests: 1e7 × 17 ≈ 1.7e8 a day ≈ 2,000 per second — about seventeen times the search traffic itself.
- That ratio is the entire point: autocomplete is not a feature on top of search, it is a system an order of magnitude busier than search.
- Distinct prefixes worth precomputing are bounded by short lengths. Storing the top 10 for every prefix up to a few characters is small enough to hold in memory; going deeper grows fast and returns less.
Debouncing on the client changes the 17 in that first line, and it is the cheapest optimisation available. Establishing the number before proposing the fix is what makes the fix arguable rather than reflexive.
The interface
What is stored
prefix (up to N characters) → [{ text, score }] × 10, held in memoryThe read is a map lookup, never a search. N and k are the two memory knobs, and both belong in the design rather than in a config file nobody revisits — raising N by one multiplies the map.
append-only: query · timestamp · whether a result was clickedThe batch job's only input, and never read online. Keeping it append-only is what lets the ranking be recomputed differently later without having lost anything.
exact terms and patterns — small enough to hold everywhereRead at build time AND at serve time, so it has to be small enough that a serving node can hold all of it. That size constraint is what makes serve-time filtering affordable at 2,000 requests a second.
The design
The decisions
Each of these could go the other way. The choice, the reason, and what it costs — a design that lists only what it chose teaches the choice; one that lists what it gave up teaches the judgement.
What breaks first
In order. Each names what you would actually observe, and each fix carries its cost.
When something fails
Scaling it
Each step is triggered by a number, not a feeling — and carries what it costs.
What gets probed
The design is the easy half. These are where the conversation goes, and each has a defensible answer above.
- A user types the fourth character. What is the deadline for your response, and what happens if you miss it?
- A query becomes popular in the last ten minutes. When does it show up, and who decided that delay is acceptable?
- Something is suggested that must not be. How fast can you remove it, and does that path need a deploy?
- How would you measure whether the suggestions are any good?