Filter concepts by levelShowing all levels.

System Design · Search Systems

Search Systems

Why free-text search needs a different index structure than a database, and where that structure lives

  • B-tree limitsno ranking, no fuzzy match, LIKE %x% can't use an index
  • Inverted indexterm -> documents, built via tokenize -> analyze
  • Rankingrelevance scoring, e.g. BM25 (TF-IDF family)
  • Not primarysearch engine holds a derived, rebuildable copy

1.When database search is insufficient

A relational database's indexes (B-trees, hashes) are built to find rows by exact value or by range on a known column — "id = 5" or "created_at between X and Y." They are not built to answer "which documents contain words similar in meaning to this phrase, ranked by how relevant each one is." A LIKE '%term%' query can't use a B-tree index at all, so it falls back to scanning every row and string-matching inside it — correct, but linearly slow, and it still can't rank results, tolerate typos, understand word forms (run/running/ran), or search across many fields at once with one relevance score. Once a product needs free-text search with relevance ranking, typo tolerance, faceted filtering, or search across millions of documents with sub-second latency, that's the signal to bring in a dedicated search engine instead of asking the primary database to do more.

Key features

  • A LIKE '%term%' predicate (leading wildcard) cannot use a standard B-tree index — the database scans every row and does a substring match inside it, so cost grows linearly with table size.
  • Relational indexes answer "equals this value" or "falls in this range" on one or a few columns — they have no built-in concept of relevance ranking across many text fields.
  • Full-text search needs typo tolerance (fuzzy matching), stemming (run/running/ran treated as related), and synonym handling — none of which a plain B-tree provides.
  • Faceted filtering — narrowing results by category, price range, and tags simultaneously while keeping counts per facet — is a search-engine feature, not a SQL index feature.
  • PostgreSQL's own full-text search (tsvector/GIN index) and MySQL's FULLTEXT index close part of this gap for a single database — the decision point is when ranking quality, query volume, or multi-field relevance outgrows what those built-ins handle well.

What a B-tree index handles vs. what free-text search needs

What a B-tree index handles vs. what free-text search needs
CapabilityB-tree / hash indexDedicated search engine
Exact match / range on a columnFast, native use caseAlso supported, but not its strength
Substring / leading-wildcard matchFull table scan — no index useNative, indexed
Relevance ranking across fieldsNot supportedCore feature (scoring algorithm)
Typo tolerance, stemming, synonymsNot supportedBuilt into the analysis pipeline
Faceted counts across filtersRequires manual GROUP BY per facetNative aggregation support
B-tree index vs. dedicated search engine

B-tree / hash index

  • Fast exact match / range on a column
  • Leading-wildcard LIKE forces a full scan
  • No relevance ranking, typo tolerance, or facets

Dedicated search engine

  • Native substring/leading-wildcard matching
  • Relevance ranking is a core feature
  • Stemming, synonyms, faceted counts built in
  • B-tree / hash index
    • Fast exact match / range on a column
    • Leading-wildcard LIKE forces a full scan
    • No relevance ranking, typo tolerance, or facets
  • Dedicated search engine
    • Native substring/leading-wildcard matching
    • Relevance ranking is a core feature
    • Stemming, synonyms, faceted counts built in

Command

sql
-- This predicate cannot use a standard B-tree index:
SELECT * FROM articles WHERE body LIKE '%distributed consensus%';
-- Postgres/MySQL will scan every row and string-match inside it.

A B-tree finds exact values and ranges; it can't rank relevance, tolerate typos, or match leading wildcards without a full scan. Reach for a dedicated search engine once ranking, fuzzy matching, or faceting outgrows the database's built-in full-text search — not before.

2.Inverted indexes, tokenization, analyzers and ranking

An inverted index flips the natural document-to-words mapping around: instead of "document 5 contains these words," it stores "this word appears in documents 3, 5, 9" for every word. That flip is what makes free-text search fast — finding every document containing a term becomes a direct index lookup instead of scanning every document's text. Getting from raw text to that index takes a pipeline: tokenization splits text into individual terms, an analyzer normalizes those terms (lowercasing, removing punctuation, reducing words to a root form, dropping common words like "the"), and the resulting terms are what actually gets stored in the inverted index. At query time the same analysis runs on the search query, matching documents are found via the index, or narrowed further with exact-match filters, and then ranked by a relevance score — typically based on how often a term appears in a document relative to how common that term is across the whole collection (a TF-IDF-style measure such as Elasticsearch's default BM25).

Key features

  • Tokenization splits raw text into individual terms (roughly: words) — "Distributed Systems!" becomes tokens like "Distributed" and "Systems".
  • An analyzer normalizes tokens after splitting: lowercasing, stripping punctuation, stemming word forms to a root (running/ran → run), and often dropping stopwords (the, a, of).
  • The inverted index maps each normalized term to the list of documents (and often positions within them) that contain it — the structure that makes "find all docs with this term" an index lookup instead of a full scan.
  • The same analyzer must run on both indexing and query text, or terms won't match — indexing "running" as "run" but searching for the literal string "running" would miss it.
  • Ranking scores matches by relevance, commonly via a TF-IDF-family formula: how often a term appears in a document (term frequency) weighed against how rare that term is across all documents (inverse document frequency) — Elasticsearch and OpenSearch use BM25 by default.
  • Filtering narrows the candidate set by exact-match criteria (category = "books", price < 50) and, unlike ranking, is typically binary — a document either passes a filter or it doesn't, with no relevance score involved.

One sentence through the analysis pipeline

One sentence through the analysis pipeline
StageInputOutput
Raw text—"The Quick foxes leap"
Tokenizer"The Quick foxes leap"[The, Quick, foxes, leap]
Lowercase filter[The, Quick, foxes, leap][the, quick, foxes, leap]
Stopword filter[the, quick, foxes, leap][quick, foxes, leap]
Stemmer[quick, foxes, leap][quick, fox, leap]

Pipeline stage vs. what it decides

Pipeline stage vs. what it decides
StageQuestion it answers
TokenizationWhere does one term end and the next begin?
Analyzer (normalization)Which distinct strings should count as the same searchable term?
IndexingWhich documents contain this term, and where?
FilteringWhich documents pass exact-match criteria, before or alongside scoring?
RankingOf the matching documents, which are most relevant to this query?
Indexing pipeline: raw text to the inverted index
splitnormalizestore

Raw text

"Running distributed systems"

Tokenizer

splits into terms

Analyzer

lowercase, stem, drop stopwords

Inverted index

term → [doc IDs]

  • Raw text — "Running distributed systems"
    • leads to Tokenizer (split)
  • Tokenizer — splits into terms
    • leads to Analyzer (normalize)
  • Analyzer — lowercase, stem, drop stopwords
    • leads to Inverted index (store)
  • Inverted index — term → [doc IDs]

Command

text
Indexing pipeline (build time):
  raw text -> tokenizer -> token filters (lowercase,
  stem, stopwords) -> terms -> written into the
  inverted index as term -> [doc IDs, positions]

Query pipeline (search time):
  raw query -> SAME analyzer -> terms -> look up each
  term in the inverted index -> candidate docs
  -> apply filters (exact match) -> score remaining
  docs (e.g. BM25) -> return ranked results

Pipeline: tokenize (split into terms) -> analyze (normalize: lowercase, stem, drop stopwords) -> build the inverted index (term -> document list) -> at query time, the same analysis on the query -> filter (exact match) -> rank (relevance score, e.g. BM25). Index and query must use the same analyzer or terms won't line up.

3.Search engines are specialized, not primary stores

Elasticsearch and OpenSearch are built to answer "find and rank the documents matching this query" extremely well — that's their one job, and their storage model, replication, and query language are all shaped around it. They are not built to be a system's primary, authoritative datastore: they generally lack the transactional guarantees (multi-row ACID transactions, foreign-key constraints) that a relational database provides, and updates are commonly modeled as reindexing a whole document rather than fine-grained partial writes. The standard architecture keeps the real, authoritative data in a primary store (PostgreSQL, MySQL, MongoDB) and indexes a copy of the searchable fields into the search engine — the search engine holds derived data, not the source of truth, and if its index is ever lost, it must be rebuildable by reindexing from the primary store.

Key features

  • Search engines optimize for read-heavy, relevance-ranked query patterns — not for the multi-row ACID transactions and foreign-key integrity a relational primary store provides.
  • The standard pattern is: primary database holds the authoritative record, an indexing pipeline (batch job, change-data-capture stream, or dual write) copies searchable fields into the search engine.
  • Because the search engine's data is derived, its index should be treated as rebuildable — a disaster-recovery plan should be able to reindex from the primary store rather than depend solely on the search engine's own durability.
  • Keeping the two stores in sync (search index following primary-store writes) is itself a design problem — with real, bounded lag between a write landing in the primary store and becoming searchable, not instant consistency.
  • Using a search engine as the only store for data that also needs strict transactional consistency (e.g. financial balances) is a common architecture mistake — those guarantees belong in the primary relational/transactional store, not the search index.

Primary store vs. search engine — where each responsibility lives

Primary store vs. search engine — where each responsibility lives
ResponsibilityPrimary store (e.g. PostgreSQL)Search engine (Elastic/OpenSearch)
Source of truthYes — authoritative recordNo — holds a derived, searchable copy
Multi-row transactionsYes (ACID)Not designed for this
Relevance-ranked free-text searchLimited (built-in full-text search)Core strength
Recovery if data is lostBackups / replication are the only path backCan often be rebuilt by reindexing from the primary store
Primary store is authoritative; the search engine is derived
writecopyindex

Writes

Primary store

Postgres/MySQL — source of truth

Indexing pipeline

CDC or batch job

Search engine

Elastic/OpenSearch — derived, rebuildable

  • Writes
    • leads to Primary store (write)
  • Primary store — Postgres/MySQL — source of truth
    • leads to Indexing pipeline (copy)
  • Indexing pipeline — CDC or batch job
    • leads to Search engine (index)
  • Search engine — Elastic/OpenSearch — derived, rebuildable

Command

text
Typical architecture:

  writes ---> [ Primary store: Postgres/MySQL/Mongo ]
                        |
                (indexing pipeline: CDC stream,
                 batch job, or dual write)
                        v
              [ Search engine: Elastic/OpenSearch ]
                        ^
  reads (search) -------'

  The primary store is authoritative. The search
  engine is a derived, rebuildable index of it.

Elastic/OpenSearch are specialized for ranked, faceted, free-text search — not ACID transactions or being the source of truth. Standard pattern: primary store (Postgres/MySQL/Mongo) holds the authoritative data; an indexing pipeline copies it into the search engine, which should always be rebuildable from the primary store.

Remember

  • A leading-wildcard LIKE query can't use a B-tree index and still can't rank or fuzzy-match results even when it finishes.
  • Tokenize -> analyze (lowercase, stem, drop stopwords) -> inverted index (term -> docs) -> filter (exact match) -> rank (relevance score).
  • Index and query text must go through the same analyzer, or terms won't line up.
  • Elastic/OpenSearch hold a derived, searchable copy of data — the primary store stays authoritative, and the index should be rebuildable from it.

Pro tip

  • Try the database's built-in full-text search (tsvector/GIN in Postgres, FULLTEXT in MySQL) before standing up a separate search cluster.
  • Map identifiers and codes as unanalyzed ("keyword") fields — the standard full-text analyzer will silently break exact-match filters on them.

Next up

  • Caching fundamentals
  • CDN and edge caching
  • Replication

Sources

  • Elasticsearch documentation — elastic.co/guide
  • PostgreSQL documentation — postgresql.org/docs
  • Checked 2026-08-22

Spotted a mistake on this page? Tell us.