Expert Case Studies

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.

August 14, 2026
case studysearch engineweb crawlerinverted indexshardingrankingscale

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

  1. Crawling: Discover and download pages across the web, following links
  2. Indexing: Build a searchable index from crawled content, updated as new pages appear
  3. Query Processing: Accept a text query and return ranked, relevant results
  4. Ranking: Order results by relevance (content match + authority signals)
  5. Freshness: Re-crawl and re-index pages that change, at a cadence appropriate to how often they change

Non-Functional Requirements

RequirementTarget
ScaleBillions of pages crawled and indexed (100B+ for a full web index)
Query Latency< 200ms end-to-end for a typical query
Query Throughput100K+ queries/sec at global scale
Crawl PolitenessBounded request rate per domain - never overload a single site
FreshnessHigh-value/fast-changing pages re-crawled within hours; long-tail pages within weeks
Availability99.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:

  1. 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.
  2. 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.
  3. robots.txt compliance: fetched once per domain and cached; disallowed paths are never enqueued, and a Crawl-delay directive (if present) overrides the default delay.
  4. Respecting HTTP response signals: back off further on 429/503 responses, treating them as an explicit "slow down" signal from the origin server.
Politeness MechanismPurpose
Per-domain queue + concurrency capPrevents overwhelming any single site regardless of overall crawler fleet size
Minimum inter-request delayKeeps steady-state load on a domain low even under sustained crawling
robots.txt parsing & cachingRespects site owner's explicit crawl preferences (legal/ethical baseline)
Backoff on 429/503Reacts 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 StructurePurpose
Term dictionaryMaps each unique term to its posting list location (often itself a compact structure like a sorted array or FST)
Posting listSorted list of doc IDs containing the term, plus term frequency and positions
Document storeMetadata 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.

StrategyHow it WorksQuery Fan-OutTradeoff
Sharding by termEach shard owns a subset of the vocabulary; a multi-term query hits only the shards holding those specific termsNarrow (only relevant term shards), but multi-term queries need cross-shard intersectionHot terms (common words) create unbalanced shard load; rebalancing is hard as vocabulary shifts
Sharding by documentEach shard holds a full mini-index for its subset of documentsEvery query fans out to (nearly) every shardEven 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

  1. 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.
  2. Query parsing: interpret structure - implicit AND between terms, explicit OR, quoted phrases, filters (site:, filetype:).
  3. Scatter: the query coordinator sends the parsed query to every relevant shard (all shards, under document-based sharding) in parallel.
  4. 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.
  5. 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).
  6. 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 StageLatency Budget ConcernMitigation
Scatter to shardsSlowest shard determines total latency (tail latency)Set a per-shard timeout; return best-effort results from shards that responded in time
Per-shard scoringMust be fast on each shard's local subsetPrecomputed scores where possible, bounded posting list traversal
Global mergeMust not re-scan full result setsEach 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.

SignalWhat it CapturesComputed
BM25 (TF-IDF family)Textual relevance of a document to the query termsPer-shard, at query time, from posting list stats
Link-based authorityDocument importance independent of the queryPrecomputed offline over the full link graph, stored as a per-document score
FreshnessRecency relevance for time-sensitive queriesPrecomputed/updated as the crawler revisits pages

Multi-Layer Caching

Cache LayerScopeWhy it Helps
Popular-query cacheSmall, in-memory, per query-server, holding a rolling set of the most frequent queriesQuery popularity follows a heavy power-law distribution - a small set of queries accounts for a large fraction of total traffic
Distributed query result cacheLarger, 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 cacheIn-memory on each index shardAvoids 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

sql
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

sql
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

sql
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:

MetricEstimate
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 freshnessRe-crawling a meaningful fraction of 100B pages within weeks implies sustained crawling in the billions of pages/day range across the whole fleet
Query throughput100K+ QPS at global scale (Google-scale); a mid-size vertical search product might target 1K-10K QPS
Shard countIf 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

ChallengeSolution
Overloading small sites during crawlPer-domain queues, minimum inter-request delay, robots.txt compliance
Duplicate URL discovery at billions-of-URLs scaleBloom filter for URL fingerprint dedup, bounded memory
No single machine can hold the full indexDocument-based sharding across thousands of index shard machines
Query fan-out to every shard adds latencyPer-shard local top-K + timeout-bounded scatter-gather, so tail shards don't block the response indefinitely
Common terms/queries dominate trafficMulti-layer caching (popular-query cache, distributed result cache) exploiting power-law query distribution
Re-indexing unchanged pages wastes workContent-hash comparison skips re-parsing pages that haven't changed since last crawl
Posting lists too large to fit in memoryCompression (delta encoding of sorted doc IDs + variable-byte/Golomb coding)

Key Takeaways

  1. Separate crawling from query serving as two independent subsystems connected only by the index - they have completely different latency and consistency requirements
  2. Politeness is a hard constraint on crawling, not an optimization - per-domain rate limiting and robots.txt compliance are non-negotiable at web scale
  3. 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
  4. 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
  5. 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

  1. How would you handle personalized or localized search results without breaking the shared index/cache model?
  2. How would you detect and handle spam pages or link farms trying to manipulate authority scores?
  3. How would you support autocomplete/query suggestions with sub-50ms latency?
  4. How would you re-balance index shards as the corpus grows from 10B to 100B documents without downtime?
  5. 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.