Zero-Downtime Reindexing in OpenSearch with Aliases
OpenSearch index mappings are immutable once data is indexed. Changing a field type (for example, from text to keyword for a product SKU) or adding an analyzer to an existing field requires creating a new index and moving data into it. The challenge is doing this without taking your search API offline. The answer is aliases combined with the _reindex API. This post demonstrates the full pattern on a live OpenSearch 2.19.1 cluster managed by FoundryDB.
All commands use YOUR_OPENSEARCH_HOST and YOUR_PASSWORD as placeholders.
Prerequisites
- A running FoundryDB OpenSearch cluster.
curlandjqinstalled locally.
Step 1: Create the Initial Index with an Alias
Start with a products-v1 index and a products alias pointing to it as the write target.
# Create v1 index
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/products-v1" \
-H "Content-Type: application/json" \
-d '{
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
"mappings": {
"properties": {
"sku": {"type": "text"},
"name": {"type": "text"},
"price": {"type": "float"}
}
}
}'
# Create the write alias
curl -u app_user:YOUR_PASSWORD -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/_aliases" \
-H "Content-Type: application/json" \
-d '{
"actions": [
{"add": {"index": "products-v1", "alias": "products", "is_write_index": true}}
]
}'
All application code writes to and reads from products, never directly to products-v1.
Step 2: Ingest Documents via the Alias
for doc in \
'{"sku":"KB-001","name":"Wireless Keyboard","price":79.99}' \
'{"sku":"HUB-007","name":"USB-C Hub 7-port","price":49.99}' \
'{"sku":"KB-002","name":"Mechanical Keyboard","price":129.99}'; do
curl -u app_user:YOUR_PASSWORD -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/products/_doc" \
-H "Content-Type: application/json" \
-d "$doc"
done
All 3 documents land on products-v1, confirmed by the _index field in each response.
Step 3: Create the Improved v2 Index
The v2 index changes the sku field from text to keyword (enabling exact-match filtering and aggregations), adds an English analyzer to name, and introduces a new category field.
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/products-v2" \
-H "Content-Type: application/json" \
-d '{
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
"mappings": {
"properties": {
"sku": {"type": "keyword"},
"name": {"type": "text", "analyzer": "english"},
"price": {"type": "float"},
"category": {"type": "keyword"}
}
}
}'
At this point, products-v2 exists but is empty. The products alias still points to products-v1. Reads and writes continue unaffected.
Step 4: Reindex Data from v1 to v2
curl -u app_user:YOUR_PASSWORD -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/_reindex" \
-H "Content-Type: application/json" \
-d '{
"conflicts": "proceed",
"source": {"index": "products-v1"},
"dest": {"index": "products-v2"}
}'
Result from the test (3 documents):
{"took": 23, "total": 3, "created": 3, "updated": 0, "failures": 0}
The reindex completed in 23 ms. For larger indices this will take longer. The key point is that during the entire reindex operation, products-v1 continues to serve reads and writes through the alias. No downtime.
conflicts: proceed tells OpenSearch to skip documents that cause conflicts rather than aborting the entire reindex. For a new index with no existing data, there are no conflicts, but it is good practice to include it for safety.
Step 5: Verify Document Count Before the Swap
Before cutting over, confirm the document counts match. Call _refresh first because the default refresh interval is 1 second and scripts can outrun it.
curl -u app_user:YOUR_PASSWORD -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/products-v2/_refresh"
curl -u app_user:YOUR_PASSWORD -k \
"https://YOUR_OPENSEARCH_HOST:9200/_cat/indices/products-v*?v&h=index,docs.count"
Expected output:
index docs.count
products-v1 3
products-v2 3
Counts match. Safe to swap.
Step 6: Atomic Alias Swap
The alias API accepts multiple actions in a single request and executes them atomically. Remove the alias from v1 and add it to v2 in one call.
curl -u app_user:YOUR_PASSWORD -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/_aliases" \
-H "Content-Type: application/json" \
-d '{
"actions": [
{"remove": {"index": "products-v1", "alias": "products"}},
{"add": {"index": "products-v2", "alias": "products", "is_write_index": true}}
]
}'
Verify:
curl -u app_user:YOUR_PASSWORD -k \
"https://YOUR_OPENSEARCH_HOST:9200/_cat/aliases/products?v"
alias index is_write_index
products products-v2 true
Step 7: Confirm New Writes Land on v2
curl -u app_user:YOUR_PASSWORD -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/products/_doc" \
-H "Content-Type: application/json" \
-d '{"sku": "DESK-MAT-01", "name": "Standing Desk Mat", "price": 39.99, "category": "accessories"}'
The _index in the response is products-v2. After a refresh, a search via the alias returns all 4 documents:
{"total": 4, "docs": [
{"id": "1", "index": "products-v2", "name": "Wireless Keyboard"},
{"id": "2", "index": "products-v2", "name": "USB-C Hub 7-port"},
{"id": "3", "index": "products-v2", "name": "Mechanical Keyboard"},
{"id": "4", "index": "products-v2", "name": "Standing Desk Mat"}
]}
Step 8: Clean Up the Old Index
Once you are confident v2 is correct, delete v1:
curl -u app_user:YOUR_PASSWORD -k \
-X DELETE "https://YOUR_OPENSEARCH_HOST:9200/products-v1"
Keep v1 around for at least one day in production before deleting, in case you need to roll back by swapping the alias back.
Important: Always Refresh Before Querying in Scripts
The default refresh interval is 1 second. In automated scripts that index and then immediately search, always call _refresh on the destination index before asserting document counts or querying results. The reindex API completes when all documents are written, not when they are searchable.
What's Next
Provision a FoundryDB OpenSearch cluster at foundrydb.com. Documentation at docs.foundrydb.com.