Web search: crawl, index, rank
Crawling a web nobody controls, an index too large for one machine, and a query that is as slow as its slowest shard.
Crawling a web nobody controls
intermediateA crawler is a breadth-first traversal of a graph with no map, no schema and no cooperation. The frontier, the set of URLs known and not yet fetched, is the central data structure, and it is not a simple queue: it has to be prioritised by importance and freshness, deduplicated, and partitioned so that the same host is not being fetched by twenty machines at once.
Politeness is the constraint that shapes everything. A crawler must respect robots.txt, must rate limit per host rather than globally, and must identify itself so an operator can block it. This is not only courtesy: a crawler that hammers a small site is a denial of service, and the practical consequence of getting it wrong is being blocked by exactly the sites you wanted.
So the frontier is partitioned by host, with one queue per host drained at a polite rate and many hosts in flight simultaneously. That single design decision explains why a crawl is wide rather than deep, and why crawl capacity is measured in hosts and politeness delays rather than in raw bandwidth.
The web then attacks the crawler in ways nobody designs for. Infinite calendars generate a new URL for every future date. Session identifiers make every visit a new address. Redirect chains loop. Pages are generated by JavaScript and are empty without a renderer, which turns a cheap fetch into an expensive browser. Each of these needs a specific defence: URL normalisation, depth and pattern limits, budget per host, and a decision about which pages are worth rendering.
The last problem is knowing when to come back. Content changes at wildly different rates, so recrawl is scheduled per URL from observed change frequency: a news front page in minutes, an archived page in months. Getting that wrong wastes the crawl budget on pages that never change while missing the ones that do, which is the trade the freshness topic later returns to.
The frontier is the system. Everything a crawler does well or badly, politeness, coverage, freshness and resistance to traps, is a property of how that queue is prioritised and partitioned rather than of how fast it can fetch.
The robots exclusion protocol dates from 1994 and was only standardised as RFC 9309 in 2022, having been honoured by convention for nearly thirty years. That is a useful reminder that a great deal of the web works on agreement rather than enforcement.
What is a crawler trap?
Duplicates, canonicals and near-identical pages
advancedA large fraction of the web is duplicated. The same article is syndicated across a dozen sites, the same product page exists under printable, mobile and tracking-parameter variants, and entire sites are mirrored. An index that stores every copy wastes space and, far worse, returns ten results that are the same page, which is a bad result set no ranking improvement can rescue.
Exact duplicates are cheap to detect: hash the normalised content and compare. That catches mirrors and identical variants and misses everything interesting, because most duplication is near-duplication with a different header, an inserted advertisement or a changed date.
Near-duplicate detection is where the technique gets specific. Shingling breaks the document into overlapping word sequences and compares the sets, and comparing every pair is quadratic, so a fingerprinting scheme is used instead: MinHash estimates set similarity from a small signature, and SimHash produces a hash where similar documents differ in few bits, making near-duplicates findable by looking for hashes within a small Hamming distance. Both turn an impossible comparison into an index lookup.
Choosing which copy to keep is a ranking problem in itself. The canonical should be the version most likely to be wanted: the original publisher rather than the syndicator, the version without tracking parameters, the one with the most inbound links. Publishers can state a preference with a canonical link element, which is a hint from an interested party and therefore treated as evidence rather than as instruction.
The consequence for the results page is diversity. Even after canonicalisation, ten results from one domain about the same topic is a worse answer than a mix, so the final ranking applies clustering and per-site limits. That is a deliberate reduction in per-result relevance for a large increase in the usefulness of the page as a whole, which is a trade worth recognising because it appears in every ranked list of anything.
Deduplication is not a storage optimisation, it is a results-quality feature: ten copies of one page is a failed search regardless of how relevant each copy is. The techniques matter because near-duplication is the common case and exact matching cannot see it.
SimHash was published by Google researchers and described for exactly this problem, finding near-duplicate web pages at crawl scale, which is why the technique appears in nearly every large crawler built since.
How should a publisher's canonical link be treated?
An index too large for one machine
advancedThe index is inverted: each term points at the list of documents containing it, with positions so that phrases can be matched. At web scale those posting lists are enormous, so they are compressed aggressively, using delta encoding of document identifiers and variable-length integers, because the index is read constantly and every byte saved is bandwidth and cache.
It cannot live on one machine, so it is sharded, and the choice of how is consequential. Sharding by document means each shard holds a slice of the corpus and a query goes to every shard, which is more network traffic and balances naturally. Sharding by term means a query touches only the shards holding its terms, which sounds efficient and produces terrible skew, because a common term's posting list is vast and multi-term queries need intersections across machines. Document sharding wins in practice, and the fan-out that follows is the defining property of search serving.
Documents are assigned identifiers in an order that makes compression effective, typically by clustering similar or related documents so that posting lists contain runs of nearby identifiers. That is an unglamorous decision that changes index size by a large factor, and it illustrates a general point: the encoding and the ordering are as much of the design as the structure.
Tiering is the other lever. Not every document deserves the same treatment, so an index is split into a small tier of high-quality documents held in memory and larger tiers on cheaper storage. Most queries are answered from the top tier, and the lower tiers are consulted only when the top does not produce enough good results, which is what makes the economics work at all.
Updates make this harder than a static structure. Posting lists are expensive to modify in place, so new documents accumulate in a small fresh index and are merged into the main one periodically, with deletions handled by a separate list of identifiers to suppress. That is the same design as a log-structured merge tree, arrived at independently for the same reason, which is that sequential writes and periodic merges beat random updates at scale.
Sharding by document is what forces every query to touch every shard, which sets the shape of serving and makes tail latency the central problem. Everything else here, compression, identifier ordering, tiering and merge-based updates, exists because the index is read far more often than it is written.
Search engines and log stores converge on the same answer for updates, an immutable main index plus a small fresh segment merged periodically, because in-place modification of compressed posting lists is prohibitively expensive.
What does index tiering achieve?
Serving a query in under a second
advancedA query arrives, is parsed and expanded with synonyms and spelling corrections, and is sent to every shard. Each shard finds its candidates, scores them cheaply, and returns its best few. A root node merges those, applies expensive scoring to the small combined set, adds diversity rules and returns the page. The whole thing has a budget measured in a few hundred milliseconds.
The dominant problem is tail latency, and it is arithmetic rather than bad luck. If a query touches a thousand shards and each has a one per cent chance of taking longer than a second, the chance that at least one does is essentially certain, so the median shard's latency is irrelevant and the request is as slow as its slowest participant. Dean and Barroso's tail at scale paper is the canonical treatment, and the numbers are why every large fan-out system takes the mitigations seriously.
The mitigations are specific. Hedged requests: send a duplicate to a second replica after a short delay and take whichever answers first, which costs a few per cent more work and removes most of the tail. Tied requests, where the two replicas coordinate so the loser drops its copy. Micro-partitioning so slow shards can be rebalanced. And returning early with what has arrived, because a slightly incomplete result page delivered on time is better than a complete one that missed the budget.
Caching helps less than intuition suggests and still matters. Query popularity is a long tail, so a cache of full result pages hits on the head of the distribution and misses on most of the traffic. Caching posting list intersections and per-shard partial results is more effective, and the freshness of everything cached has to be bounded because the index behind it keeps changing.
The design principle underneath is that a request touching a thousand machines cannot depend on all of them behaving. It must be able to answer with most of them, cut off the ones that are late, and degrade the result rather than the deadline. That is the same shed-load-deliberately argument as anywhere else, applied at the level of a single query.
Fan-out to a thousand shards makes the tail the median. That single fact explains hedged requests, early return with partial results, and why a search engine is engineered around latency budgets rather than around throughput.
Dean and Barroso reported that in a system where each of 100 servers has a 1% chance of exceeding a second, 63% of requests exceed a second, which is the clearest statement of why tail latency dominates any fan-out design.
What is the right response when the latency budget is nearly spent?
Freshness: from batch to incremental
advancedFor years the index was rebuilt in batches. Google's pre-Caffeine pipeline ran crawled documents through roughly a hundred sequential MapReduce stages, and a document took two to three days to travel from being crawled to being searchable. That was acceptable when the web changed slowly and unacceptable once it did not.
The reason batch was chosen first is worth stating plainly, because it is the same trade everywhere: batch processing is simple, restartable and efficient per document, and it is inherently latent, because a stage cannot start until the previous one has finished with everything. Adding one new document meant waiting for the next full pass.
Caffeine, the indexing system Google moved to in 2010, replaced that with incremental processing built on Percolator, which added transactions and change notifications on top of the storage layer so that a single document could be updated in place and trigger the work that depends on it. The published result is precise: the same number of documents processed per day, the median document moving through more than a hundred times faster, and the average age of a document in results cut by half.
The cost of that is real and worth naming: incremental processing runs many small distributed transactions instead of a few enormous batch jobs, so the system uses substantially more resources per document and is far more complex to operate. Google's own paper is explicit that the trade was made for latency rather than efficiency, which is the honest way to describe most moves from batch to streaming.
The general lesson generalises well beyond search. Batch is the correct default because it is simple and cheap; incremental is what you buy when the age of the data is a product problem rather than an engineering preference. And the way to know which you are in is to ask what changes when the data is two days old, which for a search engine in a world with breaking news is quite a lot.
This is the clearest published example of the batch to incremental trade, with numbers on both sides: a hundredfold improvement in latency, bought with more resources per document and considerably more operational complexity. Most streaming decisions are the same trade with less measurement.
Google's Percolator paper reports that the previous system fed crawled documents through about a hundred MapReduces with a two to three day pipeline, and that the incremental replacement cut the average age of a document in search results by 50% at the same daily volume.
Why is a batch pipeline inherently latent?
Ranking, and knowing whether it improved
advancedMatching produces thousands of documents and the page shows ten, so ranking is the product. The signals fall into three groups: how well the document matches the query, how good the document is independent of any query, and what the person is likely to want given their language, location and context.
Query-independent quality is the idea that made web search work. PageRank treated a link as a vote and weighted votes by the authority of the linking page, which gave a way to tell an authoritative page from a keyword-stuffed one at a time when text matching alone could not. It is one signal among hundreds now, and the principle it established, that the structure of the graph carries information the documents do not, long outlived the specific formula.
Modern ranking is a learned function over those signals, trained on labelled judgements and behavioural data, and the interesting engineering is the pipeline shape rather than the model. Cheap scoring runs on every candidate in every shard, and expensive scoring runs only on the small set the root has already merged, because the expensive model cannot be run on a million documents inside the latency budget. That two-stage retrieve-then-rerank pattern is now the standard shape for search and recommendation systems generally.
Adversaries are a permanent part of the problem, which is unusual among ranked systems. Every published signal becomes a target, so link farms, keyword stuffing, cloaking and generated content all exist because ranking exists. That is why signals are numerous, partly undisclosed and constantly adjusted, and it is why a search team spends as much effort on defending the ranking as on improving it.
Knowing whether a change helped is the hardest part and is entirely a measurement problem. Human raters give labelled judgements against published guidelines, offline metrics compare rankings against those labels, and online experiments measure what people actually did. Both are needed because they answer different questions: the offline number says the ranking matches expert judgement, and the experiment says it changed behaviour, and a change can move one without the other.
Ranking is where a search engine is judged, and the two things that make it tractable are the two-stage pipeline, which keeps expensive scoring off the hot path, and a measurement discipline that can tell an improvement from a change. Without the second, ranking work is a matter of opinion.
The retrieve-then-rerank shape, cheap scoring across all candidates followed by an expensive model over a few hundred, is now standard well beyond search, appearing in recommendation systems and in retrieval-augmented generation for the same reason: the good model is too slow to run on everything.
Why is ranking split into cheap scoring and expensive reranking?