Skip to content
ExponaBLOG
ELASTICSEARCH & SEARCH ENGINEERING · DAY 033 / 100

Zero-downtime reindexing: a hands-on guide for engineers

Change mappings safely.

Expona Admin6 min read

The challenge of changing Elasticsearch mappings

Once you've indexed millions of documents in Elasticsearch, you'll discover that mappings aren't as flexible as you'd hoped. A field type needs to change, a new analyzer is required, or a field must be renamed—and reindexing the entire dataset during normal business hours isn't an option. This guide walks you through zero-downtime reindexing: a hands-on guide for engineers who need to modify mappings safely without dropping queries or accepting downtime.

The core challenge is that Elasticsearch doesn't allow you to modify field mappings in place. The solution is a four-step process: create a new index with the updated mapping, copy data from the old index to the new one, catch up on writes that arrived during the copy, and atomically switch traffic over using aliases. Let's build this step by step.

Step 1: Create a new index with the new mapping

Before you touch any production data, create a second index with your desired mapping changes. The key is to use an alias so your application continues writing to a logical name rather than a concrete index name.

Assume your current setup has an index called products and an alias products-live that points to it. You want to add a new field type and change an analyzer. First, create the new index:

json
PUT /products-v2
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "custom_text": {
          "type": "standard",
          "stopwords": "_english_"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id": { "type": "keyword" },
      "name": {
        "type": "text",
        "analyzer": "custom_text",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "description": { "type": "text" },
      "category": { "type": "keyword" },
      "created_at": { "type": "date" },
      "stock_level": { "type": "integer" },
      "last_updated": { "type": "date" }
    }
  }
}

Notice we've named it products-v2. This naming convention makes it easy to track versions and clean up old indices later. The settings and mappings should mirror your current index, with only the specific changes you need applied. If you're unsure about shard count, match your current index or consult your cluster's capacity planning.

Step 2: Reindex the data

With both indices in place, copy all existing documents from the old index to the new one. Elasticsearch's Reindex API handles this efficiently, and you can control the batch size and throttling to avoid overwhelming your cluster.

json
POST /_reindex
{
  "source": {
    "index": "products"
  },
  "dest": {
    "index": "products-v2"
  },
  "slices": 5
}

The slices parameter parallelizes the reindex operation across five slices, which speeds up the copy significantly on multi-node clusters. For very large indices (hundreds of millions of documents), monitor cluster health and consider reducing slices if you see elevated CPU or memory usage.

The Reindex API is synchronous by default, so if your dataset is enormous, you can run it asynchronously and check status:

json
POST /_reindex?wait_for_completion=false
{
  "source": {
    "index": "products"
  },
  "dest": {
    "index": "products-v2"
  },
  "slices": 5
}

This returns a task ID. Check progress with GET /_tasks?detailed=true&actions=*reindex. In production, we typically let reindex run to completion on a less-busy time window (late evening or early morning) to minimize impact on read latency.

Step 3: Catch up on writes made during the copy

Here's the critical insight: while reindex ran, your application kept writing to products-live. New documents or updates arrived that aren't yet in products-v2

The challenge is determining what changed. If your documents have a last_updated timestamp, you can use a range query:

json
POST /_reindex
{
  "source": {
    "index": "products",
    "query": {
      "range": {
        "last_updated": {
          "gte": "2024-01-15T14:32:00Z"
        }
      }
    }
  },
  "dest": {
    "index": "products-v2"
  }
}

Replace the timestamp with the exact moment the first reindex began. This approach works well if all your documents are timestamped. For systems without reliable timestamps, consider using change data capture (CDC) or maintaining a changelog table to track modifications.

A more robust pattern in high-throughput systems is to dual-write during the catch-up phase: your application writes to both the old index and the new index simultaneously for a short window. This guarantees no writes are missed. Then, once you're confident the new index has all data, you switch traffic over.

Step 4: Swap the alias atomically

Now both indices contain the same data. The final step is to point the alias from the old index to the new one. This is atomic at the Elasticsearch level, so no queries are dropped:

json
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products", "alias": "products-live" } },
    { "add": { "index": "products-v2", "alias": "products-live" } }
  ]
}

All subsequent reads and writes to products-live go to products-v2. Your application code doesn't change; it continues querying and indexing against the alias. After the switch, monitor error rates and latency for a few minutes to confirm everything is working.

Once you're satisfied, delete the old index:

json
DELETE /products

This frees up disk space immediately. If you want to keep a safety window in case you need to rollback, keep the old index around for a day or two before deleting.

Common mistakes

  • Forgetting to use aliases: If your application is hardcoded to query a specific index name rather than an alias, you'll have to deploy code changes to complete the reindex. Always query through aliases so you can swap indices without touching your application.
  • Mismatched shard counts: If the new index has far fewer shards than the old one, write throughput may drop immediately after the switch. Match or slightly increase the shard count to maintain performance.
  • Not testing the mapping changes: Before reindexing production data, test your new mapping on a sample or staging dataset. Edge cases like numeric strings or deeply nested objects can behave unexpectedly with new analyzer or field configurations.
  • Ignoring catch-up writes: The gap between when reindex finishes and when you switch the alias is often underestimated. In high-throughput systems, hundreds of thousands of new writes may arrive. Have a strategy to capture them, don't just hope they're small.
  • Reindexing without monitoring: A reindex operation can consume significant cluster resources. Without monitoring CPU, memory, and query latency, you might tank performance for your users. Run reindex during off-peak hours and watch dashboards.

Key takeaways

  • Elasticsearch mappings cannot be changed in place; you must create a new index and reindex your data.
  • Always route application traffic through aliases, not direct index names, so you can swap indices without code deployments.
  • Use the Reindex API with slicing for parallelism, and throttle it to avoid cluster strain.
  • Capture writes that arrive during reindex using range queries on timestamps or dual-write strategies.
  • Swap traffic atomically using the _aliases API, then delete the old index once you're confident in the new setup.
  • Monitor cluster health, error rates, and query latency before and after the alias switch to catch regressions early.

ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles