Skip to lesson
‹

Courses / System Design Interview / Chapter 18

Module 2 · Building Blocks · Chapter 18 of 39

Distributed Search

17 sections · ~31 min✓ Fact-checked

Builds on Databases: Replication, Partitioning and Choosing a Store, Distributed Caches and Message Queues. Next up: Distributed Logging.

Part 1 · The Problem Search Solves

Why a database query is not enough

It is midnight, and a shopping app's Diwali sale has just opened. Four thousand shoppers a second type into the search box: "red running shoes", "phone under 15000". The search box runs WHERE title LIKE '%shoes%' on the product database, and every query reads all 50 million listings. Within two minutes the database's CPUs hit 100%, searches take ten seconds, and checkout, which shares the same database, starts timing out too. The busiest hour of the sale is spent on a spinner. What the team needed was a search index: a separate copy of the data, built only for finding things.

The database failed for a simple reason: a pattern that starts with a wildcard cannot use a normal index, so it scans every row and returns matches in no useful order. A search system solves three problems that a plain query does not. It finds documents containing the words quickly, it matches words written differently (Shoes, shoe, running and run), and it puts the best matches first. This chapter builds one for that shopping app.

Here is what shoppers expect, and what each expectation demands of the system.

What users expectWhat the system must do
Results the moment they stop typingAnswer 99% of queries in under 200 ms
Words, not exact stringsMatch plurals, capital letters, accents and common spellings
The best results firstRank by relevance, popularity and freshness
New items show up quicklyMake a new listing searchable within seconds to a minute
It keeps workingKeep answering when a machine or a whole rack fails

Every row is a number or a property you can test, and together they shape the rest of the chapter.

Search is a derived copy

The database stays the source of truth. The search index is a second copy of the data, shaped for one job: finding things fast. If the index is lost or corrupted, you rebuild it from the database.

The words you will need

Search has its own vocabulary. These terms appear throughout the chapter, so fix them now.

TermMeaningExample
DocumentOne thing a user can findA product listing, a job post, a song
FieldA named part of a documenttitle, description, brand, price
TermA cleaned-up word as stored in the indexrun, shoe, red
CorpusAll documents in the index50 million listings
QueryWhat the user typed, turned into termsred AND run AND shoe
PostingA record saying a term appears in a documentterm shoe in doc 17, twice

Two of these carry the whole design: an index is built from terms, and each term points to its postings.

Users also mix text with exact filters. Someone who types "shoes" and ticks "under ₹2,000" and "size 9" wants word matching plus precise conditions. So the index stores some fields, like price, size and brand, as plain values that can be filtered and sorted, not only as words.

Keep one picture in mind: documents go in at one end, get broken into terms, and are stored so that any term leads straight to the documents that contain it. The next page shows that structure.

Part 2 · How a Search Index Works

The inverted index

The index at the back of a textbook maps a word to the pages it appears on. Search engines do the same. Instead of storing each document's words (document → terms), they store, for each term, the list of documents containing it (term → documents). This is the inverted index, and each list is a postings list.

Documents
DocTitle
1Red running shoes
2Blue running shorts
3Red trail shoes for men
4Leather shoes
Inverted index
TermPostings (doc ids)
red1, 3
run1, 2
shoe1, 3, 4
blue2
short2
trail3
men3
leather4

The word for is left out as a stop word; the next page explains why.

To answer red shoes, the engine fetches two lists, red → 1, 3 and shoe → 1, 3, 4, and intersects them. Both lists are sorted by document id, so one pass with two pointers finds the common ids, 1 and 3, without ever touching document 2 or 4.

Real postings carry more than ids. They record how often the term appears in each document, for ranking, and where it appears, for phrase queries like "running shoes" where the words must sit side by side. Lists are kept sorted and compressed, often as gaps between ids (1, 3, 4 is stored as 1, 2, 1), because small numbers take fewer bytes.

Why AND queries are fast

Start from the shortest list. If trail appears in 2,000 documents and shoe in 3 million, you walk the 2,000 and use skip pointers, shortcuts stored inside the long list, to jump ahead in it. The work depends mostly on the rare word, not the common one.

Tokenising and normalising text

The index on the previous page stored run and shoe, not "Running-Shoes". Before text enters the index it passes through an analyser: a fixed chain of steps that turns raw text into terms. The same chain must run on every query too, or the query's terms will not match the stored ones.

From raw text to index terms
  1. 1Raw textMen's Red Running-Shoes, size 9
  2. 2Tokenisesplit on spaces and most punctuation: Men's | Red | Running | Shoes | size | 9
  3. 3Lowercase and foldmen's | red | running | shoes | size | 9; accents go too, so café matches cafe
  4. 4Drop stop words (optional)very common words like for and the say little about meaning
  5. 5Stemrunning becomes run, shoes becomes shoe, men's becomes men
  6. 6Add synonyms (optional)sneakers is also stored as shoe, tee as t-shirt
  7. 7Termsmen | red | run | shoe | size | 9

Each optional step helps some searches and hurts others.

ChoiceHelpsHurts
Stemming"running" finds "run"Can join unrelated words: "university" and "universe" may share a stem
Stop wordsA smaller indexBreaks searches like "to be or not to be"
Synonyms"mobile" finds "phone"A careless list returns strange results
Word pieces (n-grams)Partial words and some typos match: "sho" finds "shoe"The index grows several times larger

Every choice trades recall, finding more matches, against precision, keeping them relevant. Languages differ too: Hindi, Tamil and other Indian languages need their own tokenisers, and many users type Hindi in Latin letters, such as "joote" for shoes, so an app used across India may index the same text with more than one analyser.

Ranking: which match comes first

Finding matches is half the job. A query for shoes may match 3 million listings, and users look at the first ten. Ranking gives every match a score and returns the highest. The classic text signals rest on two simple ideas.

  • Term frequency (TF): a listing that mentions trail three times is probably more about trail running than one that mentions it once.
  • Inverse document frequency (IDF): rare words matter more, because far fewer documents contain them. In a catalogue of 1 million listings:
TermDocuments containing it (of 1 M)IDF = log10(1 M ÷ count)
the900,0000.05
red100,0001.0
shoes50,0001.3
trail2,0002.7

A match on trail counts about 50 times more than a match on the: rare words carry the meaning. A document's text score adds up TF × IDF over the query's terms. BM25, the default in many engines, refines this in two ways: each extra mention adds less than the one before, and long documents do not win just because they hold more words. Two settings control it: k1, often about 1.2, sets how quickly extra mentions stop counting, and b, often 0.75, sets how strongly long documents are pulled back.

Text match is rarely enough on its own. Good ranking mixes in freshness, popularity (clicks, sales, ratings), quality (complete listings, trusted sellers) and sometimes the user's history. Large systems rank in two stages: a cheap score picks a few hundred to a few thousand candidates, then a slower machine-learned model re-ranks only those, because running it on 3 million matches would blow the latency budget.

How you know ranking got better

Test a change offline first on a few thousand real queries whose results people have graded, scored by a metric such as NDCG, which rewards putting the best results at the top. Then run an A/B test on a small share of users and compare clicks on top results, purchases per search and searches with zero results. Ship only if those improve and p99 latency stays within budget.

Part 3 · Spreading the Index Across Machines

Splitting the index: by document or by term

One index on one machine works until the sale. Fifty million listings and their index take a couple of hundred gigabytes, and the traffic reaches thousands of queries per second, too much for one machine. So the index is split into shards, and there are two ways to cut it.

By document (local index)By term (global index)
Each shard holdsEvery term, for a slice of the documentsFull postings lists, for a slice of the terms
A query goes toEvery shardOnly the shards that own its terms
Adding a documentTouches one shardTouches every shard that owns one of its terms
Multi-word queriesEach shard intersects its own listsPostings lists must travel between shards first
Hot spotsEven load: every shard does a little per queryA popular term makes its shard hot
Seen in practiceMost search systemsRarely, for special cases

Document partitioning is the usual choice. Sending every query to every shard sounds wasteful, but each shard searches only its slice, all of them work in parallel, the load stays even, and a new document touches exactly one shard. Documents are placed by hashing their id, so shards stay close in size and their word statistics, such as IDF, come out nearly the same.

There is one exception. When every query stays inside one tenant, such as a seller searching their own 30,000 orders in a seller dashboard, route documents by tenant id instead, so each query visits one shard rather than all of them. A few huge tenants would break the balance, so give any tenant above a size limit several shards of its own and scatter only across those.

Sizing a search cluster

Splitting decides how the data is cut; sizing decides how many machines hold it. Think of shards as shelves and replicas as shop counters: more stock needs more shelves, more customers need more counters.

A shard should be small enough that its index sits in memory or on fast SSD, often tens of gigabytes, because a query that waits on slow disk misses its budget. Each shard then gets replicas, full copies on other machines, and any copy can answer any query for that shard. For the shopping app, under stated assumptions:

  • Data: 50 M listings × 2 KB of text ≈ 100 GB; assume the index is about the same size.
  • Shards: at about 33 GB each, 3 shards, the three you will see in the next scene.
  • Traffic: a peak of 4,000 queries a second, and every query visits every shard. If one copy handles about 500 a second, each shard needs 8 copies.
  • Failures: spread copies over three zones. To lose a zone at peak and keep 8, run 12 copies, 4 per zone: 36 shard copies in all.

Shards set how much data you can hold; replicas set how many queries you can serve. Adding shards adds no query capacity, because every query still visits every shard, and adding replicas adds no room for data. The advice flips at the extremes: hundreds of tiny shards make each query pay per-shard overhead and lengthen the tail, so keep the shard count as low as memory allows. How does one query use all those copies? That is the next page.

Scatter-gather: one query, many shards

A query first reaches a coordinator, a service that knows where every shard lives from a shard map kept in a coordination service (the coordination chapter). It sends the query to one copy of every shard at once: the scatter. Each shard scores its own matches and sends back only its best 10. Collecting those replies is the gather: the coordinator merges the short lists and keeps the overall top 10.

Sending only 10 per shard is safe, because the overall top 10 must be among the shards' top 10s: with 3 shards the coordinator merges 30 candidates, not millions. It is also why deep pages are expensive: page 500 (results 4,991 to 5,000) forces every shard to send its top 5,000.

Predict before you watch: shard 2 is busy and answers in 240 ms, while the other two answer in about 20 ms. How long does the shopper wait, and what could the coordinator do?
  • About 240 ms: the coordinator cannot know the overall top 10 until every shard has answered.
  • It can send the same query to a second copy of shard 2 and use whichever reply comes first.
Animated sceneOne query, three shards, one slow copy0:27

Watch for shard 2: the coordinator cannot answer until every shard has

Read it as steps

Scatter, gather, then a busy shard: first the query waits for it, then a hedge to its replica fixes it.

A query is only as fast as its slowest shard, because the overall top 10 is unknown until every shard answers. Replicas do double duty: a hedge to a second copy cuts the slow tail, and the same copy takes over when a shard dies. The next page puts numbers on both.

Tail latency and hedged requests

The scene had three shards. Real clusters have 20 or 100, and waiting for all of them is like a group trip that leaves only when the last friend arrives: the more friends, the more often someone is late. If each shard copy is slow on 1 query in 100, the chance that at least one of n shards is slow is 1 − 0.99ⁿ: about 10% for 10 shards, 18% for 20, 40% for 50 and 63% for 100. Rare slowness on single machines becomes common slowness for users.

A hedged request attacks this directly. The coordinator waits about as long as a shard's p99, say 50 ms, and if a copy has not answered, it sends the same query to another copy and takes the first reply. Suppose each copy is slow 1% of the time, independently. A shard now stays slow only if both copies are: 0.01 × 0.01 = 1 in 10,000. Across 50 shards, 1 − 0.9999⁵⁰ ≈ 0.5% of queries meet a slow shard, down from 40%. The cost is small: only the 1% of requests that pass 50 ms get a second copy, so about 1% more load.

The advice flips when slowness is not independent. A query that is heavy on every copy, or a cluster that is overloaded everywhere, gains nothing from a hedge and gets extra load at the worst moment, so cap hedges at a few percent of traffic. Behind the hedge sits a deadline: after, say, 150 ms, the coordinator returns whatever has arrived and marks the result partial.

Partial results are a product decision

For a shopping app, results from 19 of 20 shards after 150 ms beat a spinner. For a court records search, a missing document may be unacceptable. Decide which one your product is, and write it into the API contract.

Building the index: in batches or as you go

A fast cluster is useless if its data is old. There are two ways to get documents into the index, and many real systems use both.

Offline batch buildIncremental updates
HowRead every document, write a fresh index, swap it inAdd each new or changed document as it arrives
FreshnessHours old by the time it goes liveSeconds to a minute
Index shapeCompact and fully merged; easy to test before releaseMany small pieces that need merging in the background
Good forThe first build, analyser changes, rebuilding after a bugNew listings, price and stock changes, deletes

Batch builds give a clean, tested index; incremental updates give freshness. Incremental indexing usually works in segments. New documents collect in a small in-memory buffer, and every second or so the buffer is written out as a small, immutable segment that searches can see. A query searches every segment of a shard, and a background job merges small segments into bigger ones so the count stays low.

Because segments never change once written, an update is a delete plus a re-add. The old copy is marked deleted in a small bitmap and dropped for good when its segment is merged. This keeps reads simple and makes segments easy to cache and to copy to replicas.

Keeping the index fresh

Users spot stale search quickly: a sold-out phone still listed, a price that changed an hour ago, a deleted post still showing. Freshness comes from a pipeline that follows the database closely.

  • Capture every change: put each insert, update and delete on a queue, either from the application or by reading the database's own change log (change data capture). As the message queue chapter showed, the queue absorbs bursts.
  • Apply changes in order per document: tag each change with the document's version number, and have the indexer ignore anything older than what the index already holds. A late, out-of-order message then cannot undo a newer one.
  • Refresh often, not instantly: making new segments visible every second is cheap. Doing it on every single write would create thousands of tiny segments.
  • Separate fast fields from slow ones: price and stock change every few minutes, titles rarely. Some systems keep fast-changing values in a separate store that ranking reads at query time, instead of re-indexing the whole document.

When the analyser or the document shape changes, incremental updates are not enough, because every old document was analysed the old way. Build a new index next to the live one from the database, replay the changes that arrived meanwhile, compare results, then point the search alias (a name that refers to one index) at the new index. Rolling back means pointing it back.

Deletes need care

A deleted or banned listing must vanish fast. Give deletes top priority in the pipeline, and for urgent removals also filter results at query time against a small blocklist until the index catches up.

Part 4 · Search by Meaning

Search by meaning: embeddings

Keyword search fails when the words differ but the meaning matches. A shopper who types "shoes for standing all day" wants the listings that say "cushioned sole" and "arch support", yet none of them contains the word "standing". Vector search handles this. A machine-learned model turns each text into an embedding, a list of a few hundred numbers, often 768, placed so that texts with similar meanings get nearby lists.

Every document is embedded once, at index time. Each query is embedded when it arrives, and the engine returns the documents whose vectors sit closest to it. Comparing the query with all 50 million vectors would take about 38 billion multiplications (50 M × 768), far too slow at thousands of queries a second.

So engines build an approximate nearest neighbour (ANN) index. HNSW links each vector to a few dozen neighbours in a layered graph and walks greedily towards the query. IVF groups the vectors into thousands of clusters and searches only the few clusters nearest the query.

Approximate means some true matches are missed. Recall is the share of the true top 10 that the index returns. Every ANN index has a knob that trades speed for recall: how many graph nodes HNSW explores, or how many clusters IVF opens. Turn it up and recall rises towards 100% while each query slows; a common target is about 95%, measured on real queries.

Here is what those vectors cost in memory for the shopping app.

Vectors for 50 M listingsMemory
768 numbers × 4 bytes each50 M × 3 KB ≈ 150 GB
768 numbers × 1 byte each (quantised)≈ 38 GB
HNSW graph links (32 per vector × 4 bytes)≈ 6 GB more

Vectors are memory-hungry: about 3 KB each at full precision, and ANN indexes are fastest when the vectors sit in RAM. Storing each number in 1 byte cuts memory to a quarter at a small cost in recall; many engines win that back by re-scoring the best few hundred candidates with the full vectors kept on SSD.

Hybrid search: keywords plus meaning

Vectors are weak where keywords are strong. A shopper who pastes a model number or a seller's product code wants that exact item, and an embedding may happily return a similar but wrong one. Brand names, sizes and rare words behave the same way. So most systems run both searches side by side and merge the two lists: hybrid retrieval.

Merging is harder than adding scores, because a BM25 score and a vector similarity live on different scales. Reciprocal rank fusion (RRF) ignores the scores and uses only positions: a document earns 1 ÷ (60 + its rank) from each list it appears in, and the totals decide the order. A listing ranked 1st by keywords and 3rd by meaning scores 1/61 + 1/63 ≈ 0.032, ahead of one ranked 1st in a single list (1/61 ≈ 0.016).

A slower re-ranking model can then reorder the top few hundred, as in two-stage ranking. Three things are harder with vectors than with an inverted index.

  • Filters: "under ₹2,000, size 9" is a cheap postings intersection in a keyword index, but an ANN graph has no such shortcut. Filtering after the vector search can leave 2 results out of 10, so engines filter during the walk or fetch extra candidates, such as 200 to return 10.
  • Freshness: a new listing must pass through the embedding model before it can be found, and inserting into an HNSW graph costs more than adding postings. Changing the model means re-embedding all 50 M listings and swapping the alias, as with an analyser change.
  • Sharding: shard by document, as before. Each shard holds an ANN index over its slice and returns its nearest 10, and the coordinator merges by similarity. Each shard's answer is approximate, so the merged one is too.

Part 5 · Running It

What goes wrong at scale

Slow shards are the best-known failure, but not the only one. These are the others that appear as traffic and data grow.

ProblemWhat users seeWhat to do
Hot queriesThe same query thousands of times a minuteCache full result pages for popular queries for 30 to 60 seconds (see the distributed cache chapter)
Deep pagesPage 500 is slow and loads every shardCap the page depth, or page with a cursor: results after the last score and id seen
Uneven shardsOne shard much bigger or busier than the restPlace documents by hashing their id; split large shards; avoid sharding a shared catalogue by seller, where one seller can be huge
Indexing burstsA bulk import slows down queriesGive indexing and querying separate machines or limits; throttle bulk re-indexing
Replicas out of stepResults change on every refreshSend one user's queries to the same replica for the session
A dead machineNothing, if each shard has copies in other zonesRoute around it; build a new copy by copying segments from a healthy replica

Most fixes either keep the load even across shards or keep one heavy job from starving the queries. A machine that dies outright is the easy case, because replicas already cover it.

Names you will hear

Lucene is the Java library inside many engines, with the inverted index, segments and BM25. Elasticsearch and OpenSearch are distributed engines built on it, with shards, replicas and aliases, a common default for product and log search; Solr is an older Lucene-based choice. FAISS is a library of ANN indexes for building your own vector service, and pgvector adds vector search to PostgreSQL, a good fit for a few million vectors beside your data.

In the interview

Search turns up inside many designs, and you rarely build the engine: you decide how data reaches it, how it is split and how queries use it.

Say
"The database stays the source of truth. Changes reach the index through a queue in about 2 seconds, and a lost index is rebuilt from the database."
Say
"I shard by document id: 3 shards of about 33 GB, each with 12 copies across three zones, so 4,000 queries a second survive a lost zone."
Say
"Every query waits for its slowest shard, so after 50 ms I hedge to a second copy: about 1% more load for a much shorter tail."
Avoid
"We will run LIKE queries on the main database." It scans every row, cannot rank, and drags checkout down with it.
Avoid
Sharding by term without explaining multi-word queries and hot terms.

Wrap-up

TermMeaning
Inverted indexA map from each term to the documents that contain it
AnalyserThe steps that turn text into terms: split, lowercase, stem
BM25The usual text-relevance score, built on TF and IDF
Scatter-gatherSend a query to every shard, then merge their top results
SegmentAn immutable piece of a shard's index, merged in the background
AliasA name that points at one index; swap it to switch indexes
EmbeddingA list of numbers that puts similar meanings close together
ANN indexFinds nearby vectors fast by giving up a little recall

Next comes Distributed Logging: the same inverted index pointed at billions of log lines a day, and the question of what is worth keeping, and for how long.

Part 6 · Check yourself

Quick check

Q1A seller cuts a phone's price from ₹15,999 to ₹13,999. The product page updates at once, but search still shows the old price 20 minutes later. Where do you look?

Think it through, then check.

Q2Why shard a search index by document rather than by term, when every query must then visit every shard?

Think it through, then check.

Q3A scraper requests page 1,000 of "shoes", 10 results a page, on a cluster of 20 shards. How many candidates does the coordinator merge, and what do you do?

Think it through, then check.

Q4Shoppers type "shoes for standing all day" and get poor results. The team wants to replace keyword search with vector search. What do you recommend, and at what cost?

Think it through, then check.

Q5A seller saves a new listing, searches for it at once, and it is missing; two seconds later it appears. Which idea from the databases chapter explains this, and how do you hide it?

Think it through, then check.

Next chapter · 19 of 39Distributed Logging~26 min · 1 animated scene