Full-Text Search & Elasticsearch: Inverted Indexing, BM25 Scoring & Search Architecture
Why SQL LIKE collapses at scale, Inverted Index data structures, BM25 relevance scoring, typo tolerance with fuzzy matching, PostgreSQL TSVECTOR vs Elasticsearch, and CDC synchronization.
Relational databases collapse on text search because wildcard SQL LIKE queries force sequential full-table scans. Specialized search engines like Elasticsearch solve this with Inverted Indexing—mapping every word directly to its document locations. Combined with BM25 relevance scoring, field boosting, and Levenshtein typo tolerance, full-text search transforms multi-second database disk thrashing into sub-50ms search ranking.
1. The Fundamental Bottleneck: Why Relational Databases Struggle with Search
When building an initial prototype, backend engineers frequently implement search using simple SQL wildcard queries such as SELECT * FROM products WHERE description LIKE '%laptop%'. While this works instantly on 100 rows, it triggers a catastrophic architectural failure when scaled to production datasets.
'%laptop%' prevents the database B-Tree index from performing binary lookups, forcing the engine into a 100% Full Table Scan that reads every 8KB disk page from disk.| Search Pattern | Database Index Usability | Engine Complexity | Scale Behavior (1,000,000 Rows) |
|---|---|---|---|
Exact Equality (= 'laptop') | ✅ B-Tree Index Active | O(log N) | Sub-millisecond (3 page reads) |
Prefix Match (LIKE 'laptop%') | ✅ B-Tree Range Scan Active | O(log N + K) | Fast (~2–10ms) |
Substring Wildcard (LIKE '%laptop%') | ❌ B-Tree Index Disabled | O(N) Full Table Scan | Multiple seconds (Reads all 100k disk pages) |
Multi-Term ('%high%' AND '%laptop%') | ❌ Exponential Scan Penalty | O(N) Multi-Column Filter | Extreme CPU saturation & connection pool starvation |
2. Inverted Indexing: The Core Data Structure of Search Engines
Instead of storing data as row records and scanning through them, full-text search engines (Elasticsearch, OpenSearch, Apache Lucene) invert the relationship by creating an index of the terms themselves.
[142, 143, 208]. You jump directly to those pages in constant time.01Analysis & Tokenization Pipeline
02Stop-Word Filtering & Stemming
03The Inverted Index Posting List
| Dictionary Term | Document Frequency (DF) | Posting List (Document IDs & Positions) |
|---|---|---|
| laptop | 3 | Doc #1 (pos: 3), Doc #42 (pos: 1), Doc #901 (pos: 8) |
| m3 | 2 | Doc #1 (pos: 5), Doc #88 (pos: 2) |
| ergonomic | 1 | Doc #104 (pos: 1) |
| mechanical | 4 | Doc #12 (pos: 2), Doc #104 (pos: 4), Doc #512 (pos: 1), Doc #780 (pos: 3) |
3. Relevance Scoring: The BM25 Ranking Algorithm
In a relational SQL query, search is binary: a row either matches the WHERE predicate (TRUE) or it does not (FALSE). In real-world search, thousands of documents might match a keyword; the critical requirement is relevance ranking—ensuring the best results appear on page one.
01Term Frequency (TF)
02Inverse Document Frequency (IDF)
03Field-Length Normalization
04Field Boosting (Multipliers)
name^3 is scored 3x higher than a match in description^1.4. User Experience: Typo Tolerance & Type-Ahead Autocomplete
Real users make frequent typos (10%–15% of all queries). If a customer searches for lapotp instead of laptop, a relational database returns zero results, losing the sale.
Fuzzy Matching (Levenshtein Distance)
UX Pillar 1Calculates the minimum number of single-character edits (insertions, deletions, substitutions, or transpositions) required to transform one string into another. For example, 'lapotp' has a Damerau-Levenshtein distance of 1 (transposition of 'o' and 't') from 'laptop'.
Edge N-Gram Tokenization
UX Pillar 2Splits terms into progressive prefix substrings during indexing (e.g. 'mac' ➔ ['m', 'ma', 'mac']). Enables instantaneous sub-10ms type-ahead autocomplete suggestions as the user types each keystroke in the search bar.
Synonym Filtering
UX Pillar 3Normalizes vocabulary variations using synonym graphs (e.g., mapping 'notebook', 'ultrabook', and 'laptop' as interchangeable concepts) so customers find relevant items regardless of specific dialect.
5. Practical Performance Comparison: SQL LIKE vs PostgreSQL TSVECTOR vs Elasticsearch
Real-world performance benchmarks across a dataset of 100,000 product reviews with text searches across 500-word review bodies:
| Search Implementation | Query P95 Latency | Disk / Memory Footprint | Relevance Ranking | Typo Tolerance |
|---|---|---|---|---|
PostgreSQL LIKE '%laptop%' | 2,850 ms – 6,200 ms | High disk page thrashing | ❌ None (Random row order) | ❌ Zero (Exact match only) |
PostgreSQL TSVECTOR + GIN Index | 35 ms – 85 ms | Moderate (~25% table size index) | ⚠️ Basic (ts_rank algorithm) | ⚠️ Requires pg_trgm extension |
| Elasticsearch Cluster | 8 ms – 22 ms | Distributed RAM / Inverted Index | ✅ Industry-standard BM25 + Boosting | ✅ Native Levenshtein Fuzzy Search |
6. Production Architecture: Dual-Storage & Data Synchronization
In production system design, Elasticsearch is rarely used as the primary system of record because it prioritizes search performance over strict transactional consistency (ACID guarantees). Instead, backends use a Dual-Storage Architecture:
01PostgreSQL as Primary System of Record (ACID)
INSERT, UPDATE, and DELETE mutations commit directly to PostgreSQL with strict foreign keys, constraints, and ACID durability.02Asynchronous Event Sync (Message Queue / CDC)
03Elasticsearch as Read-Optimized Search Engine
7. Multi-Language Code Implementations: Elasticsearch & PostgreSQL Full-Text Search
Production code patterns for querying full-text search across TypeScript (Elasticsearch), Go, and PostgreSQL TSVECTOR:
8. The Backend Engineer’s Strategy: Choosing the Right Search Architecture
When designing search systems, follow this decision framework to balance operational complexity against user experience:
| Technology | When to Choose | When to Avoid | Operational Overhead |
|---|---|---|---|
SQL LIKE / Equality | Exact identifiers (SKU, UUID, email lookup), tiny internal datasets (< 1,000 rows). | Any free-form user search bar or substring querying. | Zero overhead (Built-in). |
PostgreSQL TSVECTOR / GIN | Small to medium applications (10,000 to 500,000 records) wanting zero additional infrastructure. | High-scale distributed search, heavy typo tolerance, complex multi-factor BM25 boosting. | Low (Uses existing PostgreSQL database). |
| Elasticsearch / OpenSearch | High-scale applications (> 500k documents), e-commerce catalogs, log analytics, multi-field relevance ranking. | Simple CRUD applications with low traffic where managing a separate cluster is overkill. | Moderate to High (Requires cluster management, JVM tuning, and data sync). |
LIKE '%...%' for user-facing search bars in production.tsvector + GIN) if you have < 500,000 rows to avoid extra infrastructure.