Why Not LIKE '%term%'
the moment a product needs real search
The naive answer to search is WHERE body LIKE '%term%'. It fails on three counts. A leading wildcard can't use an index, so every query is a full table scan (Indexing). There's no relevance â you get matches in arbitrary order, not best-first. And there's no tokenization â searching "running" won't find "run," "Run," or "ran," because it's dumb substring matching.
The moment a product needs to search text meaningfully, it needs a structure built for it: the inverted index.
Inverted Indexes
term â the documents that contain itA normal index maps document â its contents. An inverted index flips it: each term maps to a posting list of the documents that contain it. Now "find documents with 'search'" is one lookup returning the list, not a scan of every document.
Documents Build: tokenize â normalize â index
D1: "The fast fox" split into words â lowercase, stem,
D2: "A fast car" â drop stop-words
D3: "The fox runs" âŧ
Inverted index (term â posting list):
fast ââļ [ D1, D2 ]
fox ââļ [ D1, D3 ]
run ââļ [ D3 ] ("runs" stemmed to "run")
car ââļ [ D2 ]
("the", "a" dropped as stop-words)
Building it is a pipeline. Tokenize the text into terms, then normalize: lowercase, stem words to their root ("running" â "run"), and drop stop-words ("the," "a," "is") that carry no signal. The same pipeline runs on the query, so query and index meet in the same normalized form â which is exactly what makes "Running" match "ran."
The Query Side
combining posting listsOnce terms map to posting lists, queries are set operations on those lists. An AND query intersects the lists â documents that contain both terms. An OR query unions them. A phrase query ("fast fox" as an exact phrase) needs more than membership: the index must store each term's position within the document, so you can check the words appear adjacent and in order.
Relevance
ranking matches best-firstFinding matches is half the job; ordering them is the half users feel. The intuition is TF-IDF: a term matters more in a document when it appears often there (term frequency) and rarely across the whole corpus (inverse document frequency). "The" appears everywhere, so it's worthless for ranking; a rare term is a strong signal. BM25 is the refined, saturating version of this idea and the default scoring function in modern engines.
Search Infrastructure
Elasticsearch, shards, and staying in syncElasticsearch (and OpenSearch, both on Lucene) is the standard engine. Documents are grouped into shards so the index scales horizontally, and indexing is near-real-time: new documents become searchable after a refresh interval, so there's always a small staleness window between writing and being findable.
The real design question is keeping the search index in sync with the source-of-truth database â search is a derived copy, never the primary store.
Capture changes off the database's write-ahead log and stream them into the search index. The DB commit is the single source of truth, and the index follows it reliably â nothing is missed because the log records every change (CDC).
The app writes the DB and the search index directly, in the same request. When one write succeeds and the other fails â and eventually it will â the two drift apart with no way to notice. The classic dual-write problem (Distributed Transactions).
Typeahead / Autocomplete
a different problem: prefixes under 100msAutocomplete looks like search but isn't. It's prefix matching under a brutal latency budget â suggestions must appear within ~100ms of a keystroke, and every keystroke is a query. The answer is precompute-heavy and read-cheap: do the work offline so serving is a trivial lookup.
A TRIE with precomputed top-k per node:
(root)
ââ s
ââ se
ââ sea ââ top-k: [search, seattle, season, âĻ]
ââ sear ââ top-k: [search, search engine, âĻ]
Type "sear" â walk to that node â return its precomputed top-k. Done.
Suggestions are RANKED and STORED at build time, weighted by popularity.
Each node stores the top-k completions for its prefix, ranked by popularity, computed ahead of time. Serving is a walk down the trie plus returning a cached list â no ranking at request time. Fuzzy matching (tolerating typos) and freshness of trending queries are add-ons layered on top. This is the whole shape of the Typeahead case study (Typeahead).
Quick Reference
DB query vs search vs typeaheadDB LIKE | Search engine | |
|---|---|---|
| Structure | B-tree (unusable with leading wildcard) | Inverted index |
| Speed | Full scan | Posting-list lookup |
| Relevance | None | BM25 + signals |
| Tokenization | None â raw substring | Stem, lowercase, stop-words |
| Full-text search | Typeahead | |
|---|---|---|
| Index | Inverted index | Trie with top-k per node |
| Latency budget | Sub-second | ~100ms per keystroke |
| Update pattern | Near-real-time via CDC | Precomputed offline, read-cheap |
LIKE can't do search: no index, no relevance, no tokenization. An inverted index maps terms to posting lists; queries are set ops on them. BM25 for text relevance, then layer recency and popularity. Keep the search index in sync via CDC, never dual-write, and never make it the source of truth. Typeahead is a separate, precompute-heavy prefix problem with a 100ms budget.