Designing a Distributed Search Engine: Crawling, Indexing, and Ranking Billions of Pages
Analyze how to design a web-scale search engine like Google. Cover distributed crawling, inverted indexes, index sharding, query processing pipelines, and multi-layer caching.
Why Study a Distributed Search Engine?
A web search engine has to solve two very different hard problems and stitch them together: continuously discovering and reading a large fraction of the public web (tens of billions of pages), and then answering a query against that entire corpus in under 200ms. Google indexes hundreds of billions of pages and serves on the order of 100,000+ queries per second worldwide. No single machine can hold that index or answer that query volume, which makes this one of the richest "distributed systems" interview problems - it forces you to reason about sharding strategy, scatter-gather query fan-out, and caching all at once.
Approach: Split the problem cleanly into two independent systems that meet at one artifact - the index. The crawler's job is to keep the index fresh; the query pipeline's job is to search it fast. Most candidates blur these together and end up designing neither well. Treat them as separate subsystems with a well-defined handoff.
Requirements Analysis
Functional Requirements
- Crawling: Discover and download pages across the web, following links
- Indexing: Build a searchable index from crawled content, updated as new pages appear
- Query Processing: Accept a text query and return ranked, relevant results
- Ranking: Order results by relevance (content match + authority signals)
- Freshness: Re-crawl and re-index pages that change, at a cadence appropriate to how often they change
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Scale | Billions of pages crawled and indexed (100B+ for a full web index) |
| Query Latency | < 200ms end-to-end for a typical query |
| Query Throughput | 100K+ queries/sec at global scale |
| Crawl Politeness | Bounded request rate per domain - never overload a single site |
| Freshness | High-value/fast-changing pages re-crawled within hours; long-tail pages within weeks |
| Availability | 99.99% for the query path (crawling can tolerate more slack) |
High-Level Architecture
Distributed Crawler Architecture
The URL Frontier
The URL frontier is the queue of URLs waiting to be crawled. It isn't a simple FIFO - it has to balance two competing goals: prioritizing important/fast-changing pages, and never hammering any single domain with concurrent requests.
Politeness Policy
Politeness is a hard constraint, not an optimization: crawling too aggressively can effectively DoS a small site, and most sites will simply block a crawler's IP range if it misbehaves. The standard approach:
- Per-domain queues: every URL is routed to a queue keyed by its domain, so a crawler worker only ever has a small, bounded number of requests in flight against any one host.
- Minimum delay between requests to the same domain: even with a per-domain queue, enforce a minimum gap (commonly seconds, sometimes longer) between consecutive requests to the same host.
robots.txtcompliance: fetched once per domain and cached; disallowed paths are never enqueued, and aCrawl-delaydirective (if present) overrides the default delay.- Respecting HTTP response signals: back off further on 429/503 responses, treating them as an explicit "slow down" signal from the origin server.
| Politeness Mechanism | Purpose |
|---|---|
| Per-domain queue + concurrency cap | Prevents overwhelming any single site regardless of overall crawler fleet size |
| Minimum inter-request delay | Keeps steady-state load on a domain low even under sustained crawling |
robots.txt parsing & caching | Respects site owner's explicit crawl preferences (legal/ethical baseline) |
| Backoff on 429/503 | Reacts to the origin's own signal that it's under load |
Deduplication via URL Fingerprinting
The same content is reachable through many different URLs (tracking parameters, session IDs, http vs https, trailing slashes). Before enqueueing a discovered URL, the crawler normalizes it (strip fragments, sort query params, lowercase host) and computes a fingerprint (a hash of the normalized URL) to check against an already-seen set. At web scale this seen-set is far too large to hold as an exact hash set in memory, which is exactly the problem a Bloom filter is built for - a compact probabilistic membership test that says "definitely not seen" or "probably seen," letting the crawler skip the vast majority of duplicate URLs with a small, fixed memory footprint and an acceptable false-positive rate. This guide covers the data structure itself in more depth in the Bloom Filters article - the short version here is that it's the standard tool for this exact "have I seen this URL before" check at billions-of-URLs scale.
Why not an exact hash set? Storing an exact fingerprint for tens of billions of URLs (even at 8 bytes each) runs into hundreds of gigabytes just for the dedup index. A Bloom filter sized for an acceptable false-positive rate (e.g. 1%) can represent the same seen-set in a fraction of the memory, at the cost of occasionally re-skipping a URL that wasn't actually seen before - a tolerable tradeoff for a crawler, since missing a rare page is far cheaper than the memory cost of perfect accuracy.
The Inverted Index
Why Not Scan Every Document?
A naive search ("does this document contain the word 'kubernetes'?") scanned across billions of documents at query time is hopeless at any meaningful latency budget. The inverted index flips the relationship: instead of document -> words it contains, it stores word -> list of documents containing it (a "posting list"). A query for a term becomes a direct lookup into this structure instead of a scan.
Each posting list entry typically stores more than just a document ID - term frequency within that document, positions (for phrase queries), and field information (title vs body) - so the index can support both simple term matching and richer ranking signals without a second pass over the raw document.
| Index Structure | Purpose |
|---|---|
| Term dictionary | Maps each unique term to its posting list location (often itself a compact structure like a sorted array or FST) |
| Posting list | Sorted list of doc IDs containing the term, plus term frequency and positions |
| Document store | Metadata for rendering results (title, URL, snippet source) - separate from the index proper |
This is the same conceptual model used by real systems like Lucene/Elasticsearch, which is why the brief describes this design as "Lucene-style."
Index Sharding Strategy
A single machine cannot hold an inverted index for the entire web, so the index is sharded across many machines. There are two fundamentally different ways to split it, and the choice shapes the entire query path.
| Strategy | How it Works | Query Fan-Out | Tradeoff |
|---|---|---|---|
| Sharding by term | Each shard owns a subset of the vocabulary; a multi-term query hits only the shards holding those specific terms | Narrow (only relevant term shards), but multi-term queries need cross-shard intersection | Hot terms (common words) create unbalanced shard load; rebalancing is hard as vocabulary shifts |
| Sharding by document | Each shard holds a full mini-index for its subset of documents | Every query fans out to (nearly) every shard | Even load distribution and simple rebalancing (just add shards, redistribute doc ranges), but every query pays the full fan-out + merge cost |
In practice, large search engines favor document-based sharding: even though every query touches every shard, each shard does bounded, parallelizable work (find its own local top-K), and the fan-out/merge pattern is far easier to operate and rebalance than trying to keep term-based shards load-balanced as the vocabulary and its skew evolve over time.
Why document sharding wins in practice: Term-based sharding looks appealing because queries touch fewer shards, but real query traffic is dominated by common terms, so a handful of shards holding popular terms become hotspots. Document sharding trades a wider fan-out per query for uniform load and trivial horizontal scaling - and since each shard's per-query work is small and parallel, the wider fan-out doesn't dominate the latency budget the way an unbalanced shard would.
Query Processing Pipeline
- Tokenization: normalize the raw query the same way documents were tokenized at index time (lowercasing, stemming, stopword handling) so terms actually match posting list keys.
- Query parsing: interpret structure - implicit AND between terms, explicit OR, quoted phrases, filters (site:, filetype:).
- Scatter: the query coordinator sends the parsed query to every relevant shard (all shards, under document-based sharding) in parallel.
- Per-shard local top-K: each shard scores only its own documents and returns its best K candidates, not its entire result set - this bounds the data sent back over the network regardless of how many documents actually matched.
- Gather + global merge: the coordinator merges the per-shard top-K lists into a single global ranking (a K-way merge, since each shard's list already arrives sorted).
- Rerank (optional second pass): a more expensive ranking model can rerun on just the merged candidate set - now small (hundreds, not billions) - to refine final order before returning results.
| Pipeline Stage | Latency Budget Concern | Mitigation |
|---|---|---|
| Scatter to shards | Slowest shard determines total latency (tail latency) | Set a per-shard timeout; return best-effort results from shards that responded in time |
| Per-shard scoring | Must be fast on each shard's local subset | Precomputed scores where possible, bounded posting list traversal |
| Global merge | Must not re-scan full result sets | Each shard already caps its output to top-K, so merge cost is small |
Ranking Signals
Ranking a candidate document combines multiple signal families - the exact scoring math is a research topic in its own right, but the conceptual building blocks are:
- Term-frequency-based relevance (TF-IDF / BM25): how often query terms appear in a document, offset by how common those terms are across the whole corpus (so matching a rare term counts more than matching "the"). BM25 is the modern standard, refining raw TF-IDF with saturation (repeating a term 100 times doesn't count 100x as much) and document-length normalization.
- Link-based authority signals (PageRank-style): a document's importance inferred from the link graph - pages linked to by many other important pages rank higher, independent of their own text content. This is what let early Google outperform pure keyword-matching engines.
- Freshness and metadata signals: recency for time-sensitive queries, page quality/spam signals, and (in modern engines) learned ranking models trained on click behavior.
Final ranking is typically a weighted combination of these signal families, often with a fast first-pass score (used for the initial per-shard top-K) followed by a more expensive rerank pass on the smaller merged candidate set - the same two-phase pattern used in the query pipeline's merge step above.
| Signal | What it Captures | Computed |
|---|---|---|
| BM25 (TF-IDF family) | Textual relevance of a document to the query terms | Per-shard, at query time, from posting list stats |
| Link-based authority | Document importance independent of the query | Precomputed offline over the full link graph, stored as a per-document score |
| Freshness | Recency relevance for time-sensitive queries | Precomputed/updated as the crawler revisits pages |
Multi-Layer Caching
| Cache Layer | Scope | Why it Helps |
|---|---|---|
| Popular-query cache | Small, in-memory, per query-server, holding a rolling set of the most frequent queries | Query popularity follows a heavy power-law distribution - a small set of queries accounts for a large fraction of total traffic |
| Distributed query result cache | Larger, shared across query servers (e.g. Redis/Memcached tier) | Absorbs repeat queries that miss the small local cache but are still common enough to be worth serving without a full scatter-gather |
| Shard-local posting list cache | In-memory on each index shard | Avoids re-reading hot posting lists from disk/SSD on every query touching a common term |
Cache invalidation is comparatively gentle here versus other systems: search results tolerate slight staleness far better than, say, an inventory count, so a short TTL (seconds to low minutes) rather than active invalidation is usually sufficient - freshness-critical queries (like breaking news) can be explicitly excluded from caching by the query parser.
Interview tip: Don't just say "add a cache" - explain why query traffic is cacheable in the first place (heavy skew toward popular queries) and why staleness tolerance differs across query types. That's the reasoning interviewers are actually listening for.
Database Schema and Index Metadata
While the inverted index itself is typically a custom on-disk structure (not a relational table), the system still needs relational/metadata stores around it for crawl state, document metadata, and shard routing.
Crawl Queue State
CREATE TABLE crawl_urls (
url_fingerprint BIGINT PRIMARY KEY, -- hash of normalized URL
url TEXT NOT NULL,
domain VARCHAR(255) NOT NULL,
priority SMALLINT DEFAULT 0,
last_crawled_at TIMESTAMP,
next_crawl_at TIMESTAMP,
status VARCHAR(20) DEFAULT 'pending' -- pending, in_progress, done, failed
);
CREATE INDEX idx_crawl_domain ON crawl_urls(domain, next_crawl_at);
CREATE INDEX idx_crawl_priority ON crawl_urls(status, priority DESC, next_crawl_at);Document Metadata
CREATE TABLE documents (
doc_id BIGINT PRIMARY KEY,
url TEXT NOT NULL,
title VARCHAR(500),
content_hash BIGINT NOT NULL, -- detects unchanged content on re-crawl
authority_score FLOAT DEFAULT 0, -- precomputed PageRank-style score
indexed_at TIMESTAMP DEFAULT NOW(),
shard_id INT NOT NULL
);
CREATE INDEX idx_documents_shard ON documents(shard_id);
CREATE INDEX idx_documents_content_hash ON documents(content_hash);Shard Routing
CREATE TABLE index_shards (
shard_id INT PRIMARY KEY,
doc_id_range_start BIGINT NOT NULL,
doc_id_range_end BIGINT NOT NULL,
host VARCHAR(255) NOT NULL,
status VARCHAR(20) DEFAULT 'active'
);
CREATE INDEX idx_shard_range ON index_shards(doc_id_range_start, doc_id_range_end);Why store content_hash? Re-crawling a page that hasn't changed since last visit is wasted indexing work. Comparing the new content's hash against the stored content_hash lets the pipeline skip re-parsing and re-indexing unchanged pages, only paying the cost when content actually differs.
Capacity Estimate
A rough back-of-envelope sizing exercise, useful for grounding the design in real numbers during an interview:
| Metric | Estimate |
|---|---|
| Pages indexed | ~100 billion documents (full web-scale index) |
| Avg unique terms per document | ~500 (after stopword removal, stemming) |
| Posting list entries | ~100B docs x 500 terms = ~50 trillion (doc, term) postings, though heavily compressed via delta-encoding of doc IDs |
| Raw index size (compressed) | Tens of terabytes to low petabytes, depending on compression (delta + variable-byte encoding on posting lists cuts raw size dramatically) |
| Crawl rate needed for freshness | Re-crawling a meaningful fraction of 100B pages within weeks implies sustained crawling in the billions of pages/day range across the whole fleet |
| Query throughput | 100K+ QPS at global scale (Google-scale); a mid-size vertical search product might target 1K-10K QPS |
| Shard count | If each shard machine comfortably serves ~50-100M documents, a 100B-document index needs on the order of 1,000-2,000 shards |
These numbers exist to justify design decisions (why sharding is mandatory, why compression matters, why caching hot queries pays off), not to be memorized exactly - interviewers care that you can derive them from first principles.
Scaling Challenges & Solutions
| Challenge | Solution |
|---|---|
| Overloading small sites during crawl | Per-domain queues, minimum inter-request delay, robots.txt compliance |
| Duplicate URL discovery at billions-of-URLs scale | Bloom filter for URL fingerprint dedup, bounded memory |
| No single machine can hold the full index | Document-based sharding across thousands of index shard machines |
| Query fan-out to every shard adds latency | Per-shard local top-K + timeout-bounded scatter-gather, so tail shards don't block the response indefinitely |
| Common terms/queries dominate traffic | Multi-layer caching (popular-query cache, distributed result cache) exploiting power-law query distribution |
| Re-indexing unchanged pages wastes work | Content-hash comparison skips re-parsing pages that haven't changed since last crawl |
| Posting lists too large to fit in memory | Compression (delta encoding of sorted doc IDs + variable-byte/Golomb coding) |
Key Takeaways
- Separate crawling from query serving as two independent subsystems connected only by the index - they have completely different latency and consistency requirements
- Politeness is a hard constraint on crawling, not an optimization - per-domain rate limiting and robots.txt compliance are non-negotiable at web scale
- The inverted index turns search into a lookup, not a scan - this single data structure choice is what makes sub-200ms search over billions of documents possible at all
- Document-based sharding trades wider fan-out for even load - and is preferred over term-based sharding because query traffic skews heavily toward common terms
- Caching works because query popularity follows a power law - a small set of popular queries accounts for a disproportionate share of traffic, making even a small cache highly effective
Interview tip: When asked to design search, resist the urge to jump straight to "use Elasticsearch." Naming the tool isn't the point - walk through why an inverted index beats scanning, how you'd shard it, and how the scatter-gather query path bounds tail latency. That reasoning is what distinguishes a strong answer from a name-drop.
Follow-Up Questions to Consider
- How would you handle personalized or localized search results without breaking the shared index/cache model?
- How would you detect and handle spam pages or link farms trying to manipulate authority scores?
- How would you support autocomplete/query suggestions with sub-50ms latency?
- How would you re-balance index shards as the corpus grows from 10B to 100B documents without downtime?
- How would you extend this design to support image or video search, where the "content" isn't plain text?
Real search engine trivia: Google's original 1998 paper described PageRank as treating a link from page A to page B as a "vote" for B, weighted by A's own importance - a recursive definition solved via an iterative computation over the entire link graph. Modern web search ranking has since layered hundreds of additional signals and learned ranking models on top, but the core insight - that link structure carries independent signal from text content - remains foundational to how large-scale search ranking works today.