At a glance#
| Number | Value | What it drives |
|---|---|---|
| Searches | ~14B / day (2025 estimate) | ~160,000 / s on average |
| Design load | 10K → 100K QPS peak | more replicas per shard, result cache (~30 % hits) |
| Pages | 50B → 500B (design); Google ~400B docs (2020) | thousands of document shards |
| Query latency | < 500 ms p99 | understand 20 · retrieve 100 · score 200 · snippets 50 · respond 30 ms |
| Inverted index | 25T postings · 300 TB raw · ~60 TB compressed | ~180 TB with 3 replicas: sharded, hot parts in memory |
| Page store | ~2.5 PB (50B × 50 KB compressed) | the biggest storage item, not the index |
| Scoring | ~10,000 candidates × ~10 μs ≈ 100 ms | cap candidates before scoring |
| Freshness | breaking news < 15 min · most < 24 h | fresh tier + priority crawl |
Architecture#
DiagramSearch on one page: crawl and index offline, scatter-gather on the query path
%% title: Search on one page: crawl and index offline, scatter-gather on the query path
flowchart TD
U@{ icon: "sd:users", label: "Users<br/>~14B searches a day" } --> FE["Query frontend<br/>spell · tokenize · synonyms · intent · 20 ms"]:::edge
FE --> QC@{ icon: "sd:cache", label: "Result cache<br/>short TTL · ~30 % hits" }
FE ==> RT["Root / merger<br/>scatter to every shard · gather local top-k"]:::hot
RT ==> SH@{ icon: "sd:index", label: "Document-sharded index<br/>thousands of shards · 3–10× replicas" }
RT --> FT@{ icon: "sd:index", label: "Fresh tier · in memory<br/>priority-crawled news, in minutes" }
RT ==> RK["Ranker<br/>BM25 · PageRank · freshness · location · clicks<br/>snippets for the top 10"]:::core
FR@{ icon: "sd:queue", label: "URL frontier<br/>priority · 1 req/s per domain" } --> CW@{ icon: "sd:robot", label: "Crawler workers<br/>robots.txt · async fetch" }
CW -.->|"new links"| FR
CW --> PS@{ icon: "sd:database", label: "Page store<br/>~2.5 PB compressed" }
PS --> IB["Indexer<br/>tokenize · build segments · PageRank"]:::service
IB -->|"finished segments"| SH
class U client
class QC cache
class SH,FT,PS store
class FR queue
class CW service- Only serving is on the user's clock. A slow crawl makes results staler, never slower.
- Shards are by document: each holds every term for its slice, so a lost shard drops pages, not words.
DiagramScatter-gather: the slowest shard sets the pace
%% title: Scatter-gather: the slowest shard sets the pace flowchart TD RT["Root · fans out to ~1,000 shards<br/>all fast only ~0.004 % of the time"]:::hot --> S1["Shard 1 · docs 0–1M<br/>intersect postings · local top-k"]:::service RT --> S2["Shard 2 · docs 1M–2M<br/>local top-k"]:::service RT --> SN["Shard N · primary slow"]:::bad SN -->|"no answer in a few ms"| HR["Hedged request to a replica<br/>take whichever answers first"]:::queue S1 --> MG["Merge local top-k → ~10,000 candidates<br/>deadline: return what has arrived"]:::ok S2 --> MG HR --> MG
Key flows#
DiagramA query: "best pizza near me" in under 500 ms
%% title: A query: "best pizza near me" in under 500 ms
sequenceDiagram
autonumber
participant U as User
participant F as Query frontend
participant R as Root / merger
participant S as Index shards (all)
participant K as Ranker
U->>F: best pizza near me
F->>F: spell · tokenize · synonyms · local intent (20 ms)
alt popular query, cached
F-->>U: cached results
else cache miss
F->>R: parsed query
R->>S: same query to every shard in parallel
S-->>R: local top-k each (100 ms budget)
R->>K: ~10,000 merged candidates
K->>K: BM25 · PageRank · freshness · location · clicks → top 100 (200 ms)
K->>K: snippets for the top 10 (50 ms)
K-->>U: results page (~400 ms)
endDiagramTail latency: one shard is slow
%% title: Tail latency: one shard is slow
sequenceDiagram
autonumber
participant R as Root / merger
participant P as Shard 7 primary
participant Q as Shard 7 replica
R->>P: query
Note over R: no answer after a few ms
R->>Q: hedged request, same query
alt replica answers first
Q-->>R: local top-k
Note over R: use it · ignore the late primary
else both miss the deadline
Note over R: return without shard 7 · slightly incomplete beats slow
endDiagramBreaking news: searchable within 15 minutes
%% title: Breaking news: searchable within 15 minutes sequenceDiagram autonumber participant D as News detector participant C as Crawler participant T as Fresh tier participant R as Root / merger D->>D: query spike · new entities D->>C: trusted sources jump the frontier C->>T: index new pages in seconds R->>T: query the fresh tier R->>R: + main index · boost freshness, trust R->>R: cache hot query, short TTL Note over T: merged into the main index later
Data model#
DiagramCore entities
%% title: Core entities
classDiagram
class Shard {
+shardId : int
+docRange : slice of doc IDs
+replicas : 3 to 10
}
class PostingsList {
+term : string
+docFrequency : int · for IDF
+postings : sorted by docId
}
class Posting {
+docId : int
+termFrequency : int
+positions : int list
+size : ~12 B
}
class Document {
+docId : int
+url : string
+pageRank : float
+lastCrawled : timestamp
+contentFingerprint : hash · dup and spam check
}
class CrawlURL {
+url : string
+priority : importance + freshness
+lastCrawl : timestamp
+depth : int · trap guard
}
Shard "1" --> "many" PostingsList : every term for its docs
PostingsList "1" --> "many" Posting : holds
Posting "many" --> "1" Document : points to
CrawlURL "1" --> "0..1" Document : fetched as- Partition key is the document ID; postings sort by doc ID so intersections are an O(n + m) merge, rarest term first.
- Forward index (~1 KB per doc) is ~50 TB; postings are bounded by docs × distinct terms per doc, not by vocabulary size.
Lifecycle#
DiagramA page's state machine, from link to results
%% title: A page's state machine, from link to results stateDiagram-v2 [*] --> DISCOVERED: link extracted DISCOVERED --> QUEUED: new URL, deduplicated QUEUED --> FETCHED: crawled, polite rate FETCHED --> FAILED: error or robots.txt FETCHED --> REJECTED: spam, duplicate or trap FETCHED --> FRESH_TIER: breaking news FETCHED --> INDEXED: segment built FRESH_TIER --> SERVING: searchable in minutes INDEXED --> SERVING: segment pushed to shards SERVING --> QUEUED: recrawl due FAILED --> [*] REJECTED --> [*] class SERVING ok class FAILED,REJECTED bad class QUEUED queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| Index partitioning | By document | By term | even load; common terms would make hot shards |
| Serving more QPS | More replicas per shard | More shards | every query hits every shard anyway |
| Tail latency | Stage budgets, deadlines, hedged requests | Wait for every shard | 0.99^1000 ≈ 0.004 % all fast |
| Crawl and index | Async pipelines feeding finished segments | Updates on the query path | slow crawl = staler, never slower |
| Freshness | Small in-memory fresh tier | Rebuilding the main index more often | merging hundreds of TB takes hours |
| Index consistency | Eventual, stale within hours | Strong across shards | strict agreement would slow every query |
| Text relevance | BM25, then PageRank and other signals | Raw TF-IDF | term-frequency saturation, length normalization |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Breaking news, 10× spike | fast, slightly stale results | short-TTL result cache, more replicas for hot shards |
| One shard slow | normal speed, rarely a missing hit | per-shard deadline, hedged request to a replica |
| Shard unavailable | results silently missing a slice | critical alert even though queries still return |
| Crawler or indexer stalls | new pages show up late | freshness alert; serving unaffected |
| Link farms and spam pages | junk in results | link graph analysis, content fingerprints, ML classifiers |
| Crawl trap (endless URLs) | crawl budget wasted | trap URL patterns, depth limits, unique-content ratio |
Say it in the interview#
- "Shard by document, replicate for QPS." More shards grow the corpus; more replicas grow throughput.
- At 1,000-way fan-out the tail is the median: 0.99^1000 ≈ 0.004 %, so deadlines, partial results and hedged requests.
- Only serving is on the clock: crawling and indexing hand over finished segments asynchronously.
- Budget every stage: 20 + 100 + 200 + 50 + 30 ms, so one slow stage can't eat the next one's time.
- Freshness is a tier, not a rebuild: news goes into a small in-memory tier and merges into the main index later.
- Size the index from docs × terms per doc: 25T postings is ~60 TB compressed; the 2.5 PB page store is the big item.
- Cap candidates before scoring: ~10,000 × ~10 μs ≈ 100 ms of the 200 ms scoring budget; doubling candidates doubles it.