← System design

Search & Discovery

Design Search Engine

Typing a few words and getting ten good pages back feels instant, but behind it hundreds of machines each search their own slice of 20 billion documents and agree on an answer in under a third of a second. You need an index compact enough to keep in memory, a fan-out that is not held hostage by its slowest machine, and a ranking pipeline that saves its expensive models for the few hundred pages that might actually win.
Core: inverted index + ranking cascade80K queries a second at peak20B pages, tiered by qualityp99 under 300 ms

01

What the engine has to do

02

What the engine has to do

Functional
  • 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:, -exclude and 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.
Non-Functional
  • 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.txt and per-host politeness limits on every fetch.
Scope it before you draw it

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

03

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.

QuantityWorkingResult
Queries per dayassumption3B
Average query rate3B / 86,400 sabout 35,000 / s
Peak query rate2.3x average at the evening peakabout 80,000 / s
Queries that reach the index35% answered by the result cacheabout 52,000 / s at peak
Pages in the serving indexassumption20B
Raw HTML stored20B x 70 KB average, compressed about 5:1about 280 TB
Extracted text for snippets20B x 3 KB compressedabout 60 TB
Postings (doc ID plus term frequency)20B docs x 600 distinct terms x 1.2 Babout 14 TB
Position data for phrase queries20B docs x 1,000 tokens x 1.5 Babout 30 TB
Index size per full replicapostings + positions + per-doc featuresabout 50 TB
Fetches per day20B / 21 days, plus 250M new URLsabout 1.2B, roughly 14,000 / s
Crawl bandwidth14,000 / s x 70 KBabout 1 GB/s, near 8 Gbps
The number that shapes the design

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.

Where you can save

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

04

API design

GET/v1/search?q=solar+panel+efficiency&lang=en&region=IN&page=1Run a web search
// Served by the query front end. Anonymous calls allowed, rate limited per IP. { "query": "solar panel efficiency", "corrected_query": null, "took_ms": 142, "index_version": "2026-09-30T22:00Z+rt-1182", // base build plus real-time segment "results": [ { "rank": 1, "url": "https://energy.example.org/guides/panel-efficiency", "title": "How efficient are solar panels in 2026?", "snippet": "Most rooftop <b>panels</b> now convert 21 to 23 percent of sunlight...", "crawled_at": "2026-09-28T04:11:09Z" } ], "next_page_token": "p2.c1d9" // signed, encodes query hash and offset }
GET/v1/suggest?prefix=solar+pan&limit=8Typeahead suggestions
// Answered from an in-memory trie built from query logs, never from the web index { "prefix": "solar pan", "suggestions": ["solar panel cost", "solar panel efficiency", "solar panels for home"] }
POST/v1/sites/{site}/sitemapsSite owner submits a sitemap
// Authorization: Bearer <verified site owner token> { "sitemap_url": "https://energy.example.org/sitemap.xml" } // 202 Accepted: the URLs join the crawl frontier with a priority boost
POSTleaf.Search (internal RPC)What the aggregator sends every index shard
{ "terms": ["solar", "panel", "efficiency"], "phrase": false, "filters": { "lang": "en" }, "k": 200, // local top-k this shard returns "deadline_ms": 45, // leaf stops scoring and returns what it has "index_version": "2026-09-30T22:00Z" }

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

05

Architecture overview

Offline build plane and online query planeDiagram
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.

Crawler and frontier

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.

Leaf shards

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.

Aggregator tree

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.

Re-ranker

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

06

Deep dive: posting lists, fan-out and the ranking cascade

Two questions decide this design

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.

# Per-term layout inside one leaf segment term "solar" doc_freq=41,882,113 block 0 max_score=11.8 ids: gaps [3, 1, 7, 2, ...] tf: [2, 1, 5, ...] block 1 max_score=9.4 ids: gaps [...] tf: [...] positions -> separate file, offset per block # block max scores feed Block-Max WAND

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

One query, two levels of fan-outDiagram
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

StageRuns onCandidates inOutBudget
BM25 plus static qualityevery leafmillions per shard200 per shardabout 40 ms
Merge and dedupeaggregators30,0001,000about 5 ms
Gradient boosted trees, about 300 featuresaggregator1,000300about 15 ms
Transformer cross-encoderGPU re-rank pool30050about 45 ms
Diversity and business rulesfront end5010 per pageabout 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.

Interview answer in one breath

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

07

Decisions and what they cost

Chosen
Partition the index by document

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 scale
Rejected
Partition by term

A 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 fragile
Chosen
Two quality tiers

The 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 half
Alternative
One flat index on NVMe

Simpler 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 pages
Chosen
Batch index plus real-time segment

Daily 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 compact
Rejected
Fully incremental index

Updating 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 everywhere
Chosen
Short-lived result cache keyed on the rewritten query

Caching 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 rate
Alternative
Cache posting list intersections instead

Helps 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 leaves

07

Failure modes

08

Failure modes

🐢
One rack of leaves turns slow

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 query of death

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 bad index build is pushed

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.
🕸️
The crawler falls into a trap

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.
📰
Breaking news outruns the index

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 region goes dark

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

⚠️
Using a relational database with LIKE queries

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.
⚠️
Running the expensive model on every match

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.
⚠️
One aggregator fanning out to every leaf

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.
⚠️
Rebuilding everything to add one fresh page

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.
⚠️
Treating the crawler as a simple loop

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

09

How to handle the interview

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

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.

8

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

11

Growing the design in stages

Rule of thumb

Each stage below exists because some measurement crossed a line. Do not draw the aggregator tree for a site search over two million pages.

Stage 1: a few million pages
One Elasticsearch or Lucene cluster

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.

Stage 2: hundreds of millions of pages
Your own sharded index and a real crawler

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.

Stage 3: a few billion pages
Aggregator tree, replicas and a learned ranker

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.

Stage 4: tens of billions of pages
Quality tiers and a real-time segment

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.

Stage 5: global traffic
Multiple serving regions and neural re-ranking

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.