Search & Discovery
Design Search Engine
01
What the engine has to do
What the engine has to do
- Fetch pages from the open web on a schedule, find links to pages we have never seen, and revisit known pages at a rate that matches how often they change.
- Turn fetched pages into a searchable inverted index that maps every term to the documents that contain it, with positions for phrase matching.
- Answer free-text queries, quoted phrases and operators such as
site:,-excludeand a date range. - Return ten ranked results per page, each with a title, a URL and a snippet that shows the query terms in context.
- Offer spelling correction and query suggestions as the user types.
- Let site owners submit sitemaps and request removal of a URL.
- Search results in p99 under 300 ms from the moment the request reaches our front end, with p50 near 120 ms.
- Serve about 80,000 queries per second at the daily peak and survive a 1.5x burst when news breaks.
- Stay up at 99.99% for queries. Crawling and indexing may stall for hours without users noticing.
- Pages that matter, like news homepages, should be searchable within five minutes of changing; the long tail can lag by weeks.
- Lose a whole data centre and keep serving, possibly with a slightly older index.
- Respect
robots.txtand per-host politeness limits on every fetch.
A full web search engine is three systems glued together: a crawler, an index builder and a query server. In 45 minutes you cannot design all three in depth. Tell the interviewer you will sketch the crawl and index pipeline, then spend most of the time on query serving and ranking, unless they want the crawler instead. Ask how fresh results must be and whether personalisation is in scope, because both change the serving path.
02
Back-of-envelope numbers
Back-of-envelope numbers
We are not building Google. Assume a serious challenger: 20 billion pages in the serving index, 3 billion queries a day, and a crawler that refetches the average page every three weeks. Every row below follows from those choices.
| Quantity | Working | Result |
|---|---|---|
| Queries per day | assumption | 3B |
| Average query rate | 3B / 86,400 s | about 35,000 / s |
| Peak query rate | 2.3x average at the evening peak | about 80,000 / s |
| Queries that reach the index | 35% answered by the result cache | about 52,000 / s at peak |
| Pages in the serving index | assumption | 20B |
| Raw HTML stored | 20B x 70 KB average, compressed about 5:1 | about 280 TB |
| Extracted text for snippets | 20B x 3 KB compressed | about 60 TB |
| Postings (doc ID plus term frequency) | 20B docs x 600 distinct terms x 1.2 B | about 14 TB |
| Position data for phrase queries | 20B docs x 1,000 tokens x 1.5 B | about 30 TB |
| Index size per full replica | postings + positions + per-doc features | about 50 TB |
| Fetches per day | 20B / 21 days, plus 250M new URLs | about 1.2B, roughly 14,000 / s |
| Crawl bandwidth | 14,000 / s x 70 KB | about 1 GB/s, near 8 Gbps |
Fifty terabytes of index is not hard to store. It is hard to search in 50 ms while 52,000 other queries are doing the same thing. That forces the index into memory or fast NVMe, split across hundreds of machines, with every query touching many of them at once. The rest of this page is about keeping that fan-out fast and cheap.
Most queries are satisfied by a small slice of the web. If the best 3 billion pages sit in a RAM-resident first tier and the other 17 billion live on SSD and are consulted only when tier one comes up short, the hardware bill drops by more than half.
03
API design
API design
Notice the internal call carries a deadline, not just a timeout. The leaf uses it to cut scoring short and return a partial top-k, which the aggregator can still use. A timeout would throw that work away.
04
Architecture overview
Architecture overview
flowchart LR
FR[("URL frontier")] --> CR["Fetchers"]
CR --> DS[("Document store")]
DS --> PR["Parse, dedupe, link graph"]
PR --> IB["Index builder (batch)"]
PR --> RT["Real-time indexer"]
IB --> LF["Leaf shards"]
RT --> LF
U["User"] --> FE["Query front end"]
FE --> QC[("Result cache")]
FE --> QU["Query understanding"]
QU --> AG["Root and mid aggregators"]
AG --> LF
AG --> RR["Re-ranker"]
RR --> SN["Snippet servers"]Draw two planes and keep them apart. The build plane on the left runs continuously but is allowed to be slow: fetchers pull pages from a prioritised frontier, the parser strips markup, collapses near-duplicates and records every outgoing link, and the index builder turns batches of documents into sorted, compressed index segments. The query plane on the right only reads those segments, and it must answer in a few hundred milliseconds no matter what the build plane is doing.
A query lands on the front end, which normalises it and checks the result cache. On a miss, query understanding rewrites it: spelling fixes, synonym expansion, language detection and a guess at intent, such as "this looks like a navigational query for one site". The rewritten query goes to a root aggregator, which sends it to a set of mid-level aggregators, each of which owns a rack of leaf shards. Leaves score their documents and send back a few hundred candidates. Aggregators merge upwards, the re-ranker reorders the best few hundred with heavier models, and snippet servers fetch the text around each match from the document store.
Freshness comes from a second, much smaller index. The real-time indexer takes pages fetched in the last few hours and keeps them in a memory segment that leaves search alongside the big batch segment. When the next full build ships, those documents fold into it and the real-time segment is trimmed.
The frontier is a priority queue sharded by host, so politeness limits are enforced in one place per site. Priority mixes page importance, observed change rate and how long since the last fetch.
Each leaf owns about 20 million documents in tier one, holds its posting lists in RAM, and runs the cheap first-pass scoring. Leaves never talk to each other.
A root fanning out to 150 leaves directly would wait on the slowest of 150. Two levels of fan-out, with hedged requests at the lower level, keep the tail short.
A learned model that is far too expensive to run over millions of matches, but cheap enough for the top 300. It uses signals the leaves do not have, such as click history for the query.
05
Deep dive: posting lists, fan-out and the ranking cascade
Deep dive: posting lists, fan-out and the ranking cascade
How do you find and score the right few hundred documents out of 20 billion in tens of milliseconds? And how do you spread that work over hundreds of machines without the slowest one setting your latency? Answer those well and the rest of the system is standard plumbing.
What a posting list looks like
For every term, a leaf stores the IDs of documents containing it, sorted ascending. Sorting matters twice. It lets you store gaps between IDs instead of the IDs themselves, and gaps for common terms are tiny numbers that compress to a byte or less with schemes like PForDelta or Elias-Fano. It also lets you intersect two lists by walking them together, skipping ahead with a small skip index every 128 entries.
Next to each doc ID you keep the term frequency, and in a separate stream the positions, which are read only for phrase queries and for proximity scoring. Splitting the streams means a simple keyword query never pays to decompress positions it will not use.
Document IDs are assigned so that better documents get smaller IDs. A leaf walking a list in order therefore meets strong candidates first, which makes early termination far more effective.
Scoring without scoring everything
A two-word query over common terms can match tens of millions of documents in one shard. Scoring all of them would take seconds. Leaves use Block-Max WAND: every block of a posting list stores the highest score any document in it could reach. The leaf keeps a heap of its current top 200 and a threshold equal to the 200th score. If the upper bounds for a block of documents cannot beat the threshold, the whole block is skipped without being decoded. On real query logs this skips well over 90% of postings with exactly the same top-k as a full scan.
The first-pass score is cheap on purpose: BM25 over title, body and anchor text, plus a static quality score per document that combines link authority, spam likelihood and page speed. Static scores are computed offline in the build plane, once per index build.
Keeping the fan-out fast
flowchart TD R["Root aggregator"] --> M1["Mid aggregator, rack 1"] R --> M2["Mid aggregator, rack 2"] R --> M3["Mid aggregator, rack N"] M1 --> L1["Leaf 1"] M1 --> L2["Leaf 2"] M1 --> L3["Leaf 3, slow"] M1 -. "hedge after p95" .-> L3B["Leaf 3 replica"] M2 --> L4["Leaves 4 to 10"] M3 --> L5["Leaves 141 to 150"]
If each leaf has a 1% chance of a slow response, a query that waits on 150 leaves is slow more than three quarters of the time. Three things fix this. First, a tree: the root talks to about 12 mid aggregators, each of which talks to 12 or 13 leaves in its own rack, so merges happen close to the data. Second, hedged requests: a mid aggregator that has not heard from a leaf by that leaf's p95 latency sends the same request to another replica and takes whichever answers first. That costs a few percent of extra load and removes most of the tail. Third, deadlines: when the budget runs out, the aggregator returns with what it has and marks the response as partial. Missing one shard out of 150 drops less than 1% of candidates, and users cannot tell.
The ranking cascade
| Stage | Runs on | Candidates in | Out | Budget |
|---|---|---|---|---|
| BM25 plus static quality | every leaf | millions per shard | 200 per shard | about 40 ms |
| Merge and dedupe | aggregators | 30,000 | 1,000 | about 5 ms |
| Gradient boosted trees, about 300 features | aggregator | 1,000 | 300 | about 15 ms |
| Transformer cross-encoder | GPU re-rank pool | 300 | 50 | about 45 ms |
| Diversity and business rules | front end | 50 | 10 per page | about 3 ms |
Each stage is roughly ten times more expensive per document than the one before it, and sees roughly ten times fewer documents. That is the whole trick. The cross-encoder reads the query and each candidate together, which is the most accurate signal we have, but at 300 documents per query and 52,000 queries per second that is still about 15 million inferences a second, so it runs on a dedicated GPU pool with request batching.
How a new index reaches the leaves
The batch build runs as a large MapReduce-style job: mappers emit (term, doc_id, tf, positions), the shuffle groups by shard and term, and reducers write immutable segments. A build is pushed to one replica set at a time, checked against a fixed set of probe queries whose results must stay within tolerance, and only then promoted. Every leaf can hold two versions, so rolling back is a pointer swap, not a reload.
Document-partitioned index, tiered by quality, with the top tier in RAM. Leaves run Block-Max WAND over BM25 plus static scores and return a local top 200 inside a deadline. A two-level aggregator tree with hedged requests merges results, then trees and a cross-encoder re-rank the best 1,000. A small real-time segment covers the last few hours of crawling, and a result cache absorbs about a third of traffic.
06
Decisions and what they cost
Decisions and what they cost
Each leaf holds complete posting lists for its own documents, so it can score a query alone and failures only remove a slice of documents. Adding pages means adding shards. The cost is that every query touches every shard in the tier, which you pay for with replicas.
Default for web scaleA query for three terms touches only three shards, which sounds efficient. But merging requires shipping whole posting lists between machines, popular terms create hot shards, and losing one shard makes a word unsearchable.
Hot spots and fragileThe best 3 billion pages live in RAM and answer most queries. The SSD tier is queried only when tier one returns too few strong results, which happens for about one query in five, mostly rare long-tail queries.
Cuts hardware by halfSimpler to operate, one build and one shard map. Latency is higher and less predictable because every query reads from flash, and you buy far more replicas to hit the same p99.
Fine below a few billion pagesDaily or twice-daily rebuilds give well compressed, globally ordered segments. A small in-memory segment covers fresh pages in minutes. Queries search both and merge.
Fresh and compactUpdating every posting list in place as pages arrive keeps everything fresh but ruins compression and the quality-ordered doc IDs that early termination depends on.
Slower queries everywhereCaching the final ten results for a few minutes absorbs repeated queries, especially during news spikes, when millions of people type the same thing.
About 35% hit rateHelps when many different queries share a pair of terms, but it adds memory pressure on the leaves and gets invalidated on every index push.
Worth it only on leaves07
Failure modes
Failure modes
A firmware bug throttles CPUs on a single rack. Its leaves still answer, just three times slower, so nothing is marked unhealthy, but every query in the region now waits on that rack and p99 jumps from 280 ms to 700 ms.
Hedge requests at the mid aggregator after the leaf's own p95, eject replicas whose latency drifts far from their peers, and enforce deadlines so a slow rack shrinks the candidate pool instead of stretching the response.A user pastes a 4,000-character string of common words with quotes around it. The phrase match forces every leaf to decode huge position lists, leaves run out of memory and restart, and the same query is retried, taking down the next replica too.
Cap query length and term count at the front end, put per-query memory and CPU limits in the leaf, and record which query was running when a leaf crashed. After two crashes, the root blocks that query fingerprint for an hour.A tokeniser change in the build drops every word containing an apostrophe. The build passes basic checks and starts rolling out, and searches like "children's books" begin returning junk.
Push to one replica set first, compare its answers on a fixed probe set of tens of thousands of queries against the previous build, and stop the rollout if relevance metrics shift. Keep the previous build loaded so rollback takes seconds.A calendar page links to the next day forever, or a site appends a session ID to every URL. The frontier fills with millions of near-identical URLs from one host and crawl budget for the rest of the web shrinks.
Normalise URLs and strip known session parameters, cap pages per host per day, detect near-duplicate content with SimHash, and lower a host's priority automatically when its new pages keep turning out to be duplicates.A major event happens and query volume for one topic jumps fiftyfold, but the batch index is eight hours old, so the top results are about last week's story.
Crawl known news sources every few minutes into the real-time segment, boost freshness in ranking when a query's volume suddenly spikes, and lower the result cache TTL for those queries so new articles appear quickly.A power event takes out one of three serving regions. Its share of traffic moves to the other two, which are now running at 150% of their normal load.
Provision each region for half of global peak, not a third, shed the expensive re-rank stage first under overload, and keep serving the SSD tier from fewer replicas. Users get slightly worse ranking instead of errors.08
Mistakes to avoid
Mistakes to avoid
A table scan per query cannot work at billions of rows, and a B-tree index on text does nothing for "contains this word somewhere".
Build an inverted index and say early that the whole design revolves around it.A neural ranker over millions of candidates per query would need more GPUs than exist.
Describe a cascade where each stage narrows the set by about ten times before something more expensive runs.The response time becomes the slowest of hundreds of machines, and the aggregator's network becomes a bottleneck.
Use a tree of aggregators, hedged requests and per-request deadlines.Hours of build time per update make the index permanently stale for news.
Keep a small real-time segment next to the batch index and merge results at query time.Ignoring politeness, traps and duplicates gets your crawler blocked and wastes most of its budget.
Talk about per-host queues, robots.txt, URL normalisation and duplicate detection, even if briefly.09
How to handle the interview
How to handle the interview
Split the problem into crawl, build and serve. Say which one you will go deep on. Interviewers usually want serving, and naming the split shows you know where the hard parts live.
Do the index size maths out loud. Documents times distinct terms times bytes per posting gets you to tens of terabytes. That one line justifies sharding and in-memory leaves.
Choose document partitioning and defend it. Explain why term partitioning creates hot shards and moves too much data, then accept the cost of querying every shard.
Name the tail-latency problem before you are asked. Waiting on 150 machines means the slowest one decides your latency. Trees, hedging and deadlines are the expected answer.
Describe ranking as a cascade. Cheap lexical scoring on leaves, learned models on hundreds, a neural re-ranker on tens. Give rough budgets for each stage.
Have a freshness story. Batch builds for the bulk, a real-time segment for news, and a ranking boost for queries whose volume is spiking.
Mention one quality signal that is not text. Link authority, click feedback or spam scores show you know text match alone is easy to game.
Keep the first draft small. Start with one machine and Lucene, then add shards, replicas and tiers as the numbers demand. The order of growth is part of the answer.
10
Growing the design in stages
Growing the design in stages
Each stage below exists because some measurement crossed a line. Do not draw the aggregator tree for a site search over two million pages.
A handful of nodes, a simple crawler, BM25 ranking out of the box. Good enough for a site search or a vertical engine, and cheap enough to run on three machines.
Custom posting formats with gap compression, document partitioning across dozens of leaves, one aggregator, and a frontier with per-host politeness. Link analysis starts to matter for spam.
Fan-out to hundreds of leaves forces two-level aggregation and hedged requests. A gradient boosted re-ranker trained on clicks replaces hand-tuned weights. A result cache appears in front.
The full index no longer fits in RAM affordably, so the top tier stays in memory and the rest moves to SSD. News freshness needs a separate fast-path index. The single batch build becomes a pipeline with probe-query checks.
Full index copies in several regions, traffic steered to the nearest one, and a GPU pool running cross-encoders on the final few hundred candidates. Index pushes become the riskiest change you make, so they get the most careful rollout.
Watch and read
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.
