Loading
Loading
Storage Internals
Inverted indexes for full-text search, local versus global secondary indexes, and keeping a derived index in step with its source.
By the end of this chapter
A B-tree index on a title column answers 'titles equal to X' and 'titles starting with X', because both map to one contiguous range of the sorted keys. 'Titles containing the word socks somewhere' does not — the matches are scattered across the whole sort order, so the database falls back to reading every row.
Full-text search also wants things exact matching never offered: 'Running' should match 'run', 'colour' might match 'color', and the results should come back ranked by relevance rather than in table order.
Concept
For every term, a sorted list of the documents that contain it — a posting list. Searching becomes looking up a few terms and combining their lists, so the cost depends on how many documents match, not on how many exist.
Before indexing, text goes through an analysis pipeline: split into tokens, lower-case them, optionally drop very common words, and optionally reduce words to a stem so 'running' and 'runs' share an entry. The same pipeline runs on the query, which is the whole trick — query and documents are normalised the same way, so they meet in the middle.
Doc 1: "Red running shoes" · Doc 2: "Running socks" · Doc 3: "Red wool socks". Query: red AND socks.Analyse: lower-case, stem running → runDoc 1: red, run, shoe · Doc 2: run, sock · Doc 3: red, wool, sockred → [1, 3]; run → [1, 2]; shoe → [1]; sock → [2, 3]; wool → [3]Each posting list is kept sorted by document id.Query analysed the same way: red, sock'socks' in the query becomes 'sock', matching the stored term.Intersect [1, 3] with [2, 3] → [3]Walk both sorted lists together, as in a merge. Cost is proportional to list lengths.Doc 3 is the only match for the AND query; an OR query would return all three, with doc 3 ranked first.Ranking then scores each match. The common family of scoring functions rewards a term that appears often in this document but rarely across the corpus — 'wool' is more informative than 'red' if most products are red — and normalises for document length so long documents do not win by volume alone.
A dedicated search engine is usually a separate system from the primary database. That makes it the textbook case of the second-store problem from the Data section: two systems holding related data, which will disagree, and one of which must be declared the truth. The database is the truth. The index is derived, may lag, and must be rebuildable from scratch.
Components (6): Client [client]; API [api]; Primary DB [database] — source of truth + outbox table; Change stream [queue] — ordered per document id; Indexer [worker] — idempotent upserts by doc id + version; Search index [search] — derived, may lag Connections (6): Client -> API; API -> Primary DB (write row + outbox in one txn); Primary DB ~> Change stream (relay changes); Change stream ~> Indexer (consume); Indexer -> Search index (upsert / delete); API -> Search index (search queries) Legend: -> synchronous, ~> asynchronous, => replication.
Dual writes — the API writes the database, then writes the index — look simpler and drift forever. The second write fails after the first succeeds; two concurrent updates reach the two systems in opposite orders; a retry re-sends an old version after a newer one landed. Each case leaves the index permanently wrong with no record that it is.
Once data is sharded by a primary key, a secondary index has to be partitioned too, and there are two ways to do it.
| Local (per-shard) index | Global (term-partitioned) index | |
|---|---|---|
| Where index entries live | With the rows they point to | Partitioned by the indexed value |
| Write | Same shard, same transaction | Often a different partition; usually updated asynchronously |
| Query by indexed value | Ask every shard and merge (scatter-gather) | Ask the one partition that owns the value |
| Freshness | Immediately consistent | May lag the write |
Worked illustration: with 32 shards, a local-index query for 'orders with status = refunded' becomes 32 parallel requests, and its latency is the slowest of the 32. A global index answers from one partition, but an order refunded a moment ago might not appear yet.
Sign in to keep a note here. It is free, and nobody else can see it.
Sign inWhat this chapter was for: