{"id":"design-search-engine","title":"Design a web search engine","sections":[{"id":"overview","title":"Overview","html":"<p>A web search engine takes a few words and, in a couple of hundred milliseconds, returns the ten most useful pages out of tens of billions. Behind that are three systems that run at very different speeds: a crawler that fetches the web continuously, an indexing pipeline that turns pages into a searchable structure, and a serving tier that answers a query by asking hundreds of machines at once.</p>\n<p>The primer's <a href=\"/learn/design-web-crawler\">web crawler</a> and <a href=\"/learn/design-query-cache\">query cache</a> exercises cover two pieces. This chapter puts the whole thing together: how pages become an <strong>inverted index</strong>, how a crawler stays fast without hammering any one site, why large engines split the index by document rather than by term, and how BM25, PageRank and learned ranking combine to score results quickly. It builds on <a href=\"/learn/database-101\">database 101</a>, <a href=\"/learn/cache-101\">cache 101</a> and <a href=\"/learn/batch-processing\">batch processing</a>.</p>","headings":[]},{"id":"requirements-and-scale","title":"Requirements and scale","html":"<h3 id=\"requirements-and-scale\">Requirements and scale</h3>\n<p>Agree on scope first. A reasonable set:</p>\n<ul>\n<li><strong>Functional:</strong> given a text query such as <code>distributed consensus raft</code>, return the 10 most relevant pages with a title, URL and a two-line snippet that highlights the query terms. Crawl the public web continuously, finding new pages and changes to known ones. Build and update the index from the crawl.</li>\n<li><strong>Non-functional:</strong> p99 query latency under 200 ms; 99.99% availability on the query path (partial results beat an error); freshness in minutes to hours for news and fast-changing sites, days for the long tail.</li>\n<li><strong>Out of scope for now:</strong> autocomplete (see <a href=\"/learn/design-autocomplete\">design autocomplete</a>), image and video search, ads, personalization.</li>\n</ul>\n<p>Then size it. These numbers are assumptions for an interview, not figures from any real engine:</p>\n<ul>\n<li><strong>Corpus:</strong> 50 billion pages in the index.</li>\n<li><strong>Page size:</strong> about 100 KB of raw HTML, which shrinks to about 20 KB of useful text after removing markup, scripts and boilerplate.</li>\n<li><strong>Text storage:</strong> 50 × 10⁹ × 20 KB = 10¹⁵ bytes, or <strong>1 PB of text</strong>. Keeping the raw HTML too is 5 PB before compression.</li>\n<li><strong>Index size:</strong> 20 KB of text is roughly 3,000 words. Storing every word position is 50 × 10⁹ × 3,000 = 1.5 × 10¹⁴ positions; at 4 bytes each that's 600 TB uncompressed, plus document IDs and counts. Compression (covered below) brings a positional index to something like a quarter to a third of the text size, so assume <strong>about 300 TB of index</strong>.</li>\n<li><strong>Queries:</strong> 50,000 a second on average, with a peak of about 2×: <strong>100,000 queries a second</strong>.</li>\n<li><strong>Shards:</strong> split the index into 500 shards of 100 million pages each. That's 300 TB ÷ 500 = 600 GB of index per shard, which fits on one machine's SSD with the hot parts in RAM.</li>\n<li><strong>Fan-out:</strong> every query goes to every shard (the next sections explain why), so peak load is 100,000 × 500 = <strong>50 million shard queries a second</strong>. If the query cache answers 30% of queries, that drops to 35 million. If one replica of a shard handles about 1,000 queries a second, each shard needs around 70 replicas at peak: roughly <strong>35,000 shard servers</strong>. The query rate, not the data size, decides how big the serving fleet is.</li>\n</ul>\n<p>See <a href=\"/learn/estimation\">estimation</a> for more on this kind of arithmetic.</p>","headings":[{"id":"requirements-and-scale","title":"Requirements and scale","depth":3}]},{"id":"high-level-architecture","title":"High-level architecture","html":"<h3 id=\"high-level-architecture\">High-level architecture</h3>\n<figure class=\"diagram\" data-text=\"User <-> Query frontend (query); Query frontend -> Query understanding (parse); Query frontend -> Query cache (lookup); Query frontend -> Aggregator (merge, re-rank) (on miss); Aggregator (merge, re-rank) <-> Index shards (500 × replicas) (scatter / gather); Aggregator (merge, re-rank) -> Document store (titles, text) (snippets)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 -15 640 220\" width=\"640\" role=\"img\" aria-label=\"Query path: cache, then scatter-gather over index shards\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"153\" y1=\"42\" x2=\"167\" y2=\"42\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"204\" y1=\"69\" x2=\"121.3\" y2=\"131\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"240\" y1=\"69\" x2=\"240\" y2=\"128\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"473\" y1=\"42\" x2=\"487\" y2=\"42\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"400\" y1=\"71\" x2=\"400\" y2=\"126\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-client\" x=\"12\" y=\"18\" width=\"136\" height=\"48\" rx=\"24\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"46.5\">User</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"46.5\">Query frontend</tspan></text><rect class=\"dg-node dg-box\" x=\"12\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"158.5\">Query</tspan><tspan x=\"80\" y=\"174.5\">understanding</tspan></text><path class=\"dg-node dg-store\" d=\"M172,140 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M172,140 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"169.5\">Query cache</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"38.5\">Aggregator</tspan><tspan x=\"400\" y=\"54.5\">(merge, re-rank)</tspan></text><path class=\"dg-node dg-store\" d=\"M492,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M492,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"41.5\">Index shards</tspan><tspan x=\"560\" y=\"57.5\">(500 × replicas)</tspan></text><path class=\"dg-node dg-store\" d=\"M332,138 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,138 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"161.5\">Document store</tspan><tspan x=\"400\" y=\"177.5\">(titles, text)</tspan></text><rect class=\"dg-edge-bg\" x=\"139.5\" y=\"-4\" width=\"41\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"160\" y=\"9\" text-anchor=\"middle\">query</text><rect class=\"dg-edge-bg\" x=\"142.2\" y=\"91\" width=\"41\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"162.7\" y=\"104\" text-anchor=\"middle\">parse</text><rect class=\"dg-edge-bg\" x=\"216.4\" y=\"89.5\" width=\"47.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"240\" y=\"102.5\" text-anchor=\"middle\">lookup</text><rect class=\"dg-edge-bg\" x=\"292.3\" y=\"-6\" width=\"53.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"319\" y=\"7\" text-anchor=\"middle\">on miss</text><rect class=\"dg-edge-bg\" x=\"425.4\" y=\"-11\" width=\"109.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"480\" y=\"2\" text-anchor=\"middle\">scatter / gather</text><rect class=\"dg-edge-bg\" x=\"370.2\" y=\"89.5\" width=\"59.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"400\" y=\"102.5\" text-anchor=\"middle\">snippets</text></svg></div><figcaption>Query path: cache, then scatter-gather over index shards</figcaption></figure>\n<p>An <a href=\"/diagrams/search-engine.html\" target=\"_blank\" rel=\"noopener noreferrer\">interactive version of this diagram</a> lets you pan, zoom and trace the request paths.</p>\n<p>The path of one query:</p>\n<ol>\n<li>The frontend normalizes the query (lowercase, Unicode normalization) and checks the <a href=\"/learn/design-query-cache\">query cache</a>. Query popularity is very skewed, so a cache keyed by normalized query answers a large share of traffic for a few minutes at a time.</li>\n<li>On a miss, query understanding runs: tokenizing the same way the indexer did, spelling correction, synonym expansion, detecting phrases and language.</li>\n<li>The aggregator sends the query to one replica of every shard in parallel.</li>\n<li>Each shard finds and scores matching documents in its own index and returns its best ~100 with their scores and ranking features.</li>\n<li>The aggregator merges the 500 × 100 = 50,000 results, re-ranks the top few hundred with a heavier model, and keeps the best 10.</li>\n<li>It fetches titles and stored text for those 10 from the document store, builds query-specific snippets, and returns the page.</li>\n</ol>\n<p>If a shard replica is slow, the aggregator retries on another replica, or after a deadline returns results from the shards that answered. Missing 1 shard out of 500 drops 0.2% of the index for one query, which users rarely notice.</p>","headings":[{"id":"high-level-architecture","title":"High-level architecture","depth":3}]},{"id":"the-crawling-subsystem","title":"The crawling subsystem","html":"<h3 id=\"the-crawling-subsystem\">The crawling subsystem</h3>\n<p>The crawler has two goals that pull against each other: <strong>freshness</strong> (fetch as much as possible, as often as useful) and <strong>politeness</strong> (never send one site more traffic than it can comfortably handle).</p>\n<figure class=\"diagram\" data-text=\"Discovered URLs -> URL seen? (dedup); URL seen? (dedup) -> Front queues (by priority) (new URLs); Front queues (by priority) -> Back queues (one host each) (refill); Host ready-time heap -> Fetchers (DNS cache) (next host); Back queues (one host each) -> Fetchers (DNS cache) (URL); Fetchers (DNS cache) -> Parse and SimHash dedup; Parse and SimHash dedup -> Page store\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 -15 640 218\" width=\"640\" role=\"img\" aria-label=\"Crawl loop with a Mercator-style frontier\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"42\" x2=\"167\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"471\" y1=\"42\" x2=\"487\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"489\" y1=\"162\" x2=\"473\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"521.3\" y1=\"71\" x2=\"441.3\" y2=\"131\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"329\" y1=\"162\" x2=\"313\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"169\" y1=\"162\" x2=\"153\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"38.5\">Discovered</tspan><tspan x=\"80\" y=\"54.5\">URLs</tspan></text><path class=\"dg-node dg-store\" d=\"M172,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M172,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"41.5\">URL seen?</tspan><tspan x=\"240\" y=\"57.5\">(dedup)</tspan></text><rect class=\"dg-node dg-queue\" x=\"332\" y=\"16\" width=\"136\" height=\"52\" rx=\"6\"></rect><line class=\"dg-line\" x1=\"452\" y1=\"24\" x2=\"452\" y2=\"60\"></line><line class=\"dg-line\" x1=\"442\" y1=\"24\" x2=\"442\" y2=\"60\"></line><line class=\"dg-line\" x1=\"432\" y1=\"24\" x2=\"432\" y2=\"60\"></line><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"386\" y=\"38.5\">Front queues</tspan><tspan x=\"386\" y=\"54.5\">(by priority)</tspan></text><rect class=\"dg-node dg-queue\" x=\"492\" y=\"16\" width=\"136\" height=\"52\" rx=\"6\"></rect><line class=\"dg-line\" x1=\"612\" y1=\"24\" x2=\"612\" y2=\"60\"></line><line class=\"dg-line\" x1=\"602\" y1=\"24\" x2=\"602\" y2=\"60\"></line><line class=\"dg-line\" x1=\"592\" y1=\"24\" x2=\"592\" y2=\"60\"></line><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"546\" y=\"38.5\">Back queues</tspan><tspan x=\"546\" y=\"54.5\">(one host each)</tspan></text><rect class=\"dg-node dg-box\" x=\"492\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"158.5\">Host ready-time</tspan><tspan x=\"560\" y=\"174.5\">heap</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"158.5\">Fetchers</tspan><tspan x=\"400\" y=\"174.5\">(DNS cache)</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"158.5\">Parse and</tspan><tspan x=\"240\" y=\"174.5\">SimHash dedup</tspan></text><path class=\"dg-node dg-store\" d=\"M12,140 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M12,140 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"169.5\">Page store</tspan></text><rect class=\"dg-edge-bg\" x=\"289.2\" y=\"-11\" width=\"59.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"319\" y=\"2\" text-anchor=\"middle\">new URLs</text><rect class=\"dg-edge-bg\" x=\"455.4\" y=\"-6\" width=\"47.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"479\" y=\"7\" text-anchor=\"middle\">refill</text><rect class=\"dg-edge-bg\" x=\"448.1\" y=\"114\" width=\"65.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"481\" y=\"127\" text-anchor=\"middle\">next host</text><rect class=\"dg-edge-bg\" x=\"467\" y=\"92\" width=\"28.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"481.3\" y=\"105\" text-anchor=\"middle\">URL</text></svg></div><figcaption>Crawl loop with a Mercator-style frontier</figcaption></figure>\n<h4 id=\"the-crawl-frontier-priority-and-politeness\">The crawl frontier: priority and politeness</h4>\n<p>The frontier is the set of URLs waiting to be fetched. The design popularized by the Mercator crawler, and described in detail in <em>Introduction to Information Retrieval</em>, has two layers of FIFO queues:</p>\n<ul>\n<li><strong>Front queues handle priority.</strong> A prioritizer gives each URL a priority from signals such as the page's estimated importance (for example its PageRank), how often it has changed in the past, and whether it's a known news source. There's one front queue per priority level, and higher-priority queues are drained more often.</li>\n<li><strong>Back queues handle politeness.</strong> Each back queue holds URLs from exactly one host, and a small table maps each host to its back queue. When a back queue empties, it's refilled from the front queues, so priority still shapes what each host's queue contains.</li>\n<li><strong>A heap orders hosts by the earliest time each may be contacted again.</strong> A fetcher thread takes the host at the top of the heap, waits if that time hasn't arrived, fetches one URL from its back queue, and then pushes the host back with a new ready time. A common rule is to wait a multiple of how long the last fetch took (the textbook suggests about 10×), and never less than the site's <code>Crawl-delay</code> in <code>robots.txt</code> if it sets one.</li>\n</ul>\n<p>Because each host is only ever in one back queue and one heap slot, at most one fetch per host is in flight. A slow site only slows itself; the fetchers meanwhile work through thousands of other hosts. That's how a crawler can make tens of thousands of requests a second overall while each site sees a trickle.</p>\n<p>A few other pieces matter at scale. Fetchers keep their own DNS cache, because DNS lookups are slow and the crawler would otherwise flood resolvers. Each host's <code>robots.txt</code> is fetched, cached and checked before any URL from that host. The \"URL seen?\" check uses a large hash set or Bloom filter over normalized URLs, so the same page isn't queued twice from different links.</p>\n<h4 id=\"near-duplicate-detection-with-simhash\">Near-duplicate detection with SimHash</h4>\n<p>A large share of the web is near-duplicate: mirrors, syndicated articles, printer-friendly copies, pages that differ only in a date or a tracking parameter. Early studies of large crawls put it at around 30% of pages. Indexing them wastes space and fills result pages with copies.</p>\n<p>An exact hash like SHA-256 can't detect near-duplicates. Change one character and about half of the output bits flip, so the hash says nothing about how similar two pages are. <strong>SimHash</strong> is built so that similar documents get fingerprints that differ in only a few bits (a small <strong>Hamming distance</strong>):</p>\n<ol>\n<li>Extract features from the text, usually words or short word sequences, each with a weight such as its frequency.</li>\n<li>Hash each feature to 64 bits.</li>\n<li>Keep 64 running totals, one per bit position. For each feature, add its weight to position i if bit i of its hash is 1, and subtract its weight if bit i is 0.</li>\n<li>The fingerprint has bit i set to 1 if total i is positive, and 0 otherwise.</li>\n</ol>\n<p>Two pages that share most of their features end up with the same sign in almost every position. Manku, Jain and Das Sarma at Google reported that for 8 billion pages, 64-bit fingerprints treated as near-duplicates at a Hamming distance of 3 or less worked well, and showed how to find all fingerprints within 3 bits of a new one quickly using several permuted, sorted tables.</p>","headings":[{"id":"the-crawling-subsystem","title":"The crawling subsystem","depth":3},{"id":"the-crawl-frontier-priority-and-politeness","title":"The crawl frontier: priority and politeness","depth":4},{"id":"near-duplicate-detection-with-simhash","title":"Near-duplicate detection with SimHash","depth":4}]},{"id":"the-inverted-index-pipeline","title":"The inverted index pipeline","html":"<h3 id=\"the-inverted-index-pipeline\">The inverted index pipeline</h3>\n<p>An <strong>inverted index</strong> maps each term to the list of documents that contain it, the opposite direction from a document that lists its words. It's what lets a shard find every page containing <code>consensus</code> without reading every page.</p>\n<figure class=\"diagram\" data-text=\"Term &#x22;consensus&#x22; -> Dictionary (df, offset) (lookup); Dictionary (df, offset) -> Posting list (on disk or RAM) (offset)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 640 85\" width=\"640\" role=\"img\" aria-label=\"Looking up one term in the index\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"42\" x2=\"247\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"391\" y1=\"42\" x2=\"487\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"38.5\">Term</tspan><tspan x=\"80\" y=\"54.5\">\"consensus\"</tspan></text><path class=\"dg-node dg-store\" d=\"M252,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M252,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"41.5\">Dictionary</tspan><tspan x=\"320\" y=\"57.5\">(df, offset)</tspan></text><path class=\"dg-node dg-store\" d=\"M492,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M492,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"41.5\">Posting list</tspan><tspan x=\"560\" y=\"57.5\">(on disk or RAM)</tspan></text><rect class=\"dg-edge-bg\" x=\"175.4\" y=\"33\" width=\"47.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"199\" y=\"46\" text-anchor=\"middle\">lookup</text><rect class=\"dg-edge-bg\" x=\"415.4\" y=\"33\" width=\"47.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"439\" y=\"46\" text-anchor=\"middle\">offset</text></svg></div><figcaption>Looking up one term in the index</figcaption></figure>\n<p>The dictionary holds every term with its document frequency (how many documents contain it) and where its posting list starts. The posting list holds one entry per document:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>\"consensus\"  df = 3</span></span>\n<span class=\"line\"><span>  doc 12   tf = 4   positions 4, 18, 92, 140</span></span>\n<span class=\"line\"><span>  doc 45   tf = 1   positions 12</span></span>\n<span class=\"line\"><span>  doc 89   tf = 8   positions 1, 9, 34, …</span></span></code></pre>\n<p>Here tf is the term frequency: how many times the term occurs in that document.</p>\n<h4 id=\"document-processing\">Document processing</h4>\n<p>When a fetched page reaches the indexer:</p>\n<ol>\n<li><strong>Parse.</strong> Strip tags, scripts and styles. Keep the title, headings and other fields separately, because a match in the title counts for more than a match in a footer.</li>\n<li><strong>Tokenize.</strong> Split text into terms and normalize them: lowercase, Unicode normalization, and language-specific rules (some languages don't put spaces between words).</li>\n<li><strong>Handle stop words carefully.</strong> Classic systems dropped very common words like <code>the</code> and <code>of</code>. Web engines mostly keep them, because queries such as <code>to be or not to be</code> or <code>The Who</code> need them, and compression makes their long posting lists affordable.</li>\n<li><strong>Stem or lemmatize.</strong> A stemmer such as Porter's strips suffixes by rule, so <code>running</code> and <code>runs</code> both become <code>run</code>. Rules can't handle irregular forms, so <code>ran</code> stays <code>ran</code>; mapping it to <code>run</code> needs a lemmatizer with a dictionary. Engines often index both the original and the stemmed form so exact matches can still score higher.</li>\n<li><strong>Add anchor text.</strong> The text of links pointing to a page is indexed as part of that page. Other people's descriptions are often better than the page's own words. The well-known example: Adobe's Acrobat Reader download page ranked for \"click here\" because so many sites linked to it with \"click here to download Acrobat\".</li>\n</ol>\n<p>The index itself is built in batch: mappers emit (term, doc, positions) records, the shuffle groups them by term, and reducers write sorted posting lists. That's the original use case for MapReduce (see <a href=\"/learn/batch-processing\">batch processing</a>). To keep news fresh without rebuilding everything, engines also run a small, frequently updated index for recent pages, query it alongside the main one, and merge the results.</p>\n<h4 id=\"why-positions-are-stored\">Why positions are stored</h4>\n<p>Positions make two things possible:</p>\n<ul>\n<li><strong>Phrase queries.</strong> For <code>\"system design\"</code>, a document matches only if some position of <code>design</code> is exactly one more than a position of <code>system</code>.</li>\n<li><strong>Proximity scoring.</strong> Pages where the query terms appear close together usually deserve a higher score than pages where they're 500 words apart.</li>\n</ul>\n<h4 id=\"compressing-posting-lists\">Compressing posting lists</h4>\n<p>Uncompressed, the positions alone would be about 600 TB, as estimated above. Posting lists compress very well because they're sorted:</p>\n<ul>\n<li><strong>Gaps instead of IDs.</strong> Document IDs <code>104, 108, 115, 120</code> are stored as <code>104, 4, 7, 5</code>, the difference from the previous ID. Positions within a document are gap-encoded the same way. Gaps are small numbers, especially for common terms.</li>\n<li><strong>Variable-byte encoding.</strong> Each byte carries 7 bits of the number and 1 bit saying whether more bytes follow. Any gap below 128 takes 1 byte instead of 4.</li>\n<li><strong>Block bit-packing.</strong> Schemes such as frame of reference and PForDelta take a block of, say, 128 gaps and store each in just enough bits for the largest one (with the rare large values handled separately). Decoding a block uses fast SIMD instructions, and blocks can be skipped without decoding.</li>\n</ul>\n<p>Together these typically cut posting lists to a quarter or less of their uncompressed size, which is where the ~300 TB estimate comes from. For sets of document IDs used as filters (pages in a language, pages from a site), many engines use compressed bitmaps such as Roaring bitmaps instead.</p>","headings":[{"id":"the-inverted-index-pipeline","title":"The inverted index pipeline","depth":3},{"id":"document-processing","title":"Document processing","depth":4},{"id":"why-positions-are-stored","title":"Why positions are stored","depth":4},{"id":"compressing-posting-lists","title":"Compressing posting lists","depth":4}]},{"id":"partitioning-the-index-by-document-or-by-term","title":"Partitioning the index: by document or by term","html":"<h3 id=\"partitioning-the-index-by-document-or-by-term\">Partitioning the index: by document or by term</h3>\n<p>No single machine holds 300 TB of index and answers 100,000 queries a second, so the index is split. There are two ways to do it.</p>\n<p><strong>Term partitioning (a global index).</strong> Each machine holds complete posting lists for a subset of terms, say every term from <code>a</code> to <code>c</code> on one node.</p>\n<ul>\n<li>A one-word query touches one machine.</li>\n<li>A query like <code>system design interview</code> needs posting lists from three machines. Intersecting them means shipping long lists of document IDs across the network, and posting lists for common terms are huge.</li>\n<li>Popular terms make their machines hot spots, and load follows whatever people happen to search for today.</li>\n</ul>\n<p><strong>Document partitioning (a local index).</strong> Each machine holds a complete inverted index for a subset of documents, here 100 million pages per shard, assigned by a hash of the document ID or URL.</p>\n<figure class=\"diagram\" data-text=\"Aggregator <-> Shard 1 100M docs; Aggregator <-> Shard 2 100M docs; Aggregator <-> Shard 3 100M docs; Aggregator <-> Shard 500 100M docs\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 640 205\" width=\"640\" role=\"img\" aria-label=\"Document partitioning: every query goes to every shard\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"262\" y1=\"71\" x2=\"152\" y2=\"126\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"300.7\" y1=\"71\" x2=\"264\" y2=\"126\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"339.3\" y1=\"71\" x2=\"376\" y2=\"126\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"378\" y1=\"71\" x2=\"488\" y2=\"126\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"252\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"46.5\">Aggregator</tspan></text><path class=\"dg-node dg-store\" d=\"M12,138 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M12,138 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"161.5\">Shard 1</tspan><tspan x=\"80\" y=\"177.5\">100M docs</tspan></text><path class=\"dg-node dg-store\" d=\"M172,138 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M172,138 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"161.5\">Shard 2</tspan><tspan x=\"240\" y=\"177.5\">100M docs</tspan></text><path class=\"dg-node dg-store\" d=\"M332,138 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,138 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"161.5\">Shard 3</tspan><tspan x=\"400\" y=\"177.5\">100M docs</tspan></text><path class=\"dg-node dg-store\" d=\"M492,138 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M492,138 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"161.5\">Shard 500</tspan><tspan x=\"560\" y=\"177.5\">100M docs</tspan></text></svg></div><figcaption>Document partitioning: every query goes to every shard</figcaption></figure>\n<ul>\n<li>Every query goes to all 500 shards in parallel. Each intersects and scores its own posting lists locally, and returns only its top ~100 results: a few kilobytes, not posting lists.</li>\n<li>Load is even, because every shard does a similar share of every query.</li>\n<li>A failed or slow shard costs a small fraction of the results instead of whole terms, and replicas cover it.</li>\n<li>The cost is fan-out. Every query does work on every shard, and the query is as slow as the slowest shard it waits for, so tail latency matters a lot. Replicas, hedged requests (send to a second replica if the first is slow) and deadlines keep it in check; see <a href=\"/learn/reliability-patterns\">reliability patterns</a>.</li>\n</ul>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th></th><th>Term partitioning</th><th>Document partitioning</th></tr></thead><tbody><tr><td>Machines per query</td><td>one per query term</td><td>all shards</td></tr><tr><td>Network traffic</td><td>posting lists</td><td>top-k results only</td></tr><tr><td>Load balance</td><td>skewed by popular terms</td><td>even</td></tr><tr><td>Effect of a lost node</td><td>some terms missing entirely</td><td>a fraction of documents missing</td></tr><tr><td>Adding documents</td><td>touches many nodes</td><td>touches one shard</td></tr></tbody></table></div>\n<p>Large web engines use document partitioning in practice, adding replicas of each shard to handle query volume. Term partitioning shows up in research and in some specialized systems, but the network cost of multi-term queries makes it a poor fit here.</p>","headings":[{"id":"partitioning-the-index-by-document-or-by-term","title":"Partitioning the index: by document or by term","depth":3}]},{"id":"relevance-and-ranking","title":"Relevance and ranking","html":"<h3 id=\"relevance-and-ranking\">Relevance and ranking</h3>\n<p>Scoring is another funnel. Cheap scores run over many documents inside each shard, and expensive ones run over a few hundred at the aggregator.</p>\n<figure class=\"diagram\" data-text=\"Shard index 100M docs -> WAND top-k ~1,000 per shard (BM25); WAND top-k ~1,000 per shard -> Light ranker ~100 per shard (features); Light ranker ~100 per shard -> Heavy ranker top few hundred (merge)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 -15 640 100\" width=\"640\" role=\"img\" aria-label=\"Ranking funnel for one query\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"42\" x2=\"167\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"471\" y1=\"42\" x2=\"487\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><path class=\"dg-node dg-store\" d=\"M12,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M12,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"41.5\">Shard index</tspan><tspan x=\"80\" y=\"57.5\">100M docs</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"38.5\">WAND top-k</tspan><tspan x=\"240\" y=\"54.5\">~1,000 per shard</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"38.5\">Light ranker</tspan><tspan x=\"400\" y=\"54.5\">~100 per shard</tspan></text><rect class=\"dg-node dg-box\" x=\"492\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"38.5\">Heavy ranker</tspan><tspan x=\"560\" y=\"54.5\">top few hundred</tspan></text><rect class=\"dg-edge-bg\" x=\"141.6\" y=\"-11\" width=\"34.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"159\" y=\"2\" text-anchor=\"middle\">BM25</text><rect class=\"dg-edge-bg\" x=\"289.2\" y=\"-6\" width=\"59.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"319\" y=\"7\" text-anchor=\"middle\">features</text><rect class=\"dg-edge-bg\" x=\"458.5\" y=\"-6\" width=\"41\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"479\" y=\"7\" text-anchor=\"middle\">merge</text></svg></div><figcaption>Ranking funnel for one query</figcaption></figure>\n<ol>\n<li><strong>Retrieval</strong> inside each shard finds the ~1,000 best documents by a cheap score (BM25 plus static signals like PageRank), using WAND to avoid scoring most matches.</li>\n<li><strong>Light ranking</strong> inside each shard re-scores those with more features (proximity, title matches, freshness, link signals) using a fast model such as gradient-boosted trees, and returns the top ~100.</li>\n<li><strong>Heavy ranking</strong> at the aggregator merges the 50,000 results and re-scores the top few hundred with an expensive model, for example a transformer that reads the query and the page text together.</li>\n</ol>\n<h4 id=\"text-relevance-from-tf-idf-to-bm25\">Text relevance: from TF-IDF to BM25</h4>\n<p>The starting idea is <strong>TF-IDF</strong>: a term matters more in a document the more often it appears there (term frequency) and the rarer it is across the collection (inverse document frequency):</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>tf-idf(t, D) = tf(t, D) × log(N ÷ df(t))</span></span></code></pre>\n<p>N is the number of documents and df(t) is how many contain t. In a shard of 100 million pages, a term found in 100,000 of them gets log₁₀(10⁸ ÷ 10⁵) = 3, and one found in 10 million gets log₁₀(10) = 1. IDF grows with the logarithm of rarity, so a term 100 times rarer gets 3 times the weight, not 100 times.</p>\n<p>TF-IDF has two flaws: a page that repeats a word 100 times scores 100 times higher, and long pages win simply by containing more words. <strong>BM25</strong> (Okapi BM25, from Robertson and colleagues' probabilistic retrieval work) fixes both:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>score(D, Q) = sum over each query term q of</span></span>\n<span class=\"line\"><span>              IDF(q) × tf(q, D) × (k₁ + 1) ÷ ( tf(q, D) + k₁ × (1 − b + b × |D| ÷ avgdl) )</span></span>\n<span class=\"line\"><span></span></span>\n<span class=\"line\"><span>IDF(q) = ln( 1 + (N − n(q) + 0.5) ÷ (n(q) + 0.5) )</span></span></code></pre>\n<ul>\n<li>tf(q, D) is how often q appears in document D, and n(q) is how many documents contain q.</li>\n<li>|D| is D's length in words and avgdl is the average length across the collection.</li>\n<li><strong>k₁</strong> (usually 1.2 to 2.0) controls <strong>term frequency saturation</strong>. Each extra occurrence helps less. With k₁ = 1.2 and ignoring length, the term's factor is 1.0 at tf = 1, about 1.96 at tf = 10 and about 2.17 at tf = 100, and it never exceeds k₁ + 1 = 2.2. Repeating a keyword a hundred times barely helps.</li>\n<li><strong>b</strong> (usually 0.75) controls <strong>length normalization</strong>: at b = 1 a term's weight is fully scaled by the page's length relative to average, at b = 0 length is ignored.</li>\n</ul>\n<p>The IDF shown is the variant Lucene uses; the <code>1 +</code> inside the log keeps IDF positive for terms that appear in more than half the documents.</p>\n<h4 id=\"link-authority-pagerank\">Link authority: PageRank</h4>\n<p>PageRank, introduced by Brin and Page in 1998, treats a link as a vote, weighted by the importance of the page casting it. It models a \"random surfer\" who follows a random link on the current page with probability d, and otherwise jumps to a random page anywhere:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>PR(u) = (1 − d) ÷ N  +  d × sum over pages v that link to u of  PR(v) ÷ L(v)</span></span></code></pre>\n<ul>\n<li>N is the total number of pages, and L(v) is the number of outgoing links on page v, so v splits its rank evenly among the pages it links to.</li>\n<li>d is the <strong>damping factor</strong>, usually 0.85.</li>\n<li>With the ÷ N term, the ranks across all pages sum to 1. The 1998 paper wrote the first term as just (1 − d), which scales every rank by N without changing the order.</li>\n<li>Pages with no outgoing links would leak rank out of the system, so implementations treat them as linking to every page.</li>\n</ul>\n<p>A page ranks high if many pages link to it, or if a few high-ranked pages do. PageRank is computed offline by power iteration: start with every page at 1 ÷ N, apply the formula to every page, and repeat until the values stop changing, typically after a few dozen rounds. Each round is a pass over the link graph, a natural fit for MapReduce or Spark. The result is stored as a static, per-document score that shards read at query time. Today it's one signal among hundreds, not the main ranking factor it was in 1998.</p>\n<h4 id=\"early-termination-with-wand\">Early termination with WAND</h4>\n<p>A shard with 100 million pages may have millions containing <code>computer</code>. Fully scoring every match for every query would waste most of the CPU, since only the top 1,000 matter.</p>\n<p><strong>WAND</strong> (\"weak AND\", Broder et al., 2003) skips documents that can't make the top k:</p>\n<ul>\n<li>For each query term, the index stores an upper bound: the highest BM25 contribution that term gives any document.</li>\n<li>The shard keeps a threshold θ, the score of the current k-th best document.</li>\n<li>It keeps one cursor per query term, each at some document ID, and sorts the terms by those IDs. Walking through them in order, it adds up their upper bounds until the sum exceeds θ. The document ID where that happens is the <strong>pivot</strong>. Any document before the pivot appears only in terms whose bounds add up to θ or less, so it can't beat θ. Every cursor jumps straight to the pivot.</li>\n<li>Only then is the pivot scored fully, and θ rises as better documents are found, so skipping gets more aggressive as the query runs.</li>\n</ul>\n<p><strong>Block-Max WAND</strong> (Ding and Suel, 2011) stores an upper bound for each block of about 128 postings as well as for the whole list. The block bounds are much tighter, so whole blocks can be skipped without being decompressed. Published experiments show several-fold speedups over plain WAND, and Lucene, which Elasticsearch and OpenSearch are built on, uses a block-max approach for top-k queries.</p>","headings":[{"id":"relevance-and-ranking","title":"Relevance and ranking","depth":3},{"id":"text-relevance-from-tf-idf-to-bm25","title":"Text relevance: from TF-IDF to BM25","depth":4},{"id":"link-authority-pagerank","title":"Link authority: PageRank","depth":4},{"id":"early-termination-with-wand","title":"Early termination with WAND","depth":4}]},{"id":"check-yourself","title":"Check yourself","html":"<h3 id=\"check-yourself\">Check yourself</h3>\n<details>\n<summary>Why do large web search engines partition their index by document instead of by term?</summary>\n<p>With term partitioning, a multi-word query like <code>distributed system design</code> needs posting lists from three machines, and intersecting them means sending long lists of document IDs over the network. Popular terms also turn their machines into hot spots.</p>\n<p>With document partitioning, each shard has a complete index for its 100 million pages. The query goes to all shards at once, each intersects and scores locally, and each sends back only its top ~100 results, a few kilobytes. Load is spread evenly, and losing a shard costs a small slice of documents rather than whole terms. The price is that every query touches every shard and waits for the slowest one, which is why shards are replicated and the aggregator uses deadlines and hedged requests.</p>\n</details>\n<details>\n<summary>How does a Mercator-style frontier keep the crawler fast while sending each site at most one request at a time?</summary>\n<p>It separates priority from politeness. Front queues order URLs by importance. Back queues each hold URLs from exactly one host, and a heap records the earliest time each host may be contacted again. A fetcher takes the host at the top of the heap, fetches one URL from that host's queue, then puts the host back with a new ready time, a multiple of how long the last fetch took and never less than the site's <code>Crawl-delay</code>.</p>\n<p>Since each host appears once in the heap, only one fetch per host is in flight. While one site is in its cool-down, fetchers are busy with thousands of others, so total throughput stays high and no single site sees more than a trickle.</p>\n</details>\n<details>\n<summary>How does SimHash find near-duplicate pages, and why doesn't SHA-256 work for this?</summary>\n<p>SHA-256 is designed so that changing one character flips about half the output bits. Two pages that differ only in a timestamp get unrelated hashes, so the hash tells you nothing about similarity.</p>\n<p>SimHash hashes each feature (word or word sequence) to 64 bits and keeps 64 running totals: for each feature, add its weight where its hash has a 1 bit and subtract where it has a 0. The fingerprint's bit i is 1 if total i is positive. Pages that share most features end up with the same signs in almost every position, so their fingerprints differ in only a few bits. Google's crawler work treated 64-bit fingerprints within a Hamming distance of 3 as near-duplicates.</p>\n</details>\n<details>\n<summary>A page repeats \"cheap flights\" 200 times in hidden text. Why doesn't BM25 rank it first?</summary>\n<p>Term frequency saturates. With k₁ = 1.2 a term's factor approaches k₁ + 1 = 2.2 no matter how many times it appears; at tf = 10 it's already about 1.96, so 200 repetitions add very little over 10. Length normalization (b) also lowers the score of a page bloated with extra words. And BM25 is only the first filter: the light and heavy rankers use link signals, spam classifiers and many other features that keyword stuffing doesn't improve.</p>\n</details>\n<details>\n<summary>Your query cache answers 30% of queries. Roughly how many shard servers does the peak need, and what would change if the hit rate doubled?</summary>\n<p>At 100,000 queries a second, 70,000 miss the cache. Each goes to all 500 shards, so that's 35 million shard queries a second. At an assumed 1,000 queries a second per replica, that's 35,000 shard servers, about 70 replicas per shard.</p>\n<p>With a 60% hit rate, 40,000 queries miss, so 20 million shard queries a second and about 20,000 servers. Because of the fan-out, every point of cache hit rate saves 500 shard queries per avoided query, which is why search engines invest heavily in the query cache.</p>\n</details>\n<h3 id=\"further-reading\">Further reading</h3>\n<ul>\n<li><a href=\"http://infolab.stanford.edu/~backrub/google.html\" target=\"_blank\" rel=\"noopener noreferrer\">The Anatomy of a Large-Scale Hypertextual Web Search Engine</a> (Sergey Brin and Lawrence Page, 1998): the original Google architecture, including PageRank, anchor text and the early index design.</li>\n<li><a href=\"https://nlp.stanford.edu/IR-book/\" target=\"_blank\" rel=\"noopener noreferrer\">Introduction to Information Retrieval</a> (Manning, Raghavan and Schütze): the standard textbook. Chapters 1–5 cover inverted indexes and compression, chapter 11 covers BM25, and chapter 20 covers crawling.</li>\n<li><a href=\"https://nlp.stanford.edu/IR-book/html/htmledition/the-url-frontier-1.html\" target=\"_blank\" rel=\"noopener noreferrer\">The URL frontier</a>, from the same book: the front-queue and back-queue frontier design described in this chapter.</li>\n<li><a href=\"https://dl.acm.org/doi/10.1145/1242572.1242592\" target=\"_blank\" rel=\"noopener noreferrer\">Detecting near-duplicates for web crawling</a> (Manku, Jain and Das Sarma, WWW 2007): SimHash at the scale of billions of pages.</li>\n<li><a href=\"https://dl.acm.org/doi/10.1145/956863.956944\" target=\"_blank\" rel=\"noopener noreferrer\">Efficient query evaluation using a two-level retrieval process</a> (Broder, Carmel, Herscovici, Soffer and Zien, CIKM 2003): the WAND paper.</li>\n<li><a href=\"https://dl.acm.org/doi/10.1145/2009916.2010048\" target=\"_blank\" rel=\"noopener noreferrer\">Faster top-k document retrieval using block-max indexes</a> (Ding and Suel, SIGIR 2011): Block-Max WAND.</li>\n<li>Allan Heydon and Marc Najork, \"Mercator: A scalable, extensible Web crawler\", <em>World Wide Web</em> 2(4), 1999: the crawler the frontier design is named after.</li>\n</ul>","headings":[{"id":"check-yourself","title":"Check yourself","depth":3},{"id":"further-reading","title":"Further reading","depth":3}]}],"sourceFiles":[]}