System design · 18 of 20

Designing a search engine

Crawling and the frontier, deduplication, index construction and compression, document against term sharding, fan-out serving and tail latency, ranking, and vector retrieval at billion scale.

Updated 2026-09-18
On this page

The search and analytics article explained what an inverted index is and how BM25 ranks documents. This one builds the engine around them: a crawler that must not melt the web, an index of fifty trillion postings that has to be rebuilt continuously, a query that fans out to a thousand machines and comes back in under two hundred milliseconds, and a ranking stack in which the text match is only the first of a hundred signals.

The map

Read this first when short on time. Every branch is a section below.

Figure 1. The whole article on one page. Every branch is a section below; fold what you know, open what you do not.

What it must do, and at what size

It must discover documents on the open web, keep a current copy of what they say, and answer an arbitrary query with ten good results fast enough that the page feels instant. It must not, in this design, personalize deeply per user, answer questions directly rather than linking, or index anything behind authentication.

Corpus     ~100 billion documents indexed, many more discovered and rejected
Postings   ~500 useful terms per document -> ~50 trillion postings
           compressed to ~1-2 bytes each -> 50-100 TB of index

Queries    ~100,000/s at peak
Latency    ~200 ms end to end, of which ~100 ms is retrieval and ranking
Repeats    30-50% of queries have been seen recently -> result caching matters

Fan-out    index sharded across ~1,000 leaves -> every query touches all of them
           if one leaf's p99 is 100 ms, a 1,000-way fan-out hits it
           on essentially every query

That last block is the single most important consequence of the whole architecture and worth stating plainly. When a query must consult a thousand machines and wait for all of them, the slowest one determines the response time. A per-machine 99th percentile that sounds excellent becomes the common case at the query level, because with a thousand independent draws you almost always hit at least one. Tail latency stops being a statistical curiosity and becomes the dominant design constraint.

flowchart TD
  W["the web"] --> CR["crawler and frontier"]
  CR --> PR["parse, canonicalise, dedupe"]
  PR --> IDX["index build, batch"]
  PR --> GR[["link graph"]]
  IDX --> SEG[["index segments, sharded by document"]]
  Q["query"] --> RT["root"]
  RT --> SEG
  SEG --> RT
  RT --> RK["rerank with the full model"]
  RK --> RES["ten results"]
Figure 2. Two systems that meet at the index. The left half is a batch pipeline measured in hours; the right half is a fan-out tree measured in milliseconds.

Crawling

A crawler is a distributed breadth-first traversal of a graph that is hostile, infinite and owned by other people. Almost every rule in it is about not being a nuisance.

The frontier is a priority queue of URLs to fetch, fed by links found in crawled pages and by sitemaps. It is never plain breadth-first: a page's priority comes from its estimated importance, how long since it was last fetched, and how often it changes. The web is effectively infinite, so the frontier's job is to spend a finite budget on the pages that matter.

Politeness is the hard constraint. Requests are queued per host, not globally, with a small number of concurrent connections and a delay between them, because a crawler that hits one site with a thousand parallel requests is indistinguishable from an attack. The robots file is fetched, cached and obeyed, and a site that asks for a slower rate gets it. This is why the architecture is a queue per host rather than one global work queue — the unit of scheduling is the host, not the URL.

flowchart TD
  S["seed URLs and sitemaps"] --> F[["frontier, prioritised"]]
  F --> HQ["one queue per host, limited concurrency"]
  HQ --> FE["fetch, respecting robots and delay"]
  FE --> PA["parse, canonicalise, dedupe"]
  PA --> L["new links discovered"]
  L --> F
  PA --> RS["recrawl scheduled from the change rate"]
  RS --> F
Figure 3. The crawl loop. Priority decides what to fetch, the per-host queue decides how fast, and every fetched page feeds both the index and the frontier.

Recrawl scheduling is a prediction problem: a news front page changes every few minutes and an archived document never changes again. Fetching both daily is simultaneously too slow and a waste, so the crawler estimates each page's change rate from its own history and schedules accordingly.

Watch out

The web is full of traps that generate infinite unique URLs: calendars with a next-month link forever, faceted filters in every combination, and session ids that make every fetch a new address. A crawler without limits on depth, on URLs per host and on parameter combinations will happily spend its entire budget on one site's calendar.

Parsing, canonicalisation and duplicates

A fetched page becomes a document through several steps, and the interesting ones are about identity rather than content.

Extraction separates the main content from navigation, footers and advertisements, because indexing boilerplate makes every page on a site look similar. Pages that build their content with scripts have to be rendered rather than merely parsed, which costs orders of magnitude more, so rendering is a decision made per page rather than universally.

Canonicalisation answers "is this the same document?". The same content is reachable at many addresses — with and without a trailing slash, with tracking parameters, on both protocols, on several hostnames — and indexing them separately splits the ranking signals across duplicates and wastes the crawl budget. URLs are normalised, redirects followed, and the page's own canonical declaration respected.

Near-duplicate detection handles the harder case: syndicated articles, product pages that differ in one field, sites that copy others wholesale. The standard technique reduces each document to a compact fingerprint built from overlapping word sequences, chosen so that similar documents produce similar fingerprints and near-duplicates can be found by looking for fingerprints within a small distance. Clusters are collapsed to a single representative — the original where it can be identified, the most authoritative otherwise.

Building the index

Index construction is a sort. Documents produce postings — one per term occurrence — and the pipeline groups them by term, orders each group by document, compresses it and writes immutable segments.

Compression is not an optimisation here, it is what makes the index possible. Fifty trillion postings stored naively would be hundreds of terabytes of document ids alone; stored as deltas between consecutive ids and packed into the minimum number of bits, the same data is a fraction of that. Because postings are read sequentially during query evaluation, decompression is cheap and the smaller index means more of it fits in memory — which is where the real speed comes from.

Tiering is the structural idea that makes serving affordable. The index is split into a small tier holding the highest-quality documents and much larger tiers holding the rest. Most queries are answered entirely from the first tier, and only queries that fail to find enough good results there descend. That turns "search a hundred billion documents" into "search a few hundred million, usually".

Freshness needs a second mechanism, because a batch build takes hours and news does not wait. A small live index holds very recent documents, is updated continuously, and is merged with the main index at query time. It is the same pattern as an LSM-tree's memtable, and it exists for the same reason: batch efficiency for the bulk, a small mutable layer for the recent.

Sharding: by document or by term

The index is far too large for one machine, and there are exactly two ways to divide it. This choice determines the shape of query serving completely.

Partition by document · every shard holds complete documents shard 1: docs 1–100M all terms in them shard 2: docs 100–200M all terms in them every query asks every shard Partition by term · every shard holds complete posting lists shard 1: terms a–m whole lists shard 2: terms n–z whole lists few shards per query, but common terms are hot
Figure 4. The two partitionings. Document partitioning spreads load evenly and asks everyone; term partitioning asks few machines and overloads whichever one holds the popular terms.

Document partitioning gives each shard a slice of the corpus and the complete index for those documents. Every query goes to every shard, each returns its own best candidates, and a root merges them. Load is even, updates are local — a new document affects one shard — and adding capacity is adding shards.

Term partitioning gives each shard the complete posting list for a subset of terms. A two-word query touches two shards instead of a thousand, which sounds far better until the details bite: posting lists for common terms are enormous and must be shipped across the network to be intersected, the shard holding the popular terms is permanently hot, and adding a document touches every shard that holds one of its terms.

Document partitioning wins in practice, and the price it pays is the fan-out and its tail — which the next section is entirely about.

Serving a query

A query enters a root, fans out to every leaf, and comes back merged. The structure is simple; making it fast at a thousand-way fan-out is not.

sequenceDiagram
  autonumber
  participant C as Client
  participant R as Root
  participant L as Leaves
  participant M as Reranker
  C->>R: query
  R->>R: result cache hit? if so, return
  R->>L: fan out to about 1,000 shards
  L-->>R: each returns its top few, scored cheaply
  Note over R,L: hedge to a replica for leaves that are slow
  R->>M: merged candidates, a few hundred
  M-->>R: reranked with the expensive model
  R-->>C: ten results
Figure 5. The fan-out tree. Each leaf does cheap scoring on its own slice; expensive ranking runs once, at the root, on the merged candidates.

Tail latency is the defining problem. With a thousand leaves, the probability that at least one is slow is essentially one, so the query's latency is governed by the worst machine rather than the typical one. Three mechanisms fight it. Hedged requests: after waiting a short time, send the same request to a replica and take whichever answers first — this costs a few percent of extra work and removes most of the tail. Answer with what arrived: if a handful of leaves have not responded by the deadline, return results from the rest, since missing a fraction of the corpus changes the top ten far less often than being a second late hurts. And keep leaves uniform, because a single overloaded or degraded machine damages every query rather than a thousandth of them.

Caching does the rest of the work. A large share of queries are repeats, so a result cache at the root answers them without touching the index at all, and posting lists for common terms stay in memory at the leaves. The cache is invalidated when a new index version is published — which is another reason index builds are published as discrete, versioned events rather than trickling in.

Ranking

Retrieval finds documents containing the words. Ranking decides which ten are worth showing, and it is where nearly all of the quality lives.

The structure is the funnel seen throughout this series, sized by cost. Each leaf retrieves thousands of matching documents and scores them with something cheap — a text-match score plus a precomputed quality signal — keeping only its best few. The root merges those into a few hundred candidates and reranks them with a model that can afford to be expensive because it runs once per query on a small set.

flowchart TD
  A["each leaf retrieves thousands of matches"] --> B["cheap score: text match plus a quality signal"]
  B --> C["each leaf keeps its best few"]
  C --> D["root merges a few hundred candidates"]
  D --> E["expensive model reranks them"]
  E --> F["ten results"]
Figure 6. The ranking funnel across the tree. Cheap scoring runs everywhere on many documents; the expensive model runs once, at the root, on few.

The signals that model uses fall into four groups. Text match, with BM25 as the baseline and proximity, field weighting and synonym handling on top. Authority, from the link graph: a link is a citation, links from well-cited pages count for more, and the anchor text describes the target in other people's words — which is often better than the page's description of itself. Context: freshness for queries that want it, language, location. And behaviour: aggregated interaction with results for similar past queries, which is the most powerful signal available and the most easily gamed.

Combining these is a learning to rank problem — a model trained on human relevance judgements and behavioural data, evaluated with a graded metric rather than accuracy, since the order of the top few matters far more than anything below.

Vector retrieval at a billion documents

Keyword retrieval cannot match "why is my laptop hot" to a page titled "thermal throttling in notebooks". Embeddings solve that, and the engineering question is how to search a billion of them inside the same latency budget.

Exact nearest-neighbour search is out immediately: comparing a query vector against a billion stored vectors is hundreds of billions of multiplications. So vector retrieval follows the same shape as the inverted index — shard the vectors across machines, run an approximate search within each shard, and merge — which means it fits the existing fan-out tree rather than needing a separate architecture.

Inside a shard the trade is memory against recall. A graph index gives excellent recall and speed but keeps the full vectors plus the graph in memory, which at a billion vectors of several hundred dimensions is prohibitive. Quantisation compresses each vector into a short code, cutting memory by an order of magnitude at some loss of precision, and is usually combined with a clustering index so only a few clusters are searched. The common production shape is quantised candidates followed by exact rescoring of the top few hundred against full vectors fetched from disk.

Hybrid retrieval is not optional. Vectors are weak exactly where keywords are strong — product codes, error strings, rare names, quoted phrases — so both retrievers run and their ranked lists are fused, commonly by a rank-based method that needs no score calibration between two systems that score on different scales.

Watch out

Changing the embedding model invalidates every stored vector. Re-embedding a billion documents is a full rebuild costing significant compute and time, during which the old and new vector spaces cannot be compared. Plan model upgrades as index migrations, with both versions live during the transition — not as a configuration change.

Spam and adversaries

Ranking position converts directly into money, so every signal in the previous section is under continuous attack. This is not a subsystem bolted on afterwards; it shapes which signals are usable at all.

The guiding principle is to prefer signals that are expensive to fake. Keyword density on a page is free to manipulate, which is why it stopped being useful decades ago. Links from established, well-cited sites are expensive to obtain, which is why link-based authority survived — until link-selling made it worth attacking too, at which point graph analysis to detect unnatural linking patterns became necessary.

The defences form layers: content classifiers that detect low-quality or machine-generated text, graph analysis that finds link schemes by their structure, behavioural checks that notice results nobody engages with, and manual action as a backstop for cases the automation cannot reach. And because it is adversarial, the details of the signals cannot be published — a constraint that makes this the one part of the system that is deliberately not documented.

What breaks first

  1. Fan-out tail latency. The structural problem: a thousand-way fan-out makes every machine's tail the query's typical case. Hedging, deadlines with partial results and uniform leaves are the tools, and adding shards makes it worse rather than better.
  2. Index build time. A full rebuild is hours of batch work, which bounds how quickly a ranking or schema change can reach production, and makes the live tier essential rather than a nicety.
  3. Crawl capacity against freshness. Politeness limits how fast the web can be recrawled, so freshness is allocated rather than achieved — spend it on pages that change and matter.
  4. Ranking model cost. The reranker runs on every query, so its size is bounded by the latency budget and by hardware cost, which is the constraint that pushes work into cheaper earlier stages.

Recap

  • A thousand-way fan-out makes tail latency the dominant constraint: with a thousand leaves, a one-in-a-hundred slow response happens on essentially every query.
  • The crawler schedules per host rather than per URL, obeys robots rules, and spends a finite budget on pages estimated to be important and likely to have changed.
  • Recrawl frequency is a prediction of each page's change rate; the web's infinite URL traps need explicit depth and parameter limits.
  • Canonicalisation decides what counts as one document, and near-duplicate detection collapses syndicated and copied content by fingerprint similarity.
  • Index construction is a giant sort producing immutable compressed segments; delta-encoded, bit-packed postings are what make fifty trillion of them fit.
  • Tiering means most queries search a few hundred million good documents rather than a hundred billion, descending only when results are insufficient.
  • A small live index merged at query time provides freshness, exactly as an LSM-tree's memtable does for writes.
  • Document partitioning spreads load evenly and asks every shard; term partitioning asks few shards but concentrates popular terms and complicates updates. Document partitioning wins.
  • Fight the tail with hedged requests to replicas, deadlines that answer with whatever arrived, and uniform leaves so no single machine degrades every query.
  • A result cache at the root absorbs the large fraction of repeated queries, and is invalidated when a new index version is published.
  • Ranking is a funnel: cheap scoring at the leaves, expensive reranking once at the root on a few hundred merged candidates.
  • Signals group into text match, link-graph authority with anchor text, context such as freshness and location, and behavioural data — the strongest signal and the most gamed.
  • Vector retrieval shards the same way as the inverted index, uses quantisation to fit memory, and rescores a small candidate set exactly.
  • Keywords and vectors are fused rather than chosen between, because each is weak exactly where the other is strong.
  • Changing the embedding model is an index migration, not a configuration change: every stored vector must be regenerated.
  • Prefer ranking signals that are expensive to fake, and treat spam defence as a shaper of the design rather than a later addition.

Questions

Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail from a real system.

Why does tail latency dominate a search engine's design?

Because a query fans out to around a thousand shards and must wait for them, so the slowest machine sets the response time — with a thousand independent draws, a one-in-a-hundred slow response occurs on almost every query.

  • A per-machine 99th percentile that sounds excellent becomes the typical query experience.
  • Adding shards to increase capacity makes the problem worse, not better.
  • The defences are hedged requests, deadlines with partial results, and keeping leaves uniform.

This is the clearest example in the series of a statistic that is harmless in isolation and decisive under fan-out.

What are hedged requests and what do they cost?

After waiting a short time for a leaf to respond, the root sends the same request to a replica and uses whichever answers first — trading a few percent of extra work for the removal of most of the tail.

  • The delay before hedging is tuned so that only genuinely slow responses trigger a duplicate.
  • It works because slowness is usually a property of a machine at that moment rather than of the request.
  • Without a delay it would double the load, which is why naive duplication is not the same technique.

It is one of the highest-leverage latency techniques available in any fan-out system, not only search.

Why does a crawler queue per host rather than globally?

Because politeness is a per-host constraint: a crawler making a thousand parallel requests to one site is indistinguishable from an attack, so the unit of scheduling has to be the host with limited concurrency and a delay between fetches.

  • Robots rules and crawl-delay directives are fetched, cached and obeyed per host.
  • A global queue would naturally cluster requests to whichever site had just been discovered.
  • It also bounds the damage a single misbehaving site can do to crawl throughput.

It is a case where the dominant architectural constraint comes from being a good citizen rather than from capacity.

How does a crawler decide what to fetch next?

From a priority queue that weighs estimated page importance, time since last fetch and predicted change rate — because the web is effectively infinite and the crawl budget is not.

  • Plain breadth-first spends the budget on whatever happens to be linked most, which is not the same as what matters.
  • Change rate is learned per page from crawl history: news front pages in minutes, archives in months.
  • Traps such as infinite calendars and faceted filters need explicit depth and parameter limits.

Freshness is therefore allocated rather than achieved, which is why it is a ranking signal with a cost attached.

Why is canonicalisation important?

Because the same content is reachable at many addresses, and indexing them separately splits ranking signals across duplicates, wastes crawl budget and can show the same page several times in one result set.

  • Normalising URLs, following redirects and honouring the page's canonical declaration resolve most of it.
  • Near-duplicates need a fingerprint over overlapping word sequences so that similar documents produce similar fingerprints.
  • Clusters are collapsed to one representative, preferring the original or the most authoritative copy.

Getting identity wrong degrades ranking quality in a way that is very hard to diagnose from the results alone.

Why is posting list compression essential rather than an optimisation?

Because fifty trillion postings stored naively would be hundreds of terabytes of document ids alone; delta encoding and bit packing shrink that enough that far more of the index fits in memory, which is where the speed actually comes from.

  • Postings are read sequentially during evaluation, so decompression is cheap and predictable.
  • Smaller indexes also mean faster merges and faster builds.
  • Skip structures let an intersection jump forward rather than decode everything.

It is a case where a storage-format decision is a latency decision in disguise.

What is index tiering and why does it work?

Splitting the index into a small tier of high-quality documents and larger tiers of everything else, so that most queries are satisfied without ever touching the bulk of the corpus.

  • It converts "search a hundred billion documents" into "search a few hundred million, usually".
  • Queries descend to lower tiers only when the top tier yields too few good results.
  • It works because query popularity and document quality are both heavily skewed.

The same structural idea appears as hot and cold tiers in storage and as candidate generation in ranking.

How does a batch-built index stay fresh?

A small live index holds very recent documents, is updated continuously, and is merged with the main index at query time — the same arrangement as an LSM-tree's in-memory layer over its on-disk levels.

  • A full rebuild takes hours, which is far too slow for news or newly published pages.
  • The live tier is small enough to be mutable and to be searched cheaply on every query.
  • It is folded into the main index at the next build.

Batch efficiency for the bulk with a small mutable layer for the recent is one of the most reusable patterns in the series.

Document partitioning or term partitioning?

Document partitioning: each shard holds complete documents, so load is even, updates are local and capacity scales by adding shards — at the cost of every query touching every shard.

  • Term partitioning touches few shards per query but concentrates popular terms on one machine and requires shipping enormous posting lists to intersect them.
  • Adding a document under term partitioning touches every shard holding one of its terms.
  • The price of document partitioning is the fan-out tail, which is addressable with hedging and deadlines.

Choosing the partitioning determines the entire serving architecture, so it is the first decision to make and the hardest to change.

What happens if a few shards do not respond in time?

The root returns results from the shards that did answer, because missing a small fraction of the corpus rarely changes the top ten, while waiting past the deadline always hurts.

  • Partial results should be recorded, since a shard that is persistently late is a fault rather than noise.
  • Hedging to a replica catches most slow leaves before the deadline is reached.
  • The judgement is explicit: a good answer now beats a perfect answer late.

Designing for partial results is what makes a thousand-way fan-out survivable at all.

Why does ranking happen in stages?

Because the expensive model cannot run on thousands of documents inside the latency budget: leaves score many candidates cheaply and keep a few, and the root reranks a few hundred merged candidates with the full model.

  • Each stage is more expensive per item and sees fewer items, which bounds the total cost.
  • Cheap scoring uses text match plus precomputed quality signals that need no lookups.
  • The reranker's size is limited by the latency budget, which pushes work back into earlier stages.

It is the same funnel as feed ranking and hybrid retrieval, which is why the pattern is worth recognising once.

Why is anchor text such a useful ranking signal?

Because it describes a page in other people's words rather than its own, which is both more honest and often better phrased for how people actually search.

  • A page may never contain the term everyone uses to refer to it, while the links to it do.
  • It is part of the broader link-graph signal, where a link acts as a citation and links from well-cited pages count for more.
  • It is also attackable, which is why link-scheme detection became necessary.

The general lesson is that signals produced by third parties are more trustworthy than signals produced by the document's author.

How do you search a billion vectors inside the latency budget?

Shard the vectors across machines, run an approximate search within each shard, and merge — the same fan-out shape as the inverted index, so it reuses the existing serving tree.

  • Exact search is out: a billion comparisons per query is hundreds of billions of multiplications.
  • Within a shard, quantisation cuts memory by an order of magnitude at some precision cost, usually combined with clustering so only a few clusters are scanned.
  • A final exact rescoring of the top few hundred against full vectors recovers most of the lost precision.

Recognising that vector retrieval fits the existing architecture, rather than needing a new one, is the main practical insight.

Why run keyword and vector retrieval together?

Because they fail in opposite places: vectors find paraphrases and questions that share no words with the document, while keywords find exact tokens — part numbers, error codes, rare names, quoted phrases — that embeddings blur.

  • The two ranked lists are fused by a rank-based method, which needs no score calibration between systems that score differently.
  • Running both costs more than running either, and the quality difference is large enough to justify it.
  • Each can also act as a fallback when the other's infrastructure is degraded.

Treating them as complements rather than as competing approaches is the current consensus in production search.

What happens when you want to change the embedding model?

Every stored vector becomes invalid, because vectors from two different models are not comparable — so it is a full re-embedding of the corpus, which is an index migration with real compute cost and a transition period.

  • Both versions must be live during the change, since a partially migrated index would mix incomparable spaces.
  • At a billion documents this is a significant batch job, not a deployment.
  • It is a strong argument for keeping the original documents so vectors can always be regenerated.

Teams that treat the embedding model as a configuration value discover this the first time they upgrade it.

How does adversarial pressure shape which ranking signals are usable?

It eliminates any signal that is cheap to fake: keyword density stopped being useful once it was trivially manipulated, and link authority survived only because obtaining links from established sites is expensive — and even that needed graph analysis once link selling emerged.

  • The design principle is to prefer signals whose manipulation costs the attacker real money or real reputation.
  • Layered defences combine content classifiers, graph analysis, behavioural checks and manual action.
  • Because it is adversarial, the details cannot be published, which makes this the one deliberately undocumented part of the system.

Spam resistance is therefore not a subsystem added later but a constraint on what the ranking stack can be built from.