Guide
Elasticsearch fundamentals explained
Your storefront search box returns irrelevant results — users type
“waterproof hiking boots” and see unrelated accessories because
LIKE '%boot%' on
PostgreSQL
cannot rank by relevance, stem plurals, or tolerate typos at millisecond latency.
Elasticsearch is a distributed search and analytics
engine built on Apache Lucene: it stores inverted indexes that map
terms to document IDs, scores hits with BM25, and shards data
across nodes for horizontal scale. Netflix, GitHub, and the Elastic Stack use it
for product catalogs, log pipelines, and security analytics. Elasticsearch is
not a transactional database — it complements OLTP stores and pairs with
hybrid search
patterns when dense embeddings join keyword retrieval. This guide covers Lucene
inverted indexes, index mappings and analyzers, the bool Query DSL, scoring and
relevance tuning, aggregations and facets, shard and replica topology, bulk
indexing and ingest pipelines, a Harbor Market e-commerce worked example, a
search-engine decision table, common pitfalls, and a production checklist.
What Elasticsearch is
Elasticsearch (ES) is a document-oriented search engine. Each JSON document
lives in an index (roughly a table). Documents are identified
by _id and routed to a shard via a hash of
_routing (default: document ID). A cluster of nodes holds
indices; one node is elected master for cluster metadata,
while data nodes store shards and execute queries.
Lucene under the hood
Each shard is a Lucene index — an immutable segment collection on disk. Lucene builds an inverted index: for every term (token), it stores a sorted list of document IDs and positions. A query walks these posting lists, intersects or unions them, and computes a relevance score. Segments merge in the background; deleted documents are tombstoned until merge compacts them away.
Elasticsearch adds distributed coordination: routing queries to the correct shards, merging scores across shards, replication for failover, and a REST API plus Query DSL on top of raw Lucene APIs. Managed offerings (Elastic Cloud, AWS OpenSearch Service) hide node provisioning; self-hosted clusters require JVM heap tuning and disk I/O planning.
Mappings, fields and analyzers
An index mapping defines field names, types, and how text is processed. Getting mappings right at index creation matters — changing analyzers on a live field usually requires reindexing.
Core field types
text— full-text search; analyzed (tokenized) at index and query time unless you specify otherwise.keyword— exact-match strings for filters, sorting, and aggregations (SKU codes, status enums).date,long,double,boolean— structured filters and range queries.nestedandobject— arrays of objects; usenestedwhen child objects must be queried as independent units.
Analyzers
An analyzer chains a character filter (optional),
tokenizer (splits text into tokens), and token filters
(lowercase, stop words, stemming). Index-time and search-time analyzers can differ
— a common pattern is the standard analyzer at index time and
a search_analyzer with synonym expansion at query time.
Use multi-fields when one logical field needs both behaviors:
title as text for search and title.keyword
for sorting. Synonym files and ICU tokenizers fix domain vocabulary (“laptop”
= “notebook”) but increase index size and reindex cost when updated.
Query DSL: bool queries, filters and BM25
Elasticsearch queries are JSON. The bool query combines clauses:
must— required; contributes to score.filter— required; no score impact (cached bitsets).should— optional; boosts matching documents.must_not— exclusion filter.
Put structured predicates (category, price range, in-stock flag) in
filter clauses for speed. Put natural-language terms in
must or should inside a multi_match query
across title^3, description, and brand with
field boosts.
BM25 relevance
Default scoring uses BM25 (Best Matching 25): term frequency
saturates (repeating a word helps less after a point), rare terms score higher
than common ones (inverse document frequency), and shorter fields rank terms more
strongly. Tune with similarity settings or replace with a
script_score for business rules (boost in-stock, penalize low margin).
For typo tolerance, add a match query with fuzziness: AUTO
or a parallel match_phrase_prefix on autocomplete fields. For
semantic similarity beyond keywords, index dense vectors and combine with BM25 in a
hybrid retrieval
pipeline rather than forcing keyword queries to guess intent.
Aggregations, facets and analytics
Aggregations compute analytics without returning hit documents
— category counts, price histograms, date histograms for time series.
Bucket aggregations group documents; metric
aggregations compute stats (avg, percentiles,
cardinality) inside buckets.
Faceted navigation on e-commerce sites is typically a terms
aggregation on category.keyword with a filter context
mirroring the user’s active filters. Log analytics stacks (ELK) lean on
date_histogram aggregations per minute alongside
structured logs
ingested via bulk API.
Aggregations are memory-heavy on high-cardinality fields. Use
composite aggregations for deep pagination through bucket keys and
set size limits consciously — requesting ten thousand unique
user IDs per query can OOM a data node.
Sharding, replicas and cluster topology
Each index splits into primary shards (write path) and optional replica shards (read scaling and failover). Shard count is fixed at index creation (unless you use index rollover / shrink APIs with constraints). Too many shards waste heap; too few limit write throughput and shard size.
Sizing heuristics
- Target shard sizes roughly 10–50 GB for time-series; larger for static catalogs if heap allows.
- Keep JVM heap at or below 50% of RAM and below ~32 GB (compressed OOPs threshold).
- One replica minimum for production failover; two if you serve heavy read traffic.
- Separate hot (SSD, recent data) and warm tiers for logs via index lifecycle management (ILM).
Document routing defaults to hash(_id) % num_primary_shards. Custom
_routing co-locates related documents (all orders for one customer)
but can create hot shards if routing keys skew. Cross-shard queries merge results
on the coordinating node — very wide fan-out queries are latency-sensitive.
Indexing: bulk API, refresh and ingest pipelines
Single-document index requests are fine for low volume. Production
backfills use the bulk API: newline-delimited action/metadata
lines followed by source JSON. Batch 1,000–5,000 docs per request; tune
until you saturate disk without triggering rejections.
Refresh and consistency
New documents are searchable after a refresh (default every 1s),
which opens a new Lucene segment. Near-real-time search tolerates this; bulk ETL
jobs should set refresh_interval: -1 during load and restore after.
Use wait_for_active_shards when you need acknowledgment that replicas
indexed the write before returning to clients.
Ingest pipelines
Ingest pipelines transform documents on the coordinating node
before indexing: grok parsing for logs, geoip enrichment, renaming fields, dropping
PII. Pipelines decouple producers from mapping details — application code
ships raw events; Elasticsearch normalizes them. For change-data-capture from
Postgres, tools like Logstash JDBC input or Debezium + Kafka sink reindex on
every row update; design idempotent document IDs (product-{sku}).
Worked example: Harbor Market product search
Harbor Market is a mid-size outdoor gear storefront. Catalog data lives in Postgres (orders, inventory). Search must handle 80k SKUs, faceted filters (category, brand, price), typo-tolerant title search, and boost in-stock items.
Mapping sketch
{
"mappings": {
"properties": {
"sku": { "type": "keyword" },
"title": {
"type": "text",
"fields": { "keyword": { "type": "keyword" } }
},
"description": { "type": "text" },
"brand": { "type": "keyword" },
"category": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"in_stock": { "type": "boolean" },
"updated_at": { "type": "date" }
}
}
}
Search request
POST /products/_search
{
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "waterproof hiking boots",
"fields": ["title^3", "description", "brand^2"],
"fuzziness": "AUTO"
}
}],
"filter": [
{ "term": { "category": "footwear" } },
{ "range": { "price": { "lte": 250 } } },
{ "term": { "in_stock": true } }
]
}
},
"aggs": {
"brands": { "terms": { "field": "brand", "size": 20 } }
},
"from": 0,
"size": 24
}
Nightly CDC jobs bulk-index changed rows; a webhook on inventory updates issues
single-document upserts by SKU. Category pages use
cursor pagination
on search_after with sort keys [_score, sku] for stable
deep pages. Postgres remains source of truth for checkout; Elasticsearch serves
read-only discovery.
Elasticsearch vs alternatives
| Need | Elasticsearch | Alternative |
|---|---|---|
| Full-text search with ranking and facets | Strong fit | PostgreSQL tsvector (simpler, same-node OLTP) |
| Log/metrics time series at cluster scale | Elastic Stack, ILM tiers | ClickHouse, Loki, OpenSearch fork |
| Transactional CRUD and joins | Wrong tool | PostgreSQL, MySQL |
| Flexible schema documents, app JSON | Possible but secondary | MongoDB with Atlas Search |
| Managed vector + keyword hybrid | Dense vector fields + RRF | Pinecone, pgvector with extensions |
| Sub-millisecond key cache | Not designed for this | Redis |
Common pitfalls
- Dynamic mapping surprises — Elasticsearch guesses
textwhere you neededkeyword; disable dynamic mapping or use templates in production. - Too many shards — hundreds of tiny shards exhaust heap and slow cluster state; consolidate via rollover policies.
- Heavy aggregations on high-cardinality fields — unique user IDs as terms agg buckets can crash nodes.
- Scoring inside filter context — filters do not affect relevance; business boosts belong in
shouldorfunction_score. - Ignoring reindex strategy — mapping changes require reindex; plan blue/green index aliases (
products_v2+ alias swap). - Treating ES as primary store — replica loss and split-brain incidents happen; keep authoritative data in OLTP.
- Unbounded
from+sizepagination — deep offset pagination is expensive; usesearch_afteror scroll only for batch export.
Production checklist
- Define explicit index templates and mappings before first production document.
- Set shard count from expected data volume; use ILM for time-series indices.
- Monitor heap usage, GC pauses, thread pool rejections, and disk watermarks.
- Snapshot to S3 (or equivalent) on a schedule; test restore quarterly.
- Use index aliases for zero-downtime reindex and versioned mapping changes.
- Keep bulk batch sizes and refresh intervals tuned for your ingest SLA.
- Secure clusters with TLS, role-based access, and network isolation.
- Log slow queries; capture query DSL in APM for relevance debugging.
- Cap aggregation
sizeand usecompositefor deep bucket paging. - Document synonym and analyzer updates with a rehearsed reindex runbook.
Key takeaways
- Elasticsearch is a distributed Lucene layer for ranked full-text search and analytics, not OLTP.
- Mappings and analyzers determine search quality; plan them before indexing at scale.
- Bool Query DSL separates filters (fast, exact) from scored text clauses (BM25).
- Shard and replica topology trades write parallelism, failover, and query fan-out.
- Bulk indexing, ILM, and aliases are the operational backbone of production search.
Related reading
- Hybrid search explained — combining BM25 keyword retrieval with dense embeddings
- PostgreSQL fundamentals explained — OLTP source of truth beside search indices
- Structured logging explained — JSON logs that feed observability pipelines
- API pagination explained — cursor and keyset patterns for search result pages