Elasticsearch
A distributed search engine for full-text relevance, filters, facets and log analytics when a database scan or simple index is not enough.
Also worth naming: OpenSearch (Apache-2 fork from Elasticsearch 7.10.2) · Elastic Cloud · Amazon OpenSearch Service · the ELK / Elastic Stack
Elasticsearch gets you ready to defend search as a derived index, not a table scan. You’ll practise the mapping, relevance and shard choices that interviewers ask about after you say “use search”.
What it is
Elasticsearch is a distributed, document-oriented search engine built on Apache Lucene. Its job is the thing relational databases are bad at: full-text search — find the documents most relevant to a set of words, ranked by relevance, fast, across millions or billions of documents. It also does faceted filtering (filter by category, price, date and show counts), aggregations (analytics over the matched set), and geo queries, which is why it underpins both product search and log/observability stacks.
The engine at the core is the inverted index: instead of scanning each document for a word, Elasticsearch precomputes a map from every term to the list of documents containing it. A search becomes "look up a few short posting lists and intersect them," which is independent of corpus size — the opposite of WHERE body LIKE '%term%', which scans the whole table and dies at scale.
The critical framing for interviews: Elasticsearch is a derived index, not a database of record. You keep the authoritative data in Postgres/DynamoDB/etc. and project it into Elasticsearch (via CDC or an outbox), because the index can be lost, corrupted, or need a full rebuild when you change how text is analysed. Reach for it when search, faceting, or log analytics are first-class requirements; do not reach for it as your primary store.
As of 2026, name the fork and licence story precisely. Elasticsearch 9.x is Elastic's current major line; Elastic moved Elasticsearch/Kibana source from Apache 2.0 to Elastic License/SSPL before 7.11 in 2021, then added AGPLv3 as another option in 2024. OpenSearch 3.x is the current Apache-2 fork line from Elasticsearch/Kibana 7.10.2, and the OpenSearch Software Foundation has lived under the Linux Foundation since September 2024. In AWS or strict OSS procurement contexts, say OpenSearch when that is what you mean.
When to reach for it
Reach for this when…
- Full-text search is a real feature — relevance ranking, typo tolerance, autocomplete
- Faceted filtering and aggregations over a large result set (e-commerce, catalogs)
- Log, metrics, and observability analytics at scale (the ELK stack)
- You have outgrown Postgres full-text search (roughly past ~10M docs or heavy query volume)
Not really this pattern when…
- You only need exact-match lookups or simple filters — your primary DB already does that
- Search is light and the data is small — Postgres full-text (tsvector + GIN) avoids a new system
- You need it as a system of record with transactions (it is a derived index, not a database)
- Strong write-read consistency on the search path (indexing is near-real-time, not instant)
How it works
Four ideas explain how to use Elasticsearch well:
1. The inverted index is why search is fast. Lucene tokenises each document into terms and stores, per term, a posting list of the documents containing it. Querying intersects/unions a few short lists rather than scanning the corpus — so search time scales with the number of matching terms, not the number of documents.
Instead of scanning every document for a word, Elasticsearch keeps a map from each term to a posting list of document ids. A search for "quick fox" looks up two short lists and intersects them — fast regardless of corpus size.
2. Analyzers decide what "matches". Before indexing, text runs through an analyzer: tokenise → lowercase → remove stop words → stem ("running" → "run") → language-specific handling. The query is analysed the same way so terms line up. Choosing analyzers (and re-indexing when you change them) is most of the work of getting search quality right — and is why a field is also stored as a non-analysed keyword for exact-match filtering and sorting.
3. Relevance is BM25, then re-ranking. Matches are scored by BM25 (term frequency, inverse document frequency, length normalisation) so the most relevant documents sort first. Real products then layer business signals on top — popularity, recency, personalisation — via function_score, never throwing away the text relevance.
4. It scales by sharding, and queries scatter-gather. An index is split into shards (each a self-contained Lucene index), with replicas for read throughput and failover. A search fans out to every shard, each returns its local top-K, and a coordinating node merges them into the final ranking.
Documents are routed to a primary shard by hashing the document id. Each primary has replica shards for read scale and failover. A query scatters to every shard, each returns its top hits, and the coordinator gathers and merges them.
And the operational truth: the primary database is the source of truth; Elasticsearch is kept in sync via CDC and is rebuildable from the database.
Elasticsearch is not a system of record. Writes go to the primary database; a change-data-capture stream projects them into the index. If the index is lost or its mapping changes, you rebuild it from the database.
Performance envelope
Elasticsearch performance envelope — order-of-magnitude anchors; real numbers depend on hardware, shard shape, query mix, mappings, payload size, and cache hit rate.
| Dimension | Number | Why it matters |
|---|---|---|
| Search latency | Tens of ms over millions of docs | Inverted index, not a scan — the whole point |
| Scale | Billions of docs across sharded nodes | Add shards/nodes; queries scatter-gather |
| Indexing freshness | Near-real-time (~1s refresh) | Not instant — a write is searchable after refresh |
| Throughput | Workload-dependent; simple cached queries can be very high, heavy facets/vector/aggregations much lower | Benchmark your query mix; do not memorise a per-node QPS promise |
| Relevance | BM25 default + function_score re-rank | Tune ranking without replacing text scoring |
| Durability | Derived index — rebuild from source DB | Never the only copy of the data |
Capabilities in interviews
Full-text search & relevance
Rank documents by how well they match a query, with typo tolerance and field boosting.
The core use. Analyse the text, search the inverted index, rank by BM25, boost important fields:
{ "multi_match": {
"query": "wireless headphones",
"fields": ["name^3", "description"],
"fuzziness": "AUTO" // typo tolerance
}}name^3 weights title matches 3× over description; fuzziness tolerates misspellings via edit distance. This is product search, document search, anything where "most relevant first" beats exact match — and where a SQL LIKE would both miss relevance and scan the table.
Choose this variant when
- Product / catalog / document search
- Relevance ranking matters, not just matching
- Typo tolerance and synonyms are expected
Faceted filtering & aggregations
Filter the result set by attributes and return live counts and analytics in one query.
The e-commerce sidebar — "Brand (Sony 42, Bose 18), Price, Rating" — is an aggregation over the matched documents:
{ "query": { ... },
"aggs": { "by_brand": { "terms": { "field": "brand" } } } }Filters (must-clauses) restrict the set without affecting relevance score; aggregations compute counts and stats over what matched. Doing this in SQL means many slow GROUP BY queries; Elasticsearch returns search results and all facet counts in a single round trip.
Choose this variant when
- E-commerce / catalog filtering with counts
- Dashboards over a searchable dataset
- Drill-down analytics on matched results
Log & observability analytics
Ingest huge volumes of logs and metrics and query them interactively (the ELK stack).
Elasticsearch + Logstash/Beats + Kibana is the canonical observability stack. Ship logs in, index by time, and slice them interactively in Kibana — error rates by service, latency percentiles, full-text grep across billions of lines:
app logs → Beats → Elasticsearch (time-based indices) → Kibana dashboardsUse time-based indices (logs-2026-06-06) with an ILM policy so old data rolls to cheaper storage and eventually deletes. This is a different shape from product search but the same engine — high-volume ingest, ad-hoc query.
Choose this variant when
- Centralised logging and metrics
- Interactive debugging across services
- Security / audit log analytics
Geo & vector search
Filter by distance, and increasingly, search by semantic similarity (kNN vectors).
Elasticsearch supports geo_point/geo_distance for "within 5 km of here" filters combined with text relevance. Modern Elasticsearch and OpenSearch are also serious BM25 + vector engines: dense_vector / knn_vector, HNSW approximate kNN, quantization, semantic search helpers, and hybrid ranking with score normalisation or Reciprocal Rank Fusion (RRF). Elasticsearch-style kNN looks like:
{ "knn": { "field": "embedding", "query_vector": [...], "k": 10 } }The choice rule: use Elasticsearch/OpenSearch when keyword relevance, filters, facets, logs, or existing search operations are already central and vectors are another retrieval signal. Use pgvector when the data is Postgres-native, transactional consistency matters, and QPS/scale are modest. Use a dedicated vector database when vectors are the product, embedding count/QPS is very high, or you need specialised ANN operations independent of a search cluster.
Choose this variant when
- Location-filtered search (stores, listings near me)
- Hybrid keyword + semantic retrieval
- RAG retrieval where you already run Elasticsearch
Operating knobs
Analyzer & mapping design
The biggest quality lever. Pick analyzers per field (language stemming, edge n-grams for autocomplete), and store a keyword sub-field for exact filtering/sorting. Define the mapping explicitly rather than relying on dynamic mapping. Changing an analyzer means re-indexing — so design it deliberately up front.
Shard count & sizing
Too few shards caps parallelism; too many wastes overhead (each is a Lucene index with fixed cost) and slows scatter-gather. Aim for shards in the tens-of-GB range and size to your data and query concurrency. Over-sharding a small index is a classic mistake; you cannot change a shard count without re-indexing.
Relevance tuning (function_score)
Start with BM25, then multiply by business signals: score × log1p(popularity) for popular-first, a recency decay for fresh-first. Wrap in function_score so you boost rather than replace text relevance, and validate changes with A/B tests on click-through and conversion — ranking is an empirical loop.
Index lifecycle & refresh
For logs, use time-based indices + an ILM policy (hot → warm → cold → delete) to control cost. The refresh interval controls how quickly new documents become searchable (default ~1s); raise it during bulk indexing for throughput, lower it where freshness matters.
Elasticsearch vs OpenSearch
In interviews, be precise. Elasticsearch means Elastic's distribution and licence choices; OpenSearch means the Apache-2 fork, Amazon OpenSearch Service lineage, and Linux Foundation governance. Product-search designs can name either, but OSS/procurement/AWS questions often expect you to call out OpenSearch explicitly instead of saying "Elasticsearch" generically.
Hybrid keyword + vector strategy
BM25 and vector scores are not naturally comparable. Modern Elasticsearch/OpenSearch can run both and fuse them with RRF, normalization pipelines, rescoring, or application-side ranking. Keep the first design simple: retrieve candidates by text + filters + vector, fuse/rerank, then evaluate with real queries. If all you need is modest semantic search inside Postgres, pgvector is often less infrastructure.
Versus the alternatives
Elasticsearch vs the alternatives.
| Dimension | Elasticsearch / OpenSearch | Postgres full-text / pgvector | A dedicated vector DB |
|---|---|---|---|
| Scale | Billions of docs, sharded | Great for modest in-DB search; benchmark before high QPS | Billions of embeddings |
| Strength | Relevance, facets, aggregations, logs, hybrid BM25+vector | Transactional simplicity; pgvector for Postgres-native embeddings | Semantic similarity (ANN) as the core product |
| Ops cost | A cluster to run (JVM, shards) | Just a Postgres feature | Another specialised system |
| Source of truth? | No — derived, rebuildable | Yes (it is your DB) | No — derived |
| Best for | Search + faceting + log analytics + hybrid retrieval at scale | Modest search/vector needs without new infra | Pure semantic / RAG retrieval at very large scale |
Failure modes & gotchas
Elasticsearch can lose data on misconfiguration, and mapping/analyzer changes require a full rebuild. Keep the authoritative copy in a real database and treat the index as a derived, rebuildable view fed by CDC — never the only home for the data.
Indexing into Elasticsearch inside the user write coupling it to search-cluster latency and availability. Index asynchronously via CDC or a queue and accept ~1s of search lag; the write path should not block on the search cluster.
Each shard is a Lucene index with fixed memory and file-handle overhead; hundreds of tiny shards waste resources and slow every scatter-gather query. Size shards to tens of GB and keep the count proportional to data and concurrency — and remember you must re-index to change it.
Jumping to page 1,000 with from/size forces every shard to collect and sort that many results — O(from) work that blows up memory. Use search_after (cursor) for deep pages and reserve from/size for the first handful of pages.
Aggregating over a field with millions of distinct values (e.g. user id) is memory-hungry and can OOM a node. Bound cardinality, use approximate aggregations where acceptable, and push truly heavy analytics to a warehouse instead of the live search cluster.
In production
Uber
OpenSearch in consumer search products
Uber's December 2025 post says it adopted Amazon OpenSearch as a vector search engine after Lucene/HNSW prototypes ran into tuning and GPU-support limits. The case study is a large prototype over 1.5 billion items, each embedded as a vector with nearly 400 dimensions, indexed in batches through Spark and OpenSearch bulk indexing.
The concrete design lesson is operational tuning, not a magic search box. Uber reports cutting base-index ingestion from 12.5 hours to 2.5 hours by tuning bulk indexing, CPU, memory, Spark, and OpenSearch configuration, reducing index size from 11 TB to 4 TB by disabling _source and doc_values, and lowering p99 latency from 250 ms at 2K QPS to under 120 ms. Search clusters still need blue/green index builds, shard and replica tuning, memory sizing for the kNN graph, and an honest latency/recall trade.
Netflix
Elastic{ON} 2015 — Elasticsearch for operational insight
Netflix is a public, dated example of Elasticsearch used for operational insight. In an Elastic{ON} 2015 talk, Netflix presenters described 700–800 production nodes across about 100 Elasticsearch clusters and an internally built sidecar, Raigad, for managing that fleet. That is an old number, but it is sourced and useful as a scale signal.
The architecture choices are textbook log analytics: time-based indices or data streams, lifecycle policies to roll over and delete old data, careful shard sizing, and capacity sized for ingest throughput plus query concurrency. The durable lesson is not "Netflix has N nodes"; it is that the same inverted-index engine can power interactive observability when lifecycle and shard operations are designed deliberately.
Good vs bad answer
Interviewer probe
“An e-commerce site needs product search over 50M items with typo tolerance and brand/price filters. How do you build it?”
Weak answer
"Store products in Postgres and run SELECT * FROM products WHERE name LIKE '%query%', plus WHERE brand = ? for the filters. Add an index on name to speed it up."
Strong answer
"Elasticsearch — LIKE '%query%' is a full-table scan that dies well before 50M rows, and a B-tree index can't do relevance, typos, or facet counts. Postgres stays the source of truth; I project products into Elasticsearch via CDC so the index is rebuildable. The mapping analyses name and description (English stemming) for relevance, with brand, category, price as keyword/numeric for exact filtering and sorting. Search is a multi_match over name^3 and description with fuzziness: AUTO for typos; brand/price filters are must-clauses that restrict without affecting score; and the sidebar counts come from a terms aggregation — search results and all facet counts in one round trip. Base ranking is BM25, then function_score boosts by popularity and recency so good-but-boring matches don't outrank what people actually buy. For reindexing after analyzer changes I build into a new index and flip an alias atomically. LIKE gives me none of this."
Why it wins: Rejects LIKE with the scan reasoning, keeps the DB as source of truth with CDC, designs the mapping (analysed vs keyword fields), uses fuzziness + facet aggregations + BM25/function_score ranking, and plans alias-based reindexing — a complete, scale-aware search design.
Interview playbook
When it comes up
- A feature needs full-text search, autocomplete, or relevance ranking
- Faceted filtering with live counts (e-commerce, catalogs)
- Centralised logging / observability / security analytics
- Someone proposes a LIKE query on the primary database
Order of reveal
- 11. Reject scan-based search. LIKE is a full scan with no relevance; real search means an inverted index, so I name Elasticsearch (or Postgres FTS if small).
- 22. Keep the DB as truth. The primary DB is the source of truth; I project into Elasticsearch via CDC so the index is rebuildable.
- 33. Design the mapping. Analysed text fields for relevance, keyword fields for exact filter/sort; analyzers chosen per language.
- 44. Relevance. BM25 base score, then function_score to boost by popularity and recency.
- 55. Reindex safely. Build into a new index and flip an alias atomically when the mapping changes, keeping the old one for rollback.
Signature phrases
- “Search is an inverted-index lookup, not a table scan.” — The line that justifies a search engine existing.
- “The DB is the source of truth; the index is a derived, rebuildable view.” — The single most important operational fact.
- “Analysed fields for relevance, keyword fields for exact filtering.” — Shows real mapping design, not hand-waving.
- “BM25 first, then boost by popularity and recency.” — Separates textual match from business ranking.
Likely follow-ups
?“How do you keep the index in sync with the database?”Reveal
Asynchronously, via change-data-capture. Every committed change in the source DB (Debezium tailing the WAL, or an outbox topic) becomes an event that an indexer consumes and upserts into Elasticsearch. The write path never blocks on the search cluster, and I accept ~1 second of indexing lag (near-real-time refresh). Because the index is derived, if a consumer bug corrupts it I fix the code and replay from the stream, or bulk-rebuild from the database — the authoritative data is never at risk.
?“Is Postgres full-text search ever enough instead?”Reveal
Yes — up to roughly 10M documents with modest query volume and no heavy faceting, a tsvector column with a GIN index and ts_rank is genuinely sufficient, and it keeps search transactionally consistent with the data while avoiding a whole separate cluster to operate. I graduate to Elasticsearch when scale, faceted aggregations, typo tolerance, or fine relevance tuning exceed what FTS offers. Reaching for an Elasticsearch cluster on a 100k-row catalog is over-engineering.
?“A user reports their just-created product is not searchable yet. Why?”Reveal
Indexing is near-real-time, not instant. A newly indexed document becomes searchable only after the next refresh (default ~1s), and if I index asynchronously via CDC there is additional pipeline lag of perhaps a second or two. That is almost always an acceptable trade for not coupling writes to the search cluster. If a specific flow truly needs read-your-write search immediately, I can force a refresh for that document or serve that one view from the source DB — but I would not lower the global refresh interval, which hurts indexing throughput.
Worked example
Setup. Add product search to a marketplace with 50M listings: typo-tolerant full-text search over titles and descriptions, brand/price/category filters with live counts in the sidebar, and results ranked so popular items surface — at thousands of queries/sec.
The move. Postgres stays the source of truth; I project listings into Elasticsearch via CDC so the index is a derived, rebuildable view (a Debezium stream off the products table → an indexer that upserts documents). The mapping is the real design work: title and description are analyzed text (English analyzer, stemming) for relevance; brand, category are keyword for exact filtering and facet counts; price, rating, sales_30d are numeric for filtering and boosting.
Query shape. A search is a multi_match over title^3 and description with fuzziness: AUTO for typos. Brand/price filters are must-clauses that restrict the set without touching the score; the sidebar counts come from a terms aggregation in the same request — so one round trip returns results and all facet counts. Base relevance is BM25, then function_score multiplies by log1p(sales_30d) and a recency decay so good-but-boring matches don't outrank what people actually buy.
Scale. I shard the index sized to ~tens of GB per shard with replicas for read throughput and HA; queries scatter to every shard and the coordinator merges the top-k. At 50M docs this is a modest cluster.
What breaks. Two things. Deep pagination — jumping to page 1,000 with from/size forces every shard to collect and sort that many hits, so I use search_after (cursor) for deep pages. And reindexing when I change an analyzer: I build into products-v2, bulk-reindex from the source, then flip an alias atomically so traffic cuts over with zero downtime and I keep v1 for one-command rollback. I never put indexing on the synchronous write path — it's async via CDC, ~1s lag.
The result. Typo-tolerant relevance over 50M listings in tens of ms, search results and all facet counts in one query, business-aware ranking, and a rebuildable index that stays in sync with Postgres — none of which a SQL LIKE could deliver.
Cheat sheet
- •Distributed search engine on Lucene: inverted index → fast full-text search with BM25 relevance.
- •Derived index, NOT a system of record. Keep truth in the DB; sync via CDC; rebuildable.
- •Analyzers (tokenise/lowercase/stem) decide matches; keyword sub-fields for exact filter/sort.
- •Scales by sharding; queries scatter-gather across shards; replicas for read scale + HA.
- •Facets/aggregations return filtered counts in one query — the e-commerce sidebar.
- •Relevance: BM25 base, function_score to boost popularity/recency. Tune via A/B tests.
- •Indexing is near-real-time (~1s refresh), async off the write path.
- •Reindex behind an alias (atomic flip + rollback). Use search_after, not deep from/size.
Drills
Why is an inverted index faster than a SQL LIKE for search?Reveal
LIKE '%term%' cannot use a normal B-tree index (the leading wildcard defeats it), so it scans every row and tests the predicate — O(rows). An inverted index precomputes, for each term, the list of documents containing it, so a search looks up a few short posting lists and intersects them — work proportional to the matching terms and result size, not the corpus. It also enables relevance ranking, stemming, and typo tolerance, none of which LIKE can express.
Interviewer: "results are textually correct but popular items rank low. Fix it."Reveal
Layer business signals on top of BM25 with function_score rather than replacing text relevance: multiply the base score by a popularity factor like log1p(sales_30d) and apply a recency decay so fresh, popular items rise. Keep the text match as the foundation so irrelevant-but-popular items do not flood the top. Then validate with A/B tests on click-through and conversion — relevance tuning is an empirical loop, not a one-shot formula.
You changed an analyzer and existing documents no longer match correctly. What is the process?Reveal
Analyzer changes require re-indexing, because documents were tokenised under the old rules. Create a new index with the updated mapping/analyzer, bulk-reindex from the source database (or the old index via the reindex API), verify results, then flip the index alias from old to new atomically so live traffic cuts over with zero downtime — keeping the old index briefly for one-command rollback. This alias pattern is why reindexing is routine rather than scary.
What it is