Art of Codeartofcode
LLeeaarrnniinngg
BBlloogg
TToooollss
GGaammeess AArreennaa
JJoobbss
LLiivvee CCoouurrsseess

Courses

  • System DesignHigh-level system design, one everyday scene at a time: 30 concepts and 15 interview rounds.712 lessons
All coursesTutorialsMy Learning

Categories

  • 01AI & Tools
  • 02Tutorials
  • 03Engineering
  • 04Reviews
  • 05Build Logs
  • 06Tales of Misfits
  • 07Opinion
  • 08Career
  • 09News

Read & write

All storiesEvery post, newest first.Write for usPublish your stories and earn from what you write.Writer leaderboardsWho is writing the most read posts.

Categories

  • PDF Tools25
  • Development09
  • Utilities10
  • Design04
  • Converters04
  • Generators02

Featured

JSON FormatterFormat, validate and minify JSON.Regex TesterTest patterns against live text.Merge PDFCombine PDFs in your browser.JWT DecoderRead a token’s header and claims.Password GeneratorStrong passwords, generated locally.Gradient GeneratorBuild CSS gradients visually.
All 53 tools

Play a round

DevdleOne developer word a day, six guesses.Python RushClassic snake, with coding icons to eat.Blink or BreakA quick test of your reflexes.
Games Arena

Learn with us

Live CoursesInstructor-led courses. Join a waitlist and be first in when one starts.LearningStructured programming courses with AI assistance and certificates.

System Design

19. Search and Inverted Indexes

Lesson 13 of 15

19. Search and Inverted Indexes

1
The picture to keep in your head
2
19.1 Why doesn't `LIKE '%word%'` scale?
3
19.2 How do you tokenise and normalise text?
4
19.3 What is an inverted index?
5
19.4 How does ranking work (TF-IDF, BM25 in brief)?
6
19.5 How do prefix search and tries work?
7
19.6 How do you keep the search index in sync with the database?
8
19.7 Where does Elasticsearch fit in the big picture?
9
19.8 How do you shard a search index?
10
Memory card and where the analogy breaks
11
Practice: five levels
12
Interview questions
13
Solutions
14
Sources
15
Code lab
19. Search and Inverted Indexes

Solutions

3 min read+10 XP

Solutions

Level 1

reveal

Level 2

reveal

Level 3

reveal

Level 4

reveal

Level 5 (sketch)

reveal

No tracks available

Lofi Player

artofcode

artofcode.in

The den of devs.

Stories, tools, games, and gear for developers.

Read the StoriesEExxpplloorree TToooollss
Art of Codeartofcode

Platform

  • BBlloogg
  • TToooollss
  • GGaammeess AArreennaa
  • CCoouurrsseess
  • LLeeaarrnniinngg
  • CCaammppuuss

Company

  • AAbboouutt UUss
  • JJoobbss && IInntteerrnnsshhiippss
  • CCoonnttaacctt

Legal

  • TTeerrmmss ooff SSeerrvviiccee
  • PPrriivvaaccyy PPoolliiccyy
  • RReeffuunndd PPoolliiccyy

Copyright © 2026 artofcode.in, All Rights Reserved.

IST·--:--

1Show the Level 1 solution
2**A.**
31. **False.** A B-tree helps prefixes (`'kerala%'`), not a word in the middle.
42. **True.** Otherwise "Database" in the query won't match "database" in the index.
53. **False.** Start from the **shorter** list (the rarer word); fewer checks.
64. **False.** BM25 saturates: extra repeats add less and less.
75. **False.** It's a rebuildable copy without multi-document transactions; orders belong in the database.
8
9**B.**
101. **Synonyms** (DB → database).
112. **CDC** (or an outbox) instead of a nightly batch.
123. **More replicas** (more copies to share the queries).
134. **Field boost** on the title.
145. **Trie or edge n-grams.**
Show the Level 2 solution
1. 2,000,000 × 1 KB = 2 GB; 2 GB ÷ 500 MB/s = **4 seconds** per search, so about **0.25 searches a second** for the whole database. Hopeless.
2. 2,000,000 × 200 = **400 million** postings at most (fewer, since repeated words in one book share a posting).
3. Walk the **"kerala"** list (50 items) and check each against "recipes": **50 checks**, instead of 20,000.
4. 3 × (1 primary + 2 replicas) = **9 shard copies**. Each query needs one copy of each of the 3 shards, so total capacity ≈ 9 × 200 ÷ 3 = **600 queries a second** (assuming even spread and no other bottleneck).
1Show the Level 3 solution
2Tested solution (Python 3), the key parts:
3
4```python
5STOPWORDS = {"a", "an", "the", "of", "and", "or", "to", "in", "for", "on", "with", "is"}
6
7def stem(word):                      # tiny on purpose; use a real stemmer in production
8    for suffix in ("ing", "s"):
9        if word.endswith(suffix) and len(word) - len(suffix) >= 3:
10            return word[: -len(suffix)]
11    return word
12
13def analyze(text):
14    words = re.findall(r"[a-z0-9]+", text.lower())
15    return [stem(w) for w in words if w not in STOPWORDS]
16
17
18class InvertedIndex:
19    def search_and(self, query):
20        terms = analyze(query)
21        lists = sorted((set(self.postings.get(t, {})) for t in terms), key=len)
22        result = lists[0]                      # rarest term first
23        for other in lists[1:]:
24            result = result & other
25        return sorted(result)
26
27    def bm25(self, query, k1=1.2, b=0.75):
28        avg = sum(self.doc_len.values()) / len(self.doc_len)
29        scores = Counter()
30        for t in analyze(query):
31            for doc, positions in self.postings.get(t, {}).items():
32                tf = len(positions)
33                norm = tf * (k1 + 1) / (tf + k1 * (1 - b + b * self.doc_len[doc] / avg))
34                scores[doc] += self.idf(t) * norm
35        return scores
36
37    def apply_change(self, event):
38        doc_id, version, text = event
39        if version <= self.version.get(doc_id, 0):
40            return False                       # stale or duplicate: ignore
41        self.version[doc_id] = version
42        if text is None:
43            self.remove(doc_id)
44        else:
45            self.add(doc_id, text)
46        return True
47```
48
49All checks pass: "database" finds books 2 and 6; the phrase "system design" finds only book 5; TF-IDF scores the triple "databases" title **3×**, BM25 about **1.9×**; "dat" completes to data, database, date; a late version-2 event is ignored after version 3; and the sharded search returns the same top results. The full file, including the trie and phrase search, is in `code/ch19_search.py`.
1Show the Level 4 solution
2**The bug:** books go through `analyze()` when indexed ("Database" becomes "database"), but the **query doesn't**. The lookup is for the raw string "Database", with a capital D, and no such term exists in the index. Multi-word queries fail too: "database internals" is looked up as one term.
3
4**The fix:** run the **same analyzer** on the query, then intersect the posting lists:
5
6```python
7def search(self, query):
8    terms = analyze(query)                   # same analyzer as indexing
9    if not terms:
10        return []
11    return sorted(set.intersection(*(self.postings.get(t, set()) for t in terms)))
12```
13
14In the tested demo, "Database" now returns books 2 and 6.
1Show the Level 5 (sketch) solution
21. **Sync:** the books table is the truth. Changes go through an **outbox** (or CDC) into a queue; an indexer applies them with **version checks** and handles deletes. A nightly or weekly reindex into a fresh index, switched by alias, repairs drift. Price and stock changes flow the same way, typically visible within a few seconds.
32. **Analyzer and ranking:** English analyzer (lowercase, stop words, stemming) plus synonyms ("DB" → database); author names with a lighter analyzer; ISBN as an exact keyword. BM25 with **title boosted** over description, plus popularity and in-stock as score factors. Filters for price and category don't affect score.
43. **Autocomplete:** edge n-grams on titles and authors, or a trie built hourly from popular searches and titles, with the top 5 stored per prefix; cached at the edge for one- and two-letter prefixes; client debounces keystrokes.
54. **Sharding:** 2 million books is small for a search engine: a single primary shard may be enough; choose, say, **2 to 3 primaries** for growth and **2 replicas** each for throughput and failover. Measure before adding more.
65. **Cluster down:** the rest of the site keeps working. Search falls back to cached results for top queries and a strict-timeout title-prefix query on a database read replica, behind a rate limit, with a "search is limited right now" message.