Filter concepts by levelShowing all levels.

System Design · Search vs Database

Search vs Database

What a database index already answers, what a search engine adds and charges for, and the one rule that keeps two copies of a fact from diverging

  • Structured questionsexact, range, order — and transactional reads
  • Relevance questionsranking, fuzziness, faceting — at the cost of a second system
  • One system of recordno dual writes; derived stores are rebuildable

1.When a database index is the right tool

A database index is the right tool whenever the question is structured: exact values, ranges, ordering, and combinations of those, over data you also need to write transactionally. "Orders for this customer, placed in the last 30 days, status pending, newest first" is a structured question, and a composite index answers it with a single range scan that stays fast as the table grows. The database is also the only place that can answer such a question in the same transaction as a write, which matters whenever the answer drives a decision that has to be consistent with what is stored — checking remaining stock, computing a balance, validating a constraint. Beyond the ordinary B-tree, most relational databases carry index types that cover more than people expect: an inverted index type for array containment and JSON keys, a trigram index for substring and fuzzy matching, a partial index that indexes only the rows a hot query actually touches, and built-in full-text search with stemming and ranking. That built-in full-text search is the important middle ground — for many products it is genuinely sufficient, and adopting a separate search engine before exhausting it buys an extra system to run, sync, monitor and reindex in exchange for capabilities the database already had. The rule is that structured filtering and transactional reads stay in the database, and the next concept covers what genuinely justifies leaving it.

Key features

  • A B-tree index answers exact-match, range and ordering questions, and a composite index answers combinations of them in one scan
  • The database is the only place that can answer a query in the same transaction as a write, which is required whenever the answer drives a consistent decision
  • Inverted index types cover array containment and JSON key lookups — structured questions people often assume need a search engine
  • Trigram indexes support substring and fuzzy matching, including leading wildcards that a plain B-tree cannot serve
  • Built-in full-text search with stemming and ranking is sufficient for many products, and exhausting it first avoids an extra system to run, sync and reindex
  • Structured filtering and transactional reads stay in the database even in a system that also runs a search engine

Index types and the question shape each one answers

Index types and the question shape each one answers
Index typeAnswersExample
B-treeEquality, ranges, ordering`status = 'pending' AND created_at > ...` ordered by date
Composite B-treeSeveral predicates plus a sort, in one scan`(customer_id, status, created_at DESC)`
Inverted (GIN-style)Array containment, JSON key lookup`tags @> ARRAY['urgent']`
TrigramSubstring and fuzzy matching`name ILIKE '%anders%'`
PartialA hot subset only, kept small`WHERE status = 'pending'`
Built-in full-textStemmed text matching with ranking`to_tsvector(body) @@ plainto_tsquery(...)`
Question shapes a database index already answers

Exact match

B-tree

Range and order

B-tree, composite

Array / JSON containment

inverted index

Substring, fuzzy

trigram

Stemmed text with ranking

built-in full-text search

  1. Exact match — B-tree
  2. Range and order — B-tree, composite
  3. Array / JSON containment — inverted index
  4. Substring, fuzzy — trigram
  5. Stemmed text with ranking — built-in full-text search

Command

sql
-- equality columns first, then the sort column
CREATE INDEX orders_customer_status_created
    ON orders (customer_id, status, created_at DESC);

-- answered by one contiguous range scan
SELECT * FROM orders
 WHERE customer_id = $1
   AND status = 'pending'
 ORDER BY created_at DESC
 LIMIT 20;

-- and readable inside the same transaction as a
-- write, which no external index can offer

A database index is the right tool for structured questions — exact values, ranges, ordering and their combinations — and it is the only tool that can answer inside the same transaction as a write. Composite indexes put equality columns before the range or sort column. Beyond B-trees, inverted, trigram, partial and built-in full-text indexes cover far more than people assume, and exhausting the built-in full-text search first avoids adopting a whole second system for capabilities the database already had.

2.When a search engine is the right tool

A search engine earns its place when the question stops being "which rows match" and becomes "which rows are most relevant". Four capabilities mark that boundary. Text relevance means results are ordered by how well they match rather than by a column — a document mentioning a rare term three times ranks above one mentioning a common term ten times, which is what scoring functions like BM25 compute and what a database ORDER BY cannot express. Fuzzy matching tolerates typos and spelling variation, so a search for "recieve" finds "receive" without you enumerating misspellings. Faceting returns counts per category alongside the results — 412 in Electronics, 88 in Audio — computed over the whole matching set in one pass, which as separate database aggregates is one query per facet over the same filtered set. And specialised ranking lets you blend relevance with business signals — recency, stock level, margin, personalisation — into one score, tuned continuously without touching the data. Against that, four ongoing costs: a second system to run, secure and upgrade; a synchronisation pipeline that can lag and can break; the index being derived data that must stay rebuildable; and a schema that is largely immutable, so changes mean reindexing. The decision is worth making explicitly, because the capabilities are real and so is the cost, and the middle ground — the database's own full-text search — is frequently skipped.

Key features

  • Text relevance orders results by match quality rather than by a column, using scoring functions like BM25 that weigh term rarity and frequency
  • Faceting returns per-category counts over the whole matching set in one pass, rather than one aggregate query per facet
  • Cost one: a second system to run, secure, upgrade and capacity-plan
  • Cost two: a synchronisation pipeline that lags and can break, making the index and the source disagree
  • Cost three: the index is derived data and must remain rebuildable from the source, which is a design constraint rather than a nicety
  • Cost four: mappings are largely immutable, so a schema change means building a new index and switching over

Four capabilities, and what each replaces

Four capabilities, and what each replaces
CapabilityWhat it doesThe database alternative, and why it falls short
Text relevanceOrders results by match quality (term rarity, frequency, field weights)`ORDER BY` a column — expresses no notion of "better match"
Fuzzy matchingTolerates typos and spelling variantsTrigram similarity works, but degrades on large corpora and complex queries
FacetingPer-category counts over the whole matching set, in one passOne aggregate query per facet, each re-filtering the same set
Specialised rankingBlends relevance with business signals into one tunable scorePossible in SQL, but every tuning change is a query rewrite

Four ongoing costs, stated before adopting

Four ongoing costs, stated before adopting
CostConcretely
A second systemRun, secure, upgrade, capacity-plan, and be paged for
A sync pipelineLags, breaks, and needs its own monitoring and backfill path
Derived-data disciplineThe index must always be rebuildable from the source
Immutable mappingsA schema change is a new index plus an alias switch, not an ALTER
What you buy against what you pay

Capabilities gained

  • Relevance ordering rather than column ordering
  • Typo and variant tolerance without enumeration
  • Per-category counts over the whole result set in one pass
  • Business signals blended into a tunable score

Costs incurred, permanently

  • A second system to run, secure and upgrade
  • A sync pipeline that lags and breaks
  • A derived index that must stay rebuildable
  • Schema changes become reindex-and-switch operations
  • Capabilities gained
    • Relevance ordering rather than column ordering
    • Typo and variant tolerance without enumeration
    • Per-category counts over the whole result set in one pass
    • Business signals blended into a tunable score
  • Costs incurred, permanently
    • A second system to run, secure and upgrade
    • A sync pipeline that lags and breaks
    • A derived index that must stay rebuildable
    • Schema changes become reindex-and-switch operations

Command

json
{
  "query": { "multi_match": {
      "query": "wireles headphnes",
      "fields": ["title^3", "description"],
      "fuzziness": "AUTO"
  }},
  "aggs": { "by_category": {
      "terms": { "field": "category" }
  }}
}
// relevance-ordered results, typo tolerance and
// per-category counts, in one request

A search engine is justified when the question becomes "most relevant" rather than "which match": relevance ordering, typo tolerance, faceting over the whole result set, and business signals blended into a tunable score. All four are real, and so are the four permanent costs — a second system, a sync pipeline that lags and breaks, an index that must stay rebuildable, and mappings that make a schema change a reindex. Exhaust the database's own full-text search before paying them.

3.One system of record per fact, and how the copies stay in step

Once a system holds the same fact in more than one store — a database and a search index, a cache, a warehouse, a materialised feed — exactly one of them has to be the system of record, and every other copy has to be derived, rebuildable, and never authoritative. That rule sounds obvious and is broken in two specific ways. The first is the dual write: application code writes to the database and then writes to the search index, as two separate operations with no shared transaction, so any failure between them leaves the two permanently disagreeing with nothing to detect or repair it. The fix is to write once and derive the rest: record the change in the same transaction as the data — an outbox row — or read the database's own replication stream with change data capture, and drive the index from that. Both give a single ordered source of changes, both survive a crash, and both make the index reconstructible by replaying from the beginning. The second failure is on the read side: querying the derived store for a value that has to be correct. Search indexes are the usual offender, because they are convenient and already hold most of the record. Serving a price, a balance, a permission decision or a count that anything depends on from an index that lags, that may have been reindexed with a bug, and that carries no transactional guarantee, is how a system ends up charging the wrong amount or showing one customer another customer's document. The read rule is simple: derived stores answer "which items", the system of record answers "what is true about this item".

Key features

  • Exactly one store is the system of record for a given fact; every other copy is derived, rebuildable, and never authoritative
  • A dual write — write the database, then write the index — has no shared transaction, so a failure between the two leaves a permanent, undetected disagreement
  • Writing an outbox row inside the business transaction makes the change durable with the data, and a relay drives every derived store from it
  • Change data capture reads the database's own replication stream, giving the same single ordered change source without application changes
  • Read rule: a derived store answers "which items match"; the system of record answers "what is true about this item"
  • Never serve a price, balance, permission decision or dependent count from a lagging derived store — it can be stale, wrongly reindexed, and carries no transactional guarantee

Three ways to keep a derived store in step

Three ways to keep a derived store in step
ApproachHowFailure behaviour
Dual writeApplication writes the database, then writes the indexA crash between the two disagrees permanently, silently, with no repair path
Transactional outboxChange row written in the business transaction; a relay publishes itThe change is durable with the data; a relay outage delays, never loses
Change data captureA connector reads the database replication streamSame guarantee with no application change; adds an operational component

What may and may not be read from a derived store

What may and may not be read from a derived store
ReadFrom the derived store?Why
"Which documents match this query?"Yes — this is what it is forThe result set is the derived store's purpose
Ordering and relevanceYesComputed by the index; nothing authoritative depends on it
Approximate result countsYes, if displayed as approximatePresented at the precision actually offered
Price, balance, quota remainingNoCan be stale; drives money and cannot be wrong
Permission decisionsNoA stale ACL in an index is a data leak
Counts anything depends onNoIndex lag makes the number wrong in a way nothing detects
Write once, derive everything else
one write

Application

System of record

the one authoritative copy

Outbox / change stream

one ordered source of changes

Search index

derived — answers "which items"

Cache

derived

Warehouse

derived

Direct second write

no shared transaction — permanent divergence

  • Application
    • leads to System of record (one write)
    • on error, leads to Direct second write
  • System of record — the one authoritative copy
    • leads to Outbox / change stream
  • Outbox / change stream — one ordered source of changes
    • leads to Search index
    • leads to Cache
    • leads to Warehouse
  • Search index — derived — answers "which items"
  • Cache — derived
  • Warehouse — derived
  • Direct second write — no shared transaction — permanent divergence
    • on error, leads to Search index

Command

sql
-- one transaction, two effects, no dual write
BEGIN;
  UPDATE products SET price_cents = 4200
   WHERE id = $1;
  INSERT INTO outbox (aggregate, aggregate_id,
                      event, payload)
  VALUES ('product', $1, 'product.updated', ...);
COMMIT;
-- a relay reads outbox in order and updates the
-- search index, the cache and the warehouse.
-- Replaying outbox from the start rebuilds them.

One store is the system of record for a fact; every other copy is derived, rebuildable and never authoritative. Do not dual-write — record the change in the same transaction as the data (an outbox) or read the replication stream (change data capture), so there is one ordered, replayable source every derived store is built from. And on the read side, let derived stores answer "which items match" while the system of record answers "what is true about this item": prices, balances, permissions and any count something depends on are read from the source, not from an index that lags.

Remember

  • Structured filtering and transactional reads stay in the database, even in a system that also runs a search engine.
  • A search engine buys relevance, fuzziness, faceting and blended ranking — and charges a second system, a sync pipeline, rebuild discipline and reindex-only schema changes.
  • Never dual-write: record the change in the business transaction (outbox) or read the replication stream (CDC).
  • Derived stores answer "which items match"; the system of record answers "what is true about this item".

Pro tip

  • Exhaust the database's own full-text search before adopting an engine — it is the middle ground most teams skip.
  • Use the index filter to narrow candidates and the database to authorise them; that stays fast and fails safe when the index is stale.

Next up

  • System Design Interview Process
  • The ten-step sequence
  • Starting from requirements, not technologies

Sources

  • PostgreSQL — Indexes
  • PostgreSQL — Logical Replication
  • Elasticsearch — Similarity module
  • Checked 2026-08-28

Spotted a mistake on this page? Tell us.