Elastic's own engineering guidance puts a healthy production shard somewhere between 20GB and 40GB, with 50GB quoted as the rough ceiling where things still hold up under real load (Elastic Blog). I've read a lot of candidate answers to shard-sizing questions, and the failures are rarely close. Clusters running 2GB shards by the thousand. One 400GB shard nobody ever got around to splitting. Elasticsearch interview questions test for that kind of precision on purpose, because vague intuition about shard size is exactly what breaks a real cluster at 3am.
Most prep guides spend their word count on aggregations and vector search, because those topics feel current and interesting to write about. My take, and I'll admit it's a little contrarian: the inverted index and the analyzer chain matter more in an actual interview, because half of the "why isn't my search returning the right results" bugs anyone will ever debug trace straight back to what the analyzer did to text before it hit the index. You can ship a working product search without ever touching a vector embedding. You can't debug a broken one without understanding tokenization.
This page covers Elasticsearch interview questions across eight areas: the inverted index and what actually sits underneath Elasticsearch, documents and mappings, analyzers and tokenizers, the Query DSL (match, term, bool, and range), the split between full-text and exact-value fields, aggregations, relevance scoring through BM25, and the operational trio of shards, near-real-time search, and cluster health. Query examples use the Query DSL's native JSON, the same syntax you'd send through curl, Kibana Dev Tools, or any of the official clients.
What Elasticsearch actually is, and why the inverted index runs everything under it
This section sounds basic. It's also where a surprising number of candidates with years of hands-on Elasticsearch experience go soft, because they've used it without ever needing to explain it.
Easy questions
15Elasticsearch is a distributed, JSON-based search and analytics engine built on top of Apache Lucene, a single-machine Java indexing library. Lucene handles the actual inverted index and scoring math; Elasticsearch adds the distributed layer on top, sharding, replication, a REST API, and aggregations, none of which Lucene provides on its own. Shay Banon wrote the first version in 2010, after concluding that Compass, an earlier Java search library he'd built starting from a recipe-search app for his wife, needed a full rewrite to work as a distributed system instead of a single-node one.
An inverted index maps every unique term to the list of documents (and positions within them) that contain it, the reverse of the normal document-to-word direction, which is where the name comes from. Instead of scanning every document for a word when a query comes in, Elasticsearch looks the word up once and gets back exactly which documents have it. It's the same trick as the index at the back of a textbook: you don't reread chapters 1 through 12 looking for "mitochondria," you check the index and go straight to page 214.
A document is a single JSON object, the basic unit Elasticsearch indexes and returns. An index is a collection of documents, roughly comparable to a table in a relational database, though Elastic has been careful to walk that comparison back since it removed the older "type" concept. A mapping is the schema for an index: it defines each field's type (text, keyword, date, long, and so on) and, indirectly, how that field gets analyzed and indexed.
An analyzer is a small pipeline. Optional character filters run first, stripping HTML tags or swapping characters. Exactly one tokenizer then splits the string into individual tokens, the standard tokenizer splits on Unicode word boundaries. Last, a chain of token filters normalizes each token: lowercasing, dropping stopwords, stemming "running" down to "run." The standard analyzer, the default when a field doesn't specify one, lowercases and splits on word boundaries but applies no stemming and, somewhat surprisingly, no stopword removal out of the box.
match analyzes the query text through the field's analyzer and matches any of the resulting tokens by default, an implicit OR, which is what you want for free-text search on a text field. term looks for the exact, unanalyzed value in the inverted index, no normalization at all, which is what you want for keyword, numeric, date, or boolean fields where the stored value and the search value need to match byte for byte.
keyword whenever you need exact match, sorting, or aggregating on the whole unanalyzed value: status codes, tags, SKUs, an email address treated as one unit. text whenever you need to search inside free-form content: descriptions, reviews, chat messages. Since Elasticsearch 5, dynamic mapping actually hedges this bet for you by default. String fields get mapped as text with an automatic.keyword sub-field, so you usually get both without deciding upfront. That's a quiet admission from Elastic that most string fields want both behaviors, not just one.
Metric aggregations produce a single numeric summary across a set of documents: avg, sum, min, max, cardinality. Bucket aggregations group documents into buckets by some criterion, terms, date_histogram, range, and let you nest further aggregations inside each bucket. Pipeline aggregations run on the output of other aggregations rather than on raw documents: a derivative, a moving average, a bucket_script doing arithmetic across sibling buckets. Bucket aggregations are the ones interviewers push hardest on, because nesting them is where the mental model actually gets tested.
A primary shard holds the canonical copy of a portion of an index's data; writes go to the primary first. A replica is a copy of a primary kept on a different node, for redundancy if a node goes down, and for spreading out search load, since replicas can serve reads too. Primary shard count is effectively fixed at index creation, because a document's shard is chosen by hashing its routing value against that count, and changing the count breaks that hash-to-shard mapping (the shrink and split APIs work around this by creating a new index with a different primary count rather than resizing shards in place). Replica count can be changed anytime, since adding or removing a replica is just copying, no rehashing involved.
Green means every primary and every replica shard is allocated. Yellow means all primaries are allocated but at least one replica isn't, a single-node cluster with a replica count of 1 is permanently yellow, which is expected, not an emergency. Red means at least one primary shard is unassigned, which means some slice of the cluster's data is currently unsearchable, and possibly unwritable. Red is the one worth paging someone for. Yellow on a cluster that's supposed to have enough nodes to go green deserves a look, but it isn't the same emergency.
A node is a single running instance of Elasticsearch, one JVM process with its own config, memory, and disk. A cluster is a named group of nodes working together, sharing cluster state and splitting up the actual work of storing data and running queries. You can run a cluster on one node for local development, but production clusters are usually three or more.
Nodes take on roles that determine what work they're responsible for. Master-eligible nodes participate in electing a cluster master and hold the cluster state, index mappings, shard locations, node membership. Data nodes actually store shards and do the heavy lifting for indexing and search. Ingest nodes run ingest pipelines before documents are indexed. Coordinating nodes scatter a search request out to the relevant data nodes and gather the results back into one response. In small clusters a single node often does all of these jobs at once; in larger production setups you split them out so a spike in search traffic doesn't compete with master bookkeeping for CPU.
Each individual index request carries its own HTTP overhead, its own trip through the coordinating node, and its own accounting in the cluster. If you're loading a million documents one request at a time, you pay that overhead a million times. The bulk API lets you batch many index, update, or delete operations into a single HTTP request using a newline-delimited JSON format, so the network round trip and connection overhead get amortized across the whole batch.
There's a practical sweet spot. Batches that are too small don't gain much over single requests; batches that are too large risk hitting the http.max_content_length limit or putting memory pressure on the node handling the bulk call. Most teams land somewhere between 5 and 15 megabytes per bulk request, and tune from there based on how the cluster responds. It's also worth checking the response body of a bulk call, a 200 status doesn't mean every item succeeded, individual items in the array can fail while the overall request still returns OK.
An alias is a name that points to one or more real indices. Your application queries and writes to the alias, never the underlying index name, and Elasticsearch resolves it beneath that. Aliases can also carry a filter, so two teams can share physical data but only ever see their own slice of it through different aliases.
The real payoff shows up when you need to reindex. Mappings for a field can't be changed after the fact, only reindexing into a new index gets you there. If your app is hardcoded to a raw index name, you can't swap in a new index without a code deploy and downtime. If your app talks to an alias instead, you build the new index in the background, flip the alias to point at it in one atomic call, and the application never notices the switch happened.
from and size work fine for the first few pages because Elasticsearch just skips that many results and returns the next batch. The catch is that at the implementation level, each shard has to build its own from-plus-size sorted list of matches and ship all of it to the coordinating node, which then merges the shard results and throws away everything before the requested offset. Ask for page 10,000 and every shard is doing the work of sorting and returning thousands of documents, most of which get discarded.
Elasticsearch caps this by default at 10,000 total results, and raising that limit just moves the cost around rather than removing it. For genuinely deep pagination, or for exporting an entire result set, the better tools are search_after, paging through with a cursor based on sort values, or the scroll and point-in-time APIs, both of which avoid re-sorting the whole result set on every page.
When you map an array of objects as plain object type, Elasticsearch flattens it internally. Each sub-field becomes its own array of values with no memory of which values belonged together in the same original object. So if you have a reviews array with author and rating pairs, a query for one author and a different review's rating will still match, even though no single review actually has that combination.
Nested type stores each object in the array as a separate hidden Lucene document, internally linked to the parent, so a nested query can require that author and rating come from the same array element. That correctness costs something: nested documents need a nested query or nested aggregation to search, they can't be queried with a plain match, and having many nested objects per document multiplies the actual document count Lucene has to manage, which affects memory and merge behavior. Use nested only when you actually need object-level correctness across the array; if you just need to filter on one field independently, object type is cheaper.
Every document needs a unique _id within its index, it's how Elasticsearch looks up, updates, and deletes a specific document, and it's part of what determines which shard the document routes to by default. If you don't supply one on an index request, Elasticsearch auto-generates one, a URL-safe, roughly time-ordered random string.
Whether to set your own depends on the source of truth for your data. If your data already has a natural unique key, a user id, an order number, a SKU, using that as _id makes indexing idempotent. Reindexing the same source data twice overwrites the same document instead of creating a duplicate, which is exactly what you want during backfills or retries. Auto-generated IDs are fine when the data has no natural key and you're purely appending, but they make replays trickier because reprocessing an event twice creates two documents instead of updating one.
Medium questions
25It undersells what's different, and the difference has real consequences. Elasticsearch doesn't support multi-document transactions the way a relational database does. A write to one document is atomic on its own, backed by optimistic concurrency through a sequence number and primary term, but there's no ACID transaction wrapping updates across several documents at once. Search visibility is near-real-time, not immediate, which a typical request-response database generally isn't. Teams that treat Elasticsearch as their single source of truth, instead of a search and analytics layer sitting next to one, are usually the teams that eventually get burned by one of those two gaps.
Types were originally modeled after tables inside one relational database, implying real separation between, say, a "user" type and an "order" type living in the same index. Underneath, Lucene has no concept of types at all: every field across every type in an index gets merged into one shared mapping at the Lucene level. A field named status mapped as text under one type and as an object under another, in the same index, simply doesn't work, because Lucene sees one flat set of fields, not two isolated ones. That mismatch caused enough real production bugs that Elastic restricted indices to a single type in 6.0 and removed types completely in 7.0. If you genuinely need separate schemas now, that's what separate indices are for.
match runs the query text through the same analyzer as the field, at search time, so "Running" gets normalized (lowercased, and stemmed if a stemmer's in the chain) into "run," the same token the indexer stored. term skips analysis entirely and looks for the literal string "Running shoes" in the inverted index, which was never stored that way, since indexing broke it into separate, normalized tokens in the first place. Running term against a text field is one of the most common Elasticsearch mistakes there is, and it fails silently: no error, just zero results.
GET /products/_search
{
"query": {
"match": { "description": "Running shoes" }
}
}
GET /products/_search
{
"query": {
"term": { "description": "Running shoes" }
}
}bool has four clauses: must (AND, contributes to the relevance score), filter (AND, ignores scoring entirely), should (OR, adds to score, useful for boosting rather than requiring), and must_not (NOT, excludes matches, no scoring). filter and must can produce an identical result set for a simple exact condition, but filter skips the scoring calculation per document and its result can be cached as a bitset, since a filter's answer doesn't change between identical queries. Conditions like status = "published" or category = "shoes" belong in filter almost every time, purely for the performance difference, not because the logic is different.
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "description": "running shoes" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "price": { "gte": 40, "lte": 120 } } }
]
}
}
}range handles gt, gte, lt, and lte comparisons, mainly on numeric and date fields, though it also works lexicographically on keyword fields. The gotcha on dates is date math rounding: "now-7d/d" isn't exactly 7 days before this instant, the "/d" rounds down to the start of that day, so depending on what time it is right now, the actual window can be off from what someone expects by nearly 24 hours.
GET /orders/_search
{
"query": {
"range": {
"order_date": {
"gte": "now-7d/d",
"lte": "now"
}
}
}
}A wildcard query works, but it's expensive on high-cardinality fields, especially with a leading wildcard like *shoe, since Elasticsearch can't use the term dictionary's prefix optimization and ends up scanning far more terms than a trailing wildcard would (query_string queries expose an allow_leading_wildcard flag for this, true by default, though plenty of teams flip it off once they've felt the cost). The dedicated wildcard field type, added in 7.9, handles this more efficiently at real scale, since it indexes n-grams internally for exactly this pattern. For genuine substring or prefix search under real load though, the right long-term answer is usually a text field set up with an edge_ngram tokenizer at index time, not a wildcard query at query time. Wildcard queries scan the term dictionary and slow down fast as cardinality grows; ngram fields do the expensive work once, at index time, instead of on every search.
The cardinality aggregation uses HyperLogLog++, an approximate algorithm, not an exact distinct count. An exact count across a sharded, distributed index would mean shipping every unique value to one coordinating node to deduplicate, which gets prohibitively expensive in memory and network at real scale. HyperLogLog++ trades a small, bounded error rate, tunable through precision_threshold (default 3000), for a fixed memory footprint regardless of how many actual unique values exist. For a dashboard showing roughly how many unique visitors there were, that trade-off is the right one. For something like billing reconciliation where the count has to be exact, you shouldn't be computing it in the aggregation layer at all.
BM25 (Okapi BM25) has been the default similarity since Elasticsearch 5.0, released in October 2016. It uses the same underlying signals as TF-IDF: term frequency, inverse document frequency (rarer terms score higher), and field-length normalization. What BM25 actually changes is that term frequency's contribution saturates. A term appearing 20 times in a document doesn't score anywhere near 20 times higher than it appearing once. The marginal value of each extra occurrence drops off, which tracks how relevance actually feels to a person reading the result, closer than TF-IDF's roughly linear scaling ever did.
Field-length normalization, controlled by BM25's b parameter (default 0.75). A term appearing twice in a 10-word field is a much stronger signal than the same term appearing twice in a 2,000-word field, and BM25 penalizes the longer field's match accordingly. It's tunable per field, b at 0 disables length normalization entirely, at 1 applies it fully, but I'd only touch b or the term-frequency saturation parameter k1 after actually diagnosing a bad ranking with _explain. Tuning similarity parameters before you've measured a real problem is a good way to make search worse in a way that's harder to notice.
Elastic's own sizing guidance points to roughly 20-40GB per shard for most workloads, with 50GB quoted as a rough ceiling that's been seen to work across varied use cases. Too many small shards waste resources on fixed per-shard overhead, cluster state entries, file handles, in-memory segment metadata, so a cluster can run out of heap from shard count alone even with a small total data volume. Shards that are too large recover more slowly after a node failure or restart, since the whole shard has to move or rebuild, and they can bottleneck one node's disk and CPU during merges.
GET /_cat/shards/products?v&h=index,shard,prirep,store&s=store:desc
GET /_cat/indices?v&h=index,pri,rep,store.size&s=store.size:descbest_fields is the default, and it treats each field as an independent query, then takes the single highest-scoring field match per document and uses that as the document's score, with tie_breaker optionally adding a fraction of the other fields' scores. It's the right choice when you want a document to rank well because it matched strongly on any one field, title or body, without needing to match well on both.
most_fields is for the opposite case, redundant copies of essentially the same text analyzed differently, say a title field and a stemmed title sub-field, where you want the combined evidence from all the copies to add up rather than picking the best one. cross_fields is for when a single logical value is actually split across multiple physical fields, first_name and last_name being the classic example, and you want a query for a full name to be treated as if those fields were one combined field, matching a term in either field but scoring the combination correctly rather than penalizing the document for term frequency being split across two fields. Getting this wrong usually shows up as relevance complaints, someone querying full names against split fields with best_fields and getting bad ranking because the query is scoring first_name and last_name as if they were competing, unrelated fields.
search_after takes the sort values from the last document on the current page and uses them as a starting point for the next query, so Elasticsearch can jump directly to documents after that point using the index structure instead of re-sorting and discarding everything before the offset. Because it relies on sort values rather than a numeric offset, you need a tiebreaker, a unique field like _id or _seq_no included in the sort, so that documents with identical sort values on the leading field still get a deterministic order across pages.
What you give up is random access. You can't jump straight to page 40, you have to walk through every page before it, because each request needs the previous page's last sort values. That's usually fine for infinite-scroll UIs or export jobs that walk the whole result set sequentially, but it doesn't work for a UI with numbered page links. It's also worth knowing search_after alone doesn't give you a consistent snapshot if the index is being written to concurrently, documents can shift between shards being refreshed mid-walk; pairing it with a point-in-time context is what actually freezes the view for the duration of the pagination.
An index template defines settings, mappings, and aliases that get applied automatically to any new index whose name matches the template's index_patterns. Component templates are reusable building blocks, a shared set of settings or mappings that multiple index templates can reference, so you don't repeat the same shard count or common field mappings across a dozen near-identical templates.
When more than one index template's pattern matches a new index name, priority decides which one wins, the template with the higher priority value is applied and lower-priority templates are ignored entirely for that index, it's not a merge across templates at that level. Within a single template though, component templates listed in composed_of are merged in the order listed, later ones override earlier ones field by field. This gets people in practice when they have a broad catch-all template with priority 0 and a specific one with priority 100, forget the specific one exists, and can't figure out why a new index isn't picking up settings they expect, usually because the wrong template matched or a component template further down the composed_of list silently overwrote a setting.
ILM automates what happens to an index as it ages, without someone manually running curator-style scripts. The phases are hot, warm, cold, frozen, and delete. Hot is where an index is actively being written to and queried frequently, typically on your fastest, most expensive nodes. Warm is for indices that are done being written to but still get queried occasionally, you'd usually force-merge segments down and maybe reduce replica count here to save resources. Cold and frozen push data onto cheaper, slower storage for indices that are rarely queried but still need to be searchable for compliance or occasional lookups. Delete removes the index outright once it's past its retention window.
A realistic policy for application logs might be: hot for the first day while the index is actively receiving writes and being watched for incidents, roll over to a new index once it hits a size or age threshold, move to warm after a day, force merge, reduce replicas, move to cold after seven days, move to cheaper storage tier, maybe make it read-only, and delete after 30 or 90 days depending on the retention requirement. The actual thresholds always depend on query patterns, if support engineers regularly need to search 60-day-old logs, cold phase needs to actually stay fast enough to be usable, not just cheap.
Rollover creates a new backing index and starts writing to it once the current index crosses a threshold you define, typically max_age, max_docs, or max_primary_shard_size. A data stream is the abstraction on top of this, it presents as a single named target for writes and reads while internally it's actually a sequence of backing indices that ILM rolls over automatically as thresholds are hit.
The reason this beats one giant index is almost entirely operational. A single index that grows forever eventually has oversized shards that are slow to relocate, slow to recover after a node failure, and expensive to force-merge. With rollover, each backing index stays a manageable size, and ILM can independently apply different settings to each one based on its age, an index from six months ago can sit on cold storage with zero replicas while this week's index is on hot nodes with full replication. Deleting old data also becomes trivial, you just drop the whole backing index for data past its retention window, which is a metadata operation, instead of running a slow delete-by-query across a giant index looking for old documents.
doc_values is an on-disk, column-oriented data structure built at index time for every field that supports sorting, aggregating, or scripting, keyword, numeric, date, and so on. Because it's built once at index time and stored on disk, it's memory-efficient and the operating system's file cache handles the hot parts naturally.
text fields don't get doc_values because they're analyzed, split into tokens, so there's no single stable per-document value to store that structure for. If you need to sort or aggregate on a text field anyway, the fallback is fielddata, which builds that same structure in heap memory on demand at query time, effectively loading every unique term from every document into JVM heap. On a field with high cardinality or a lot of documents, that can consume gigabytes of heap in one query and has genuinely taken down production clusters with OutOfMemoryErrors or long GC pauses. The fix is almost always to map a keyword sub-field, the default with dynamic mapping's fields block, and aggregate on that instead, since keyword fields get doc_values automatically and never need fielddata at all.
Highlighting takes the terms that matched your query and wraps them in markup, em tags by default, in the returned document snippet, so a UI can show the user why a document matched. To do that, Elasticsearch needs to know exactly where in the text those terms occur, which requires re-locating the matches against the field's actual content.
The plain highlighter does this the expensive way, it re-analyzes the field text at query time using the same analyzer the field was indexed with, then rescans it to find matches. That's fine occasionally but scales badly across many fields or large documents. The unified and fvh (fast vector highlighter) highlighters instead rely on data captured at index time, fvh needs term_vectors set to with_positions_offsets on the field, which stores exact term positions and offsets during indexing so the highlighter doesn't need to re-analyze anything at query time. The tradeoff is index size and indexing cost go up a bit to buy query-time speed, which is the right trade for a field you know will be highlighted often, like a search result's title or body snippet.
The straightforward tool is function_score, which wraps your normal query and lets you attach one or more functions that modify the base relevance score, a field_value_factor function that multiplies the score by a transformed version of a numeric field like view_count, a decay function such as gauss, exp, or linear that scores documents higher the closer they are to some target, useful for recency where you want a smooth falloff rather than a hard cutoff, or a script_score for genuinely custom logic.
The part people get wrong is not normalizing before combining. A raw view_count field might range from 0 to 10 million, while BM25 relevance scores typically live in the single digits to low tens. Multiply them together directly and popularity completely drowns out actual text relevance, every search just becomes sort by popularity. The fix is to compress the business signal first, a log transform on view counts is common, and use the boost_mode and score_mode settings, multiply, sum, avg, deliberately rather than accepting the defaults, then actually eyeball the resulting scores on real queries to check the blend feels right instead of assuming the math works out.
Every document carries a _seq_no and _primary_term underneath, which together uniquely identify the exact version of a document at the point it was last written. When you read a document back, you can capture those two values, and when you write an update, you pass them back with if_seq_no and if_primary_term. If another process updated the document in between your read and your write, the sequence number has moved on, and Elasticsearch rejects your update with a version conflict instead of silently overwriting the other change.
This matters anywhere multiple processes can update the same document concurrently, a stock-decrement operation, a shopping cart, a shared counter. Without concurrency control, two nearly-simultaneous read-modify-write cycles can produce a lost update, both reads see the same starting state, both writes succeed, but one of them's changes just vanish. The older approach used a plain internal version number and worked the same conceptual way; _seq_no and _primary_term replaced it because they also correctly account for primary shard relocations and promotions, situations where a simple incrementing version number could actually go backward or get reused after a failover.
An ingest pipeline is a sequence of processors that run inside Elasticsearch itself, on the ingest node, before a document is actually indexed. Each processor does one small transformation, grok or dissect to parse an unstructured log line into structured fields, date to parse a timestamp string into an actual date field, geoip to turn a raw IP into a country and city, remove or rename to clean up fields, script for anything more custom.
The case for doing this inside Elasticsearch rather than in application code is mostly about centralizing the transformation logic across many different producers. If ten different services all ship raw log lines in slightly different formats, and you want them all to end up with the same normalized field names and geo enrichment, putting that logic in one pipeline means you fix the parsing rule once, in one place, and it applies to every future document regardless of which service or agent shipped it, since Filebeat and Logstash both support pointing at an ingest pipeline by name. It also means you can change the transformation without redeploying every producer, you just update the pipeline definition. The tradeoff is it adds CPU cost on the ingest node path, and a badly written grok pattern can silently fail to match and leave a document unparsed, which is worth testing with the simulate pipeline API before turning it loose on real traffic.
A field mapped as geo_point stores a latitude and longitude pair, internally encoded so it can be indexed efficiently and used in distance calculations and sorting. geo_bounding_box filters documents whose point falls inside a rectangle defined by a top-left and bottom-right corner, it's cheap, essentially two range comparisons, and it's the right tool when you're filtering to what's visible in the current map viewport.
geo_distance filters, or scores if used in a function_score, by actual distance from a center point, restaurants within five miles of here, which is a genuinely different geometric shape, a circle, and costs more to compute per document than the bounding box check. A common pattern is to use geo_bounding_box as a cheap pre-filter to narrow down candidates to roughly the right area, then geo_distance for the actual precise radius check or for sorting results by distance, since geo_distance sorting lets you order results by proximity to the query point directly. One gotcha specific to geo_point: because of how the coordinates get encoded for indexing, extremely high-precision requirements can run into small rounding effects, which almost never matters for typical local search but has bitten teams doing anything closer to engineering-grade geospatial work.
An edge_ngram analyzer breaks a word into indexed fragments of increasing length, so a prefix search just becomes a normal match query against those pre-generated fragments. It's flexible, it composes with normal relevance scoring, and it's simple to reason about, but every extra character of ngram length multiplies the index size, and it can only really match from the start of a token unless you also generate ngrams per-word for multi-word autocomplete.
The completion suggester is purpose-built for this instead. It builds an in-memory finite state transducer structure specifically for prefix lookups, which makes it extremely fast, sub-millisecond typically, and memory-efficient compared to ngram indexing the same data as regular text. The catch is it's a separate, more rigid feature, it needs its own completion field type and input values, and it doesn't participate in the normal query DSL or scoring the way a match query does, so if you need typo tolerance, filtering by other fields alongside the suggestion, or ranking suggestions by a business metric like popularity, you either lean on the suggester's own limited scoring or fall back to ngrams inside a real query. In practice a lot of production search boxes end up combining both, completion suggester for the fast dropdown-as-you-type experience, and a full query against ngram or text fields once the user actually submits a search.
Circuit breakers exist to stop a single request or operation from consuming so much heap that it crashes the node with an OutOfMemoryError. Before allocating memory for something expensive, Elasticsearch estimates the size and checks it against the breaker's configured limit, if the estimate would push past the limit, the operation is aborted with a CircuitBreakingException instead of letting the JVM actually run out of heap.
There's a parent breaker that caps total memory across all the child breakers combined, and then specific ones underneath it: the fielddata breaker guards against the fielddata-on-text-field problem, loading too many unique terms into heap, the request breaker guards against a single request doing something memory-heavy, like a poorly-bounded aggregation with very high cardinality, and the in-flight requests breaker guards against too much data queued up from network requests that haven't been processed yet. In practice the fielddata and request breakers are the ones that trip most often, usually from an aggregation with cardinality or terms size set way too high, or an old fielddata-on-text mapping nobody cleaned up. When you see repeated CircuitBreakingExceptions in logs, the fix is almost never raising the breaker limit, that just delays the crash, the real fix is finding and fixing the query or mapping causing the unbounded memory use.
refresh_interval, 1 second by default, controls how often Elasticsearch takes documents sitting in an in-memory buffer and writes them into a new, searchable Lucene segment. The document is already durable at that point, since it's written to the translog. It just isn't visible to search yet. That gap is exactly why Elasticsearch calls itself near-real-time instead of real-time: there's a small, configurable window between "written" and "searchable." You can force it early with refresh=wait_for on a request, but doing that on every write hurts throughput, since every refresh creates a new small segment that eventually needs merging.
Since a field's mapping can't be edited after documents exist with the old type, the standard move is to build a brand-new index with the corrected mapping, then use the _reindex API to copy documents from the old index into the new one. For a large index you'll usually run this with slicing, either manual slices or the automatic slice count, so multiple workers copy in parallel instead of one single-threaded pass grinding through everything.
The zero-downtime part comes from aliases. Your application should already be pointing at an alias, not the raw index name. Once the reindex completes and you've spot-checked document counts and a few sample queries against the new index, you atomically swap the alias to point at the new index using the alias update API, which supports removing the old mapping and adding the new one in a single call so there's no window where the alias points nowhere. Any documents written to the old index during the reindex window need to be replayed, which is why teams often pause writes briefly, or run a second, smaller reindex pass filtered by timestamp to catch the delta before flipping the alias.
Hard questions
12Dynamic mapping only samples the first document it encounters to decide a new field's type. Once that guess is locked in, it's fixed for the life of the index. Elasticsearch doesn't retroactively widen a field's type just because a later document looks different. A field that got typed as date from "2026-01-05" will reject a document later sending "01/05/2026" unless that exact format was anticipated.
PUT /orders
{
"mappings": {
"properties": {
"order_date": {
"type": "date",
"format": "yyyy-MM-dd||dd/MM/yyyy||epoch_millis"
}
}
}
}The real fix is an explicit mapping set up before data starts flowing, listing every date format you actually expect, like the example above. For an index that's already broken, the field's type is locked, so the honest answer is reindexing into a new index with the corrected mapping. I've seen teams just disable dynamic mapping in production entirely once they've been bitten by this once.
Analyzers run at two points: index time, to build the stored tokens for a text field, and query time, to process the search string. Changing an analyzer's definition doesn't retroactively re-analyze anything already indexed. Old documents' inverted-index entries were built with the old chain, before the synonym filter existed, and they stay that way until something re-indexes them. To fix it for existing data, you reindex through the new analyzer, usually via the Reindex API into a new index. Separating index_analyzer and search_analyzer buys you some flexibility going forward, but it doesn't reach backward into segments that were already written.
The analyzer chain and field mapping, before touching query syntax. Common culprits: stopword or stemming settings that stripped a distinguishing token, term-frequency effects where a short field scores higher for the same match count (which is BM25's field-length normalization working as designed, not a bug), or a multi_match type that doesn't fit the use case. The actual debugging tool is the _explain API, or explain=true on the search request, which returns the full scoring breakdown per document. Guessing at ranking problems from the outside wastes time that _explain gives you for free.
A terms aggregation runs independently per shard first. Each shard returns only its own top buckets, governed by shard_size, a separate and usually larger setting than the final size, and the coordinating node merges those partial results. A category that's genuinely common overall but spread thin across many shards, never ranking high enough locally on any single one, can get its true count undercounted, since it's missing contributions from shards where it didn't make the local cut. doc_count_error_upper_bound is Elasticsearch's own worst-case estimate of that undercount for the buckets it did return. It gets worse the more shards you have and the flatter the data distribution is; raising shard_size tightens accuracy at the cost of more data moving between nodes.
refresh makes data searchable, moving the in-memory buffer into a new Lucene segment that may still only exist in the filesystem cache, not yet fsynced to disk. flush is the operation that actually fsyncs segments to disk and trims the translog up to that point. That's the durability boundary, not refresh. A document is already durable, and survives a node crash, once it's written to the translog, which by default fsyncs on every single request (index.translog.durability defaults to "request"), completely independent of refresh_interval. People who stretch refresh_interval out to speed up indexing sometimes think they're trading away durability. They aren't. They're trading search latency for indexing throughput; durability is a separate setting they never touched.
Older versions of Elasticsearch relied on you manually setting a minimum master nodes value to roughly half the master-eligible nodes plus one, so that a group of nodes could only elect a master if it had a majority of the master-eligible nodes present, making it mathematically impossible for two separate partitions to both have a majority at the same time. Get that setting wrong, set it too low, or leave it at a value that made sense for a three-node cluster after scaling to six, and you could end up with two masters simultaneously believing they were in charge, each accepting writes independently, which corrupts cluster state when the partition heals and the two histories have to be reconciled.
Modern Elasticsearch, from version 7 onward, removed that manual setting and replaced it with a quorum-based voting configuration that Elasticsearch manages automatically as nodes join and leave, it's fundamentally the same math, majority quorum required to elect a master, but it adapts as cluster membership changes so you can't misconfigure it the way people used to. If a network partition splits a five-master-eligible-node cluster into a three-node side and a two-node side, the three-node side has quorum and can still elect a master and keep accepting writes for indices whose shards it holds, while the two-node side can't elect a master, effectively goes read-only for cluster state changes, and any of its nodes serving as the sole copy of unreplicated data become unavailable until the partition heals. This is exactly why running an even number of master-eligible nodes, or running only two, is a real production risk, there's no way to guarantee a majority on either side of certain splits.
When a document is indexed, it's written into an in-memory buffer, eventually becoming a Lucene segment on refresh, and also appended to the translog, a write-ahead log on disk. The translog exists because refresh, the operation that makes documents searchable, happens far more often and cheaply than flush, the operation that actually fsyncs Lucene segments to disk and lets Elasticsearch discard the corresponding translog entries. Between flushes, if a node crashes or loses power, whatever's only in the in-memory Lucene buffer is gone, but the translog on disk lets Elasticsearch replay everything since the last flush and rebuild the correct index state on restart.
The durability setting controls when a write is fsynced to the translog itself, and that's where the real tradeoff lives. With translog durability set to request, the default, every single index, delete, or bulk request is fsynced to the translog before Elasticsearch returns success, so an acknowledged write is genuinely durable against a crash even before the next flush, at the cost of fsync latency on every write. Set to async, the translog is only fsynced on an interval, five seconds by default, which is meaningfully faster for high-throughput bulk loads, but it means a crash within that window can lose writes that were already acknowledged to the client as successful. Teams doing large one-off bulk imports where losing the last few seconds of data just means re-running the import will sometimes flip to async temporarily for the throughput win, then flip back to request durability for normal live traffic.
This is the signature of hitting a disk watermark threshold. Elasticsearch monitors disk usage per node against three thresholds by default: the low watermark, around 85 percent used, stops new shards from being allocated to that node, but existing shards keep running fine; the high watermark, around 90 percent, actively starts relocating shards away from that node to free up space; and the flood-stage watermark, around 95 percent, is the aggressive one, at that point Elasticsearch puts a read-only-allow-delete block on every index that has a shard on that node, meaning you can delete documents to free space but you can't index or update, which is exactly the sudden-writes-stopped symptom.
The fix isn't just freeing disk space, that's necessary but not sufficient. Once the flood-stage block has been applied, the index-level read-only setting sticks even after disk usage drops back down, you have to explicitly remove it with a settings update clearing the read-only-allow-delete flag, otherwise the index stays stubbornly read-only even though there's plenty of disk again. The actual root-cause fix depends on why disk filled up, old indices that should've been deleted by ILM but weren't, check the ILM policy actually applied and didn't error out, unexpectedly large replica counts multiplying storage, or genuinely needing to add nodes or storage because data volume grew past what was provisioned.
Exact kNN compares your query vector against every single vector in the index and returns the true closest matches by cosine similarity, dot product, or l2 norm, whatever similarity metric you configured. That's correct by definition but scales linearly with the number of vectors, which gets slow fast once you're past a few tens of thousands of documents, since it's brute-force distance computation per query.
Approximate kNN, which is what the indexed knn search type actually uses for dense_vector fields, builds an HNSW graph at index time, essentially a layered graph structure where each vector is connected to its approximate nearest neighbors, and querying it means walking the graph greedily toward the query vector's region instead of touching every vector. This is dramatically faster, sublinear rather than linear in the number of vectors, but it's approximate, it can miss the true nearest neighbor occasionally in exchange for that speed. The knobs you actually have are num_candidates, how many candidates each shard considers before returning its local top matches, higher means better recall at the cost of more compute per shard, and the graph construction parameters set at index time, higher values build a denser, more accurate graph at the cost of slower indexing and more memory. In practice you tune num_candidates against a held-out set of queries where you know the true nearest neighbors, checking recall at a few different settings, until you find the point where recall is good enough for your use case without over-paying in query latency, there's no universally correct default because it depends entirely on the vector distribution and how much a wrong result actually costs your application.
Cross-cluster search lets a coordinating node fan a single query out to remote clusters and merge the results back, which is useful when you want one query to search data that legitimately lives in separate clusters, different regions each running their own cluster for data residency reasons, say, and you occasionally need a global view. The data never moves, each cluster still owns its own copy, and a query against a remote cluster is genuinely slower because it's a real network round trip per search, and if the remote cluster is unreachable that portion of results is simply missing, or the whole query fails, depending on how skip_unavailable is configured.
Cross-cluster replication is a different tool for a different problem, it actually copies index data from a leader index on one cluster to a read-only follower index on another cluster, continuously, by replaying the leader's operations. The main uses are disaster recovery, a follower cluster in another region that's ready to be promoted to primary if the leader region goes down, and moving data physically closer to readers to cut query latency without giving up a single source of truth for writes. What CCR guarantees is eventual consistency of the follower relative to the leader, not synchronous replication, there's a real lag window, and the follower index is strictly read-only until you explicitly promote it, unfollow and convert to a regular writable index, which is a manual, deliberate step you'd do during an actual failover, not something that happens automatically just because the leader became unreachable.
You start by tagging nodes with a custom attribute reflecting their tier, hot, warm, or cold, on top of the normal data node role, and provisioning genuinely different hardware behind each tier, hot nodes on fast local SSDs with more CPU for active indexing and querying, warm nodes on cheaper, larger disks for data that's done being written but still queried occasionally, cold nodes on the cheapest storage available for data that's rarely touched but still needs to be searchable. Then an ILM policy, tied to index templates for your data streams, actually moves indices between tiers as they age, using the migrate action or explicit allocation settings that require the shard land on a node with the right tier attribute at each phase transition.
What actually goes wrong is usually one of two things. First, if the allocation attribute value has a typo or mismatch between what the ILM policy sets and what nodes are actually tagged, say the policy requires warm but no node in the cluster has that exact attribute value, the shard becomes unassigned, cluster health goes yellow or red, and it looks like a totally unrelated allocation problem until you check cluster allocation explain and see the allocation rule can't be satisfied. Second, under-provisioning a tier relative to how much data actually lands there over time, if warm tier capacity was sized for the data volume at rollout but ingest volume doubled, ILM tries to migrate an index into warm and there isn't room, shards sit unassigned or the migration silently stalls, and you only notice when someone's dashboard query against recent data starts timing out because the index that should have already moved to faster storage is still stuck on hot, crowding out genuinely active indices.
The first stop is the cluster allocation explain API, which for an unassigned shard tells you exactly why Elasticsearch's allocator won't place it, rather than making you guess. Common answers it gives: no node currently satisfies a shard allocation filtering rule you've set, say an awareness attribute requiring the shard land in a specific rack or zone that has no available capacity, a node hit a disk watermark threshold and Elasticsearch refuses to allocate more shards there, the shard's data actually doesn't exist anywhere because the node that held it left the cluster and never came back, which usually means you're looking at actual data loss if there's no replica, or in the case of a replica specifically, the primary just hasn't finished initializing yet and the replica is waiting its turn.
Beyond that single-shard check, node-level context matters. Cat APIs for nodes and allocation show whether nodes are actually up, how full their disks are, and whether a node dropped out of the cluster entirely, check the master's logs for node departure events, and check that node's own logs for why it left, OOM, GC pauses long enough to miss heartbeat, a hardware or network issue. If it turns out the shard is genuinely gone, primary lost, no replica existed, the honest fix is restoring from a snapshot if you have one, there's no way to allocate a shard whose data physically doesn't exist anywhere in the cluster, and forcing allocation of an empty primary, which Elasticsearch will let you do as a last resort, means accepting data loss for whatever was in that shard.
How to prepare for an Elasticsearch interview in 2026
Skip the slide deck on shard architecture and just build a small cluster. docker-compose gets a 2-node Elasticsearch cluster running in a few minutes. Load a few thousand documents, run a match query and a term query against the same field, and stare at why one works and the other doesn't. Change a field's analyzer, reindex, and watch old search behavior actually change. Kill a node on purpose and watch cluster health flip from green to yellow to, if you're unlucky with replica placement, briefly red. Reading about near-real-time search is nothing like watching your own document sit in the index for a fraction of a second before a search query actually returns it.
Across mock interviews run through LastRoundAI tagged backend or search, more candidates stumble on the shard-sizing and bool-versus-filter questions than on aggregations, even though most study time goes toward aggregations because they read as the more sophisticated topic. My guess is that operational judgment gets skipped in favor of features that demo well, right up until an interviewer asks a candidate to reason about a cluster under real load. We don't have a clean percentage to attach to that pattern, only that it comes up often enough in review to flag here.
Get the follow-up questions before an interviewer does
Explaining an analyzer chain on paper is easy. Defending it after an interviewer changes the field type, drops a replica, or asks why your query just returned zero results is a different skill entirely. LastRoundAI's mock interview mode runs backend and search-focused rounds with follow-up questions that adapt to what you actually said, not a fixed script, and the free plan includes 15 credits a month that reset monthly. Starter is $19/mo if fifteen sessions isn't enough runway to feel ready.
If the harder problem right now is finding enough roles that actually mention Elasticsearch or search infrastructure, rather than passing the interview once you land one, Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on Ultimate, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com. That's the only inbox we check.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
Do I need hands-on Elasticsearch experience to pass?
It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.
Is Elasticsearch still worth learning in 2026?
For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.
Should I memorise Elasticsearch syntax for the interview?
Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.
What is the most common mistake in Elasticsearch interviews?
Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.

