skip to content

How do you change an Elasticsearch index mapping with zero downtime while writes keep arriving?

level: seniorimportance: should knowfreq 65%

answer

  1. Clients must already talk to an alias
  2. Reindex copies a snapshot, not a live tail
  3. Something must close the gap while it runs
  4. op_type create protects newer dual writes
  5. Verify, then one atomic swap, keep rollback

basics

~20 s

Build a new index with the corrected mapping beside the old one, reindex into it, keep it current with dual writes or repeated timestamp catch-up passes, verify counts and queries, then move the alias in one atomic call and keep the old index for rollback.

solid answer

~50 s

The shape is always the same. Clients read and write through an alias, never a concrete index. You create `products-v2` with the new mapping, then `POST /_reindex` from v1 — remembering that reindex works from a **snapshot taken when the task starts**, so anything written afterwards is missed. You close that gap one of two ways: have the application **dual-write** to both indices during the migration, or run repeated **catch-up reindexes** with a `range` query on an `updated_at` field, with overlap, until the delta is seconds. With dual writes, set `"op_type": "create"` and `conflicts: proceed` on the bulk reindex so the older snapshot copy never clobbers a newer dual-written document. Verify: document counts, sampled document diffs, and real queries run against v2 directly. Then one `_aliases` call removes the alias from v1 and adds it to v2. Keep v1 until you are confident, then delete it.

code

json · 6 lines
json
POST /_reindex?wait_for_completion=false
{
  "conflicts": "proceed",
  "source": { "index": "products-v1", "size": 1000 },
  "dest":   { "index": "products-v2", "op_type": "create" }
}

go deeper

for a junior

Recall the outline: new index, reindex, move the alias. You are not expected to handle live writes or rollback at this level, but knowing the index is rebuilt rather than altered matters.

for a middle

Explain that reindex works from a snapshot taken at task start, so a live index needs dual writes or catch-up passes, and that the alias move is a single atomic actions call.

for a senior

Demonstrate the full production sequence including the op_type create guard against clobbering newer writes, deletion handling, verification against the new index by direct name, and an explicit rollback window.

for a principal

Own it as a repeatable pipeline rather than a bespoke operation: templated index creation, automated verification with a judgement set, staged read-then-write flips, and a rollback path the on-call engineer can execute in one call.

## The precondition This whole procedure is only available if **clients address an alias**, not `products-v1`. If they do not, step zero is adding the alias to the existing index and shipping a client change — which is precisely the coordinated redeploy the alias exists to avoid, and why the convention is established on day one. ## Step 1 — build the new index Create `products-v2` explicitly with the corrected mapping and settings. `_reindex` copies **documents only**: it does not carry mappings, settings, or aliases across. Deriving the new index from a versioned index template keeps the mapping under code review. A common optimisation is to create it with `number_of_replicas: 0` and a relaxed `refresh_interval` for the bulk load, then restore both before the swap, so the copy is not paying for replication and constant refreshes while nobody is querying it. ## Step 2 — understand what reindex does and does not see `_reindex` scrolls a **point-in-time view of the source captured when the task begins**. Documents created, updated or deleted in the source afterwards are invisible to it. On a live index this is the single most important fact in the whole migration: when the reindex finishes, v2 is already stale by however long the reindex took. Run it asynchronously (`wait_for_completion=false`), sliced, and throttled — see the operational detail in its own right — and record the task id. ## Step 3 — close the gap Two strategies, often combined. **Dual write.** The application writes every change to both v1 and v2 for the duration. From the moment dual-writing starts, v2 never falls behind for new traffic; the reindex only has to carry the historical backlog. The hazard is ordering: a document dual-written at 10:00 could be overwritten at 10:05 by the reindex copying the *older* snapshot version. Guard against it with `"op_type": "create"` on the destination, which makes the reindex refuse to overwrite an existing document id, plus `conflicts: proceed` so those refusals do not abort the job. Alternatively, use external versioning so the higher version always wins. **Catch-up passes.** If dual writing is not practical, run further reindexes filtered on `updated_at >= <start of previous pass minus a safety margin>`. Each pass is smaller than the last. When the residual window is a few seconds, you can either accept it (if reads tolerate brief staleness) or flip the *write* alias first, drain, run one final catch-up, then flip the read alias. **Deletes are the hard part.** A timestamp catch-up finds creates and updates but cannot see a document that was deleted from v1 after the snapshot — it leaves a ghost in v2. Handle it with soft deletes (a `deleted` flag the catch-up query can see), by having the dual-write path replicate deletes, or by reconciling ids after the fact. Candidates who raise deletes unprompted are showing they have actually run one of these. ## Step 4 — verify before you swap - Compare `_count` on both indices, accounting for documents deliberately filtered out. - Diff a random sample of documents by id. - Run production-shaped queries **directly against `products-v2`** and compare hits and ordering with v1. A mapping change frequently changes relevance; discovering that after the swap is the classic self-inflicted incident. - Restore `number_of_replicas` and let the new replicas allocate before taking traffic, or the first minute of production load lands on unreplicated shards. ## Step 5 — the atomic swap ``` POST /_aliases { "actions": [ { "remove": { "index": "products-v1", "alias": "products" } }, { "add": { "index": "products-v2", "alias": "products" } } ] } ``` One cluster-state update, no gap. If reads and writes use separate aliases you can stage them: flip reads first to validate under real query load while writes still land in both, then flip writes. ## Step 6 — keep the exit open Do not delete v1 in the swap. Leave it for a rollback window — reverting is another single `_aliases` call, which is the cheapest incident response available. If dual writing continued through the swap, v1 stays current and rollback is lossless. Once satisfied, stop dual writes and delete v1. ## What good answers include Snapshot semantics, the `op_type: create` guard, deletes, verification against the new index before the swap, and an explicit rollback plan. What weak answers do: reindex, swap, delete the old index, and assume the counts matched.

  • Your catch-up passes rely on an updated_at range query. Which change in the source will they never repair?
    Deletions. A document removed from the source after the reindex snapshot leaves no row for a range query to match, so the copy survives in the new index as a ghost. Cover it with soft deletes that the catch-up query can see, by replicating deletes through the dual-write path, or by reconciling id sets before the swap.
  • How do you validate relevance before flipping the alias, given the mapping changed?
    Query `products-v2` directly — by name, not through the alias — with a set of production-representative queries, and compare hit counts and top-N ordering against v1. Analyzer or field-type changes shift scoring, so a purely count-based check passes while search quality regresses. Judgement lists or a saved sample of real queries make this repeatable.
  • Why not simply stop writes for the duration of the reindex?
    On a small index during a maintenance window that is genuinely the simplest correct answer, and worth saying. It stops being viable when the reindex takes hours or the write path cannot buffer — then you need dual writes or catch-up passes. Naming the cheap option and its limits is better than pretending complexity is always required.
  • What is your rollback if the new index misbehaves ten minutes after the swap?
    One `_aliases` call moving the alias back to v1, which is why v1 is not deleted during the cutover. If dual writes were kept running through the swap, v1 is still current and rollback loses nothing; if they were not, you must weigh the writes that landed only in v2 and replay them.

saying these in an interview costs you the question

  • Assumes _reindex keeps tailing documents written after it starts
  • Deletes the old index immediately after the swap
  • Repoints the alias with separate delete and add calls
  • Ignores documents deleted from the source during migration
  • Validates only document counts and never runs real queries

context