SD System Design In Depth
0%
design system-designsearchindexing

Design: a search service

Module 6.2: Interactive Product System Designs Updated Sep 18, 2026
📄 Cheat Sheet
Design FB Post Search: System Design Interview breakdown w/ ex Meta Interviewer
Hello Interview Design FB Post Search: System Design Interview breakdown w/ ex Meta Interviewer Watch on YouTube ↗

A full design of a search service: ingestion, the inverted index, sharding, ranking and freshness. The current video is about autocomplete, a different problem.

0:00 / --:--

Search Service Architecture Overview

A production search service (like Elasticsearch, Solr, or internal enterprise search) consists of two decoupled subsystems: the Write/Indexing Pipeline and the Read/Query & Ranking Engine. The goal is to ingest documents, extract terms, compute relevance, and serve full-text queries in under 50 milliseconds across billions of documents.

Indexing & Query Flow

flowchart TD Doc["Raw Documents / Database"] --> Parser["Text Tokenizer & Stemmer"] Parser --> InvertedIndex[("Inverted Index & Posting Lists")] Client["Search Query"] --> QParser["Query Analyzer"] QParser --> Coordinator["Search Coordinator Node"] Coordinator --> S1["Shard 1"] Coordinator --> S2["Shard 2"] Coordinator --> S3["Shard 3"] S1 & S2 & S3 --> Aggregator["Merge & BM25 Scoring"] Aggregator --> Ranking["ML Re-ranking / Personalization"] Ranking --> Response["Top K Results"]

Inverted Index & Posting Lists

Rather than scanning records sequentially, search engines build an inverted index mapping each tokenized word to a sorted list of document IDs (called a posting list):

"distributed" -> [Doc1, Doc4, Doc12, Doc98]
"systems"     -> [Doc2, Doc4, Doc15, Doc98]

Boolean conjunction queries (distributed AND systems) intersect sorted posting lists in O(N + M) time using skip lists or bitset operations.

Relevance Scoring: BM25

Relevance ranking evaluates how well a document matches a query based on Best Matching 25 (BM25), balancing:

  • Term Frequency (TF): How often the term appears in the document (with diminishing returns).
  • Inverse Document Frequency (IDF): How rare the term is across the entire corpus (common words like "the" receive low weight).
  • Field-Length Normalization: Term frequency is normalized by document length relative to the average (BM25's b parameter), so the same match counts for more in a short document than in a long one.

Worked example: BM25 inverse document frequency

The corpus has N = 1,000,000 documents. Lucene's BM25 IDF is ln(1 + (N - n + 0.5) / (n + 0.5)), where n is the number of documents containing the term.

  • "kafka" appears in n = 1,000 documents: ln(1 + 999000.5 / 1000.5), which is about ln(1000), about 6.9.
  • "system" appears in n = 400,000 documents: ln(1 + 600000.5 / 400000.5), which is ln(2.5), about 0.92.
  • "the" appears in n = 950,000 documents: about ln(1.053), about 0.05, which is effectively noise.

The term-frequency part saturates. With Lucene's defaults k1 = 1.2 and b = 0.75, a term appearing 10 times scores only about 2 times as much as appearing once in a document of average length. Longer-than-average documents are damped by the b parameter. So a match on "kafka" is worth about 7 times a match on "system", no matter how often "system" repeats.

How to partition the index

StrategyQuery pathWrite pathUsed by
Document-partitioned (each shard indexes a subset of documents)Scatter to every shard, gather top K from each, mergeEach document goes to one shardElasticsearch, Solr, most production systems
Term-partitioned (each shard owns some terms)Only the shards holding the query's termsOne document updates many shardsRare; research systems and some early web search engines
Tiered (a small fresh index plus a large base index)Query both and mergeNew documents go to the fresh tierWeb search engines, Twitter's Earlybird real-time index

Latency budget and tail behaviour

Suppose each of 20 shards has a p99 of 40 ms. The chance that at least one of the 20 is slower than its p99 is 1 - 0.99^20, about 18%, so the query's p99 is much worse than any one shard's. Mitigations:

  • Fewer, larger shards (Elastic suggests 10 to 50 GB per shard).
  • Replicas, with adaptive replica selection that routes to the least-loaded copy.
  • Hedged requests: after about the p95 wait time, send a duplicate request to another replica and take whichever answers first.
  • Return partial results with a timeout rather than failing the whole query.

Pipeline

flowchart LR S[("Source DB")] --> CDC["CDC stream"] CDC --> AN["Analyzer: tokenize, lowercase, stem, synonyms"] AN --> IX["Indexer writes segments"] IX --> SH[("Shards and replicas")] Q["Query"] --> QA["Same analyzer"] QA --> SH SH --> RR["Top 1000 by BM25, then ML re-rank to top 20"]

The query must go through the same analyzer used at index time. If documents were stemmed ("running" became "run") but the query was not, nothing matches.

Check yourself Question 1 of 3
Posting list intersection recall
From: Inverted Index & Posting Lists

The posting lists are raft = [3, 8, 21, 40, 77] and leader = [8, 15, 40, 90]. How does the engine answer raft AND leader, and what is the result?

Key Takeaways

Decouple the asynchronous indexing pipeline from the read/query coordinator tier.
An inverted index with sorted posting lists powers sub-50ms multi-term queries.
BM25 scoring combines term frequency, inverse document frequency, and length normalization.
Shard indexes by document ID and use scatter-gather coordinator nodes to query shards in parallel.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue