Skip to content
⚠️ Before You Start
Risk: High
Duration: 2-4 hours hands-on, plus indexing time (minutes to days, measured in the sandbox) and a 24-72 hour watch window
Last Validated: Not yet validated (draft)
Kind: Runbook
Risk Note: A bad switch or reset affects every user of the index, and a reset cannot be undone

Reindex an Azure AI Search index without downtime

Draft: not validated

Not yet dry-run. Do not use on a live system until the validation record is complete and signed off.

1. Summary

This runbook rebuilds or refreshes an Azure AI Search index while users keep getting answers. There are two paths:

  • Path A, in place: reset the indexer and run it again on the same index. Use it when the schema does not change.
  • Path B, rebuild and switch: build a new index next to the live one, check it, then repoint an index alias in one call. Use it for any change that forces a rebuild, and for any change you might need to undo quickly.

Path B is the default for production. It is the only path with an instant rollback.

Facts checked against Microsoft Learn on 2 November 2026. Latest stable data-plane REST API: 2026-04-01. Index aliases are generally available from 2026-04-01. Reset docs, reset skills, resync and the enrichment cache are preview (latest preview REST API: 2026-08-01-preview).

2. When to use and when not to

Use this runbook when:

  • You change a field's type, name or attributes (searchable, filterable, sortable, facetable).
  • You change or assign an analyser on an existing field, or change an analyser definition.
  • You change chunking, the embedding model, vector dimensions or vector compression.
  • The index holds orphans or deleted documents because deletion detection was missing from the first run.
  • Source content changed in bulk (re-OCR, re-extraction, skill change) and must be reprocessed.

Do not use it when:

  • You only add a field, a scoring profile, a synonym map, a semantic configuration or CORS settings. These are in-place updates. See the decision table in section 5.
  • You only need a handful of documents refreshed. Use reset docs (preview) or push those documents directly.
  • You are moving to a new search service or region. Aliases work within one service only, so switch at the application configuration level instead. The steps for building and checking the new index still apply.
  • The change is an embedding-model upgrade across many indexes. Use PB-RAG-09-02 first, then this runbook for each index.

3. What you must know first

Master these before you start. The reviewer will ask about each one.

  1. Rebuild triggers. Changing a field's name, data type or the attributes searchable, filterable, sortable or facetable needs a full rebuild. Analysers can only be assigned when the field is created (searchAnalyzer is the exception). Analyser definitions cannot be changed or deleted without a rebuild. Removing a field completely needs a rebuild.
  2. Aliases. An alias maps a name to exactly one index. Queries and document operations can use the alias. You cannot delete an index through an alias, and an indexer's targetIndexName cannot be an alias. An alias update can take up to 10 seconds to propagate. You cannot delete an index while an alias points to it (400 Bad Request). The Serverless Developer tier (preview) does not support index aliases, so Path B needs a Dedicated tier.
  3. Reset semantics. Reset clears the indexer's internal high-water mark. It is passive: you must call Run afterwards. You cannot undo a reset. Reset does not delete orphaned documents from the index. With a skillset and enrichment cache, a reset also refreshes the cache.
  4. Indexer limits. Plan for a 2-hour maximum per indexer run. That is the limit in the default multitenant execution environment, where most skillsets run. The 24-hour limit applies only when the indexer runs in the private execution environment (executionEnvironment set to private, Standard2 or higher) or is an older indexer that cannot use the multitenant environment. On S3 HD and Serverless services, all indexers also share 24 hours of cumulative runtime per service in each 24-hour UTC window. A running indexer cannot be paused or stopped. For long jobs, schedule the indexer every 5 minutes so each run resumes where the last one stopped. This only works with change detection.
  5. Change and deletion detection. Blob indexers detect changes automatically from LastModified. Deletions are not detected unless you set a deletion policy (native blob soft delete or a soft-delete metadata column). The deletion policy must exist from the very first indexer run. If it did not, Microsoft's advice is a new index and a new indexer with the policy in place. Neither soft-delete policy applies to one-to-many parsing modes (jsonArray, jsonLines, delimitedText); delete those documents with the Documents API instead. With index projections, chunks of a changed document are added, updated or deleted on the next run, but chunks of a deleted parent are only removed if a deletion policy detects the deletion. Azure SQL and Cosmos DB sources need change detection configured.
  6. Capacity. During Path B you hold two copies of the data. Storage and vector index quota are per partition, and the vector quota is a hard limit: indexing fails once it is exceeded. Indexing load can throttle queries.
  7. Vectors with stored: false. If a vector field has stored set to false, partial updates must resend the vector. Otherwise the vector is dropped on reindexing.

4. Prerequisites and access

Item Detail
Roles Search Service Contributor (create indexes, indexers, aliases). Search Index Data Contributor for push writes and test queries. Use Microsoft Entra ID, not admin keys, wherever possible. Indexers have service-level permissions: an indexer can write to any index on the service, whatever index-scoped roles you assign.
Service authentication Role-based access must be enabled on the search service (Settings > Keys > Role-based access control, or Both while clients move off keys). With API keys only, the Entra ID token in the commands below is not accepted.
Enrichment cache (only if used) The search service identity needs Storage Blob Data Contributor and Storage Table Data Contributor on a general-purpose v2 storage account.
Tools Azure CLI (az), curl, jq, Python 3.10+ with azure-search-documents 12.x and azure-identity.
Definitions in Git Current index, indexer, data source and skillset JSON, exported in step 1. The new definitions are reviewed in a pull request.
Golden query set 30-100 representative queries with expected top results or relevance labels (see PB-RAG-08-02).
Capacity check Free storage and vector quota for a second full copy (step 2). If not available, add a partition first and agree the cost.
Change approval Change ticket with the cut-over window, the rollback owner and the sign-off owner.
Sandbox evidence A dry-run on a sandbox service with a representative corpus slice, with timings recorded (section 13).
Application readiness The application queries the alias, not the index name. If it does not yet, do step 3 as a separate, earlier change.

Get a data-plane token (Microsoft Entra ID):

export SEARCH_ENDPOINT="https://${SEARCH_SERVICE}.search.windows.net"
export API_VERSION="2026-04-01"
export TOKEN="$(az account get-access-token --scope https://search.azure.com/.default --query accessToken --output tsv)"

5. Decide your approach

5.1 Decision table

Change you need Rebuild needed? Path Notes
Add a new field No In-place schema update, then backfill Existing documents get null until the next indexing run. Backfill with reset and run (A) or mergeOrUpload.
Add, change or delete scoring profiles, synonym maps, semantic configurations, CORS, index description No In-place update No reindex needed. Re-run the golden queries afterwards.
Set retrievable on an existing field No In-place update
Change searchAnalyzer on a field that already has indexAnalyzer No In-place update
Add a new analyser definition (for new fields) No rebuild, but brief downtime B for zero downtime The Create or Update Index API needs allowIndexDowntime=true to add analysers. That takes the index offline for at least a few seconds and can impair writes for minutes.
Change a field's name, type, or searchable, filterable, sortable or facetable attributes Yes B
Assign an analyser to an existing field, or change or delete an analyser, tokenizer, token filter or char filter Yes B
Remove a field completely Yes B Until rebuilt, the field definition and content stay in the index.
Add an existing field to a suggester Yes B
Change embedding model or vector dimensions Yes (new vector field or new index) B Queries and documents must use the same model. Switching index and vectorizer together through the alias keeps them consistent.
Add scalar or binary quantisation New vector profile and new vector field B Microsoft's steps create a new field and reload data.
Change chunk size or overlap (index projections) Not by schema, but every chunk is reprocessed and re-embedded B Index projections delete chunks that no longer exist on the next run and give the remaining chunks new keys, so A would work. While A runs, users see a mix of old and new chunks, and A has no rollback. Chunks of parents deleted without a deletion policy stay in the index.
Change a skill, extraction settings or field mappings only No A, or B if you need rollback Path A reprocesses everything and re-bills every skill call. Reset skills (preview) can limit reprocessing to named skills.
Source content changed for a few known documents No Reset docs (preview) or push resetdocs with documentKeys, then run.
Deletion detection was missing from the first run Yes B with a new indexer Add the deletion policy before the first run of the new indexer.
Move to another service, tier or region Yes Rebuild in the new service, switch in app configuration Aliases do not span services.

5.2 Path comparison

Path A: reset and rerun in place Path B: rebuild and switch with an alias
Downtime None for queries, but results are mixed old and new while it runs None. Users see the old index until the switch
Rollback None. Reset cannot be undone Repoint the alias (seconds)
Capacity One copy Two copies until decommission
Orphan clean-up No Yes, by construction
Cost One full re-enrichment One full re-enrichment, plus temporary extra storage or partitions
Use for Content refresh, skill changes with low risk Schema, analyser, chunking, embedding and deletion-policy changes, and anything high risk

Rule of thumb: if you would be unhappy explaining a bad result to the client for a day, use Path B.

6. Steps

Placeholders: ${SEARCH_SERVICE}, ${RG}, ${ALIAS} (for example kb), ${OLD_INDEX} (kb-v1), ${NEW_INDEX} (kb-v2), ${OLD_INDEXER}, ${NEW_INDEXER}, ${NEW_SKILLSET}, ${DATA_SOURCE}. Never paste keys into tickets or chat.

Snippets used (in the pack's snippets/ folder):

  • ops/reindex/reindex.sh: creates the new index and indexer, enables the indexer, runs it and waits until it has caught up (steps 5 and 6). Refuses to run against the index the alias points to. Exit 0 caught up, 1 failed, 2 bad usage.
  • ops/reindex/switch_alias.py: points the alias at one index, waits 15 seconds and confirms (step 10). The same script rolls back (section 8). Exit 0 confirmed, 1 the alias did not move.
  • defs/after/indexer-kb-v2.json: the new indexer definition used by reindex.sh (step 5). Copy it to defs/after/ and change the names to yours.

Stage 0: Path A only (reset and rerun in place)

Use this short path only when section 5 says A.

  1. Export the current definitions (step 1 below).

Irreversible: reset cannot be undone

A reset makes the indexer reprocess every document, re-bills every skill call, and users see a mix of old and new results until it finishes. It does not remove orphaned documents. There is no quick rollback: see the Path A row in section 8.

  1. Reset the indexer, then run it:
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers('${OLD_INDEXER}')/search.reset?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" -o /dev/null -w "reset HTTP %{http_code}\n"   # expect 204

curl -sS -X POST "${SEARCH_ENDPOINT}/indexers('${OLD_INDEXER}')/search.run?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" -o /dev/null -w "run HTTP %{http_code}\n"     # expect 202
  1. For a few known documents, use reset docs (preview) instead of a full reset:
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${OLD_INDEXER}/resetdocs?api-version=2026-08-01-preview" \
  -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
  -d '{"documentKeys": ["1001", "4452"]}'

Then call Run. Microsoft's guidance is to call Run a second time to continue from the last high-water mark. Status currentState.mode shows indexingResetDocs until all keys are processed.

  1. Monitor with the status call in step 6 below.

Verify: reset returned 204 and run returned 202. When the run ends, lastResult.status is success, itemsFailed is 0 or within the agreed tolerance, and the golden queries pass.

Stage 1: Prepare (Path B)

Step 1. Export and commit the current definitions.

mkdir -p defs/before
for kind in indexes indexers datasources skillsets; do
  curl -sS "${SEARCH_ENDPOINT}/${kind}?api-version=${API_VERSION}" \
    -H "Authorization: Bearer ${TOKEN}" | jq '.' > "defs/before/${kind}.json"
done
git add defs/before && git commit -m "Snapshot search definitions before ${NEW_INDEX} rebuild"

Verify: each file has a value array, and indexes.json contains ${OLD_INDEX}.

Costs money: extra partitions

If this step leads you to add a partition, you pay for the extra search units until you remove them in step 13. Get the cost approved in the change ticket first. To undo, scale the partition count back down.

Step 2. Check capacity.

curl -sS "${SEARCH_ENDPOINT}/servicestats?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" | jq '.counters'

curl -sS "${SEARCH_ENDPOINT}/indexes('${OLD_INDEX}')/search.stats?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" | jq '{documentCount, storageSize, vectorIndexSize}'

Verify: used storage plus storageSize, and used vector quota plus vectorIndexSize, stay below 80% of the service quota (Orion default margin). The new index can be larger than the old one if you add fields, vectors or smaller chunks. If the margin is too small, add a partition (az search service update --partition-count) and wait for it to finish before you continue. Index statistics are collected periodically, so treat them as approximate.

Step 3. Make sure the application queries an alias (one-time).

If no alias exists, create one that points at the current index, then deploy the application change to use it:

curl -sS -X POST "${SEARCH_ENDPOINT}/aliases?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
  -d "{\"name\": \"${ALIAS}\", \"indexes\": [\"${OLD_INDEX}\"]}"

Verify: GET /aliases/${ALIAS} returns "indexes": ["${OLD_INDEX}"], and the application's query logs show traffic through the alias. Do this as its own change, days before the rebuild.

Step 4. Capture the baseline. Run the golden query set against the alias and save the results (top 10 keys and scores per query) to eval/baseline.json. Record the current p50 and p95 SearchLatency and the average ThrottledSearchQueriesPercentage over the last 7 days.

Verify: eval/baseline.json has one entry per golden query, and the latency and throttling baselines are written in the change ticket.

Stage 2: Build the new index (Path B)

Step 5. Prepare the new definitions, data source and skillset.

  • Put the reviewed index definition in Git as defs/after/index-${NEW_INDEX}.json. reindex.sh creates the index in step 6.
  • If you use index projections, create a new skillset whose projections target ${NEW_INDEX}. Name the concrete index, not the alias. The parent key field must be Edm.String, filterable, and not the document key. Do not create a field mapping for the parent key field: it breaks change tracking.
  • Prepare a new indexer definition from snippets/defs/after/indexer-kb-v2.json and save it as defs/after/indexer-${NEW_INDEX}.json. It must target the concrete index, never the alias. Point it at the same data source unless the data source itself changes (for example to add a deletion policy). It has a 5-minute schedule so each run resumes where the last one stopped.
  • If the data source must gain a deletion policy, set it now, before the first run. Native blob soft delete is in the stable API:
{
  "name": "kb-blob-ds-v2",
  "type": "azureblob",
  "credentials": { "connectionString": "ResourceId=/subscriptions/${SUB_ID}/resourceGroups/${RG}/providers/Microsoft.Storage/storageAccounts/${STORAGE};" },
  "container": { "name": "kb-docs" },
  "dataDeletionDetectionPolicy": {
    "@odata.type": "#Microsoft.Azure.Search.NativeBlobSoftDeleteDeletionDetectionPolicy"
  }
}

Native soft delete needs blob soft delete enabled with a retention period much longer than the indexer interval, and blob versioning must be off. Document keys must map to a blob property such as metadata_storage_path.

Verify: both definition files are reviewed and merged in a pull request. GET on the data source and the new skillset (if any) returns 200. GET /aliases/${ALIAS} still points at ${OLD_INDEX}.

Load and cost: full re-enrichment

Building the new index calls every skill again (re-billed) and adds indexing load that can throttle production queries. Watch ThrottledSearchQueriesPercentage (section 7). To stop new runs, set "disabled": true on ${NEW_INDEXER}. Users are not affected by the new index until step 10.

Step 6. Build the new index and wait until it has caught up. In a terminal that can stay open for hours:

bash snippets/ops/reindex/reindex.sh \
  "defs/after/index-${NEW_INDEX}.json" "defs/after/indexer-${NEW_INDEX}.json"

The script stops with exit 1 before changing anything if the alias already points at the new index, the index already exists, or the data source or skillset is missing. To check progress by hand:

curl -sS "${SEARCH_ENDPOINT}/indexers('${NEW_INDEXER}')/search.status?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" \
  | jq '{status, last: .lastResult | {status, itemsProcessed, itemsFailed, errorMessage}}'
  • status is the overall state: running, error or unknown. lastResult.status is inProgress, success, transientFailure or reset.
  • In the multitenant environment each run stops at 2 hours. The 5-minute schedule restarts it from where it stopped.
  • On a large blob container, the first run can show no documents processed for hours while the indexer lists the blobs. This is expected unless the status shows an error.
  • On S3 HD or Serverless services, check the remaining cumulative runtime with servicestats (preview, 2026-08-01-preview, indexersRuntime properties) before a long build.
  • If production queries start throttling (section 7), set "disabled": true on the new indexer. The current run finishes, but no new run starts. Resume off-peak.

Verify: reindex.sh exits 0 with "has caught up", meaning two runs in a row succeeded with 0 items processed. itemsFailed is 0, or each failure is explained in the ticket.

Step 7. Keep push writers in step (only if the application also pushes documents). From step 5 until decommission, every push write must go to both indexes, or you must replay writes made during the build. Missing this is the most common cause of "the new index is missing yesterday's documents".

Verify: look up 5 documents pushed during the build by key in ${NEW_INDEX}. Each one is present with the latest values.

Stage 3: Verify before the switch (Path B)

Step 8. Check parity and quality by querying ${NEW_INDEX} directly, not the alias.

"""Compare document counts and golden-query overlap between two indexes."""
import json
import os

from azure.identity import DefaultAzureCredential
from azure.search.documents import SearchClient

endpoint = os.environ["SEARCH_ENDPOINT"]
credential = DefaultAzureCredential()
old = SearchClient(endpoint, os.environ["OLD_INDEX"], credential)
new = SearchClient(endpoint, os.environ["NEW_INDEX"], credential)

print("documents old/new:", old.get_document_count(), new.get_document_count())

with open("eval/golden_queries.json", encoding="utf-8") as handle:
    queries = json.load(handle)

overlaps = []
for item in queries:
    old_keys = [d["id"] for d in old.search(search_text=item["query"], top=10, select=["id"])]
    new_keys = [d["id"] for d in new.search(search_text=item["query"], top=10, select=["id"])]
    overlap = len(set(old_keys) & set(new_keys)) / max(len(old_keys), 1)
    overlaps.append(overlap)
    if overlap < 0.5:
        print(f"LOW OVERLAP {overlap:.2f}: {item['query']}")

print(f"mean top-10 overlap: {sum(overlaps) / len(overlaps):.2f}")

Adapt the query to your real query shape (hybrid, semantic, filters), using PB-RAG-08-02. Low overlap is not always bad: a schema or chunking change is meant to change results. Judge by relevance labels or evaluator scores, not overlap alone.

Pass criteria (agree them in the ticket before you start):

  • Document count matches expectations. Equal for a like-for-like rebuild. Explained difference for chunking changes or orphan removal.
  • Spot lookups of 10 known keys return the expected new fields.
  • Golden-query quality is equal or better than the baseline on the agreed metric.
  • vectorIndexSize and storageSize of the new index are within the capacity plan.
  • p95 query latency on the new index is within 20% of baseline (Orion default).

Verify: every pass criterion is met and recorded in the ticket. If any fails, do not switch: fix the definitions and repeat from step 5 with a new index name.

Stage 4: Cut over (Path B)

Step 9. Start a probe. In a second terminal, run a probe that queries the alias every second and logs the HTTP status, latency and document count (see section 13 for the script). Leave it running until step 11.

Verify: the probe prints one line a second with status 200 and the current document count.

Affects every user of the index

From this step, all queries through ${ALIAS} go to ${NEW_INDEX}. Make sure the rollback owner is present and ${OLD_INDEX} is untouched. To roll back, run the same script with NEW_INDEX set to ${OLD_INDEX} (section 8).

Step 10. Repoint the alias. One call, one index in the array:

# Needs SEARCH_ENDPOINT, ALIAS and NEW_INDEX exported in this shell.
python snippets/ops/reindex/switch_alias.py

REST equivalent:

curl -sS -X PUT "${SEARCH_ENDPOINT}/aliases/${ALIAS}?api-version=${API_VERSION}" \
  -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
  -d "{\"name\": \"${ALIAS}\", \"indexes\": [\"${NEW_INDEX}\"]}"

Verify: the script prints OK: alias ... points to ${NEW_INDEX} and exits 0. With REST, wait 15 seconds, then GET /aliases/${ALIAS} returns "indexes": ["${NEW_INDEX}"].

Step 11. Verify through the alias. Within 5 minutes:

  • The probe shows no non-2xx responses across the switch.
  • A query through the alias that selects a field that only exists in ${NEW_INDEX} (or a known sentinel document) returns it.
  • The golden queries through the alias match the step 8 results.
  • Application dashboards show normal answer rates and no rise in "no results" or refusal rates.

Record the switch time in the ticket. Stop the probe.

Verify: all four checks pass. If any fails, roll back now (section 8).

Stage 5: Watch and decommission (Path B)

Step 12. Watch window (24-72 hours). Keep ${OLD_INDEX} and, if capacity allows, its indexer running, so a rollback serves fresh data. Watch the alerts in section 7.

Verify: no rollback trigger in section 8 fired during the window, and the sign-off owner records the sign-off in the ticket.

Irreversible: deletes the old index and its data

Deleting ${OLD_INDEX} permanently removes its documents and vectors, and ends the quick rollback. Only the senior who signed off may approve it. After this point, rollback means a rebuild from defs/before (section 8).

Step 13. Decommission after sign-off.

  1. Set "disabled": true on ${OLD_INDEXER}.
  2. Confirm no alias points at ${OLD_INDEX}: GET /aliases.
  3. Take a final export of the old definitions (step 1) for the record.
  4. Delete the old indexer, the old skillset (if separate) and ${OLD_INDEX}.
  5. If you added a partition only for the rebuild, scale back down and confirm the billing change.

Verify: servicestats shows storage and vector usage back to the planned level.

7. Monitoring and alerts

Thresholds are Orion starting defaults. Microsoft leaves thresholds to you, so tune them against your own baseline from step 4.

Signal Source Alert when Action
ThrottledSearchQueriesPercentage Platform metric (average) Above 1% for 5 minutes during the build, or above the 7-day baseline Disable the new indexer's schedule. Resume off-peak or add replicas.
SearchLatency Platform metric (average, seconds) Above 1.5 times the baseline for 10 minutes Check indexing load first. If it persists after the switch, roll back.
DocumentsProcessedCount with dimension Failed = true Platform metric (total), filter by IndexerName More than 0 in 15 minutes for the new indexer Read lastResult.errors from the status API. Fix and rerun.
Indexer status Status API polled by a pipeline or Logic App status = error, or lastResult.status = transientFailure twice in a row Page the on-call engineer. See known issues.
IndexVectorUsage and IndexStorageUsage Platform metrics (maximum, bytes), by IndexName Total above 80% of quota Stop indexing. Add a partition or compress vectors before you continue. The vector quota is a hard limit.
Non-2xx query responses via the alias Resource logs (AzureDiagnostics) Any rise after the switch Roll back (section 8).
Remaining cumulative indexer runtime (S3 HD and Serverless only) indexersRuntime in servicestats (preview, 2026-08-01-preview), polled by a pipeline Less than the time the build still needs in the current 24-hour UTC window Pause other indexers on the service, or plan the rest of the build for the next window.

Create a metric alert with the Azure CLI:

SEARCH_ID="$(az search service show --name "${SEARCH_SERVICE}" --resource-group "${RG}" --query id --output tsv)"

az monitor metrics alert create \
  --name "search-throttling-during-reindex" \
  --resource-group "${RG}" \
  --scopes "${SEARCH_ID}" \
  --condition "avg ThrottledSearchQueriesPercentage > 1" \
  --window-size 5m \
  --evaluation-frequency 1m \
  --severity 2 \
  --action "${ACTION_GROUP_ID}" \
  --description "Queries are being throttled while the index is rebuilt"

Resource logs must be sent to a Log Analytics workspace through a diagnostic setting (category OperationLogs). They land in AzureDiagnostics. Column names follow the Microsoft examples. Check them in your workspace before you rely on them.

Latency and throttling trend from platform metrics:

AzureMetrics
| where ResourceProvider == "MICROSOFT.SEARCH"
| where MetricName in ("SearchLatency", "ThrottledSearchQueriesPercentage", "SearchQueriesPerSecond")
| summarize avg(Average), max(Maximum) by MetricName, bin(TimeGenerated, 5m)
| order by TimeGenerated asc

Query results by status code around the switch:

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.SEARCH" and OperationName == "Query.Search"
| where TimeGenerated > ago(2h)
| summarize queries = count(), p95_ms = percentile(DurationMs, 95) by bin(TimeGenerated, 1m), tostring(resultSignature_d), IndexName_s
| order by TimeGenerated asc

Push-indexing failures (for push writers in step 7):

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.SEARCH" and OperationName == "Indexing.Index"
| where TimeGenerated > ago(24h)
| summarize calls = count(), docs = sum(Documents_d) by IndexName_s, tostring(resultSignature_d), bin(TimeGenerated, 15m)
| order by TimeGenerated desc

Check in the sandbox whether IndexName_s records the alias name or the index name for queries sent through an alias. Write the answer in the validation record.

8. Rollback

Situation Rollback Time
Path B, after the switch Run NEW_INDEX="${OLD_INDEX}" python snippets/ops/reindex/switch_alias.py. It waits 15 seconds and confirms the alias. Verify with the probe. Under 5 minutes
Path B, before the switch Nothing to roll back for users. Disable the new indexer, then delete the new index, indexer and skillset when convenient. Minutes
Path B, after decommission No quick rollback. Rebuild the old version from defs/before using this runbook. This is why the watch window exists. Hours to days
Path A, in progress or done A reset cannot be undone. Restore the previous indexer and skillset definitions from Git, then reset and run again. Results stay mixed until it finishes. Same as the original run
In-place schema change gone wrong Fields cannot be removed in place. Use Path B to build a corrected index. Hours

Rollback trigger (any one): non-2xx query rate above baseline, a golden-query regression that the business owner rejects, a p95 latency rise of more than 50% for 15 minutes, or a missing-content report confirmed by a lookup.

9. Known issues and fixes

Symptom Cause Fix
Alias create or update is rejected More than one index in the indexes array Put exactly one index name in the array.
Deleting the old index returns 400 Bad Request An alias still maps to it Repoint or delete the alias first. Wait at least 10 seconds after the alias update.
Indexer creation fails or writes to the wrong place targetIndexName set to the alias Indexers and index projections must name the concrete index.
Indexer stops after about 2 hours with work left Indexers in the multitenant environment run with a 2-hour maximum Keep a 5-minute schedule and change detection on. Each run resumes from the high-water mark.
First run shows 0 documents processed for hours on a large blob container The indexer is still listing the blobs Wait unless the status shows an error. To go faster, split the container into folders with parallel indexers into the same index.
Indexers on an S3 HD or Serverless service stop starting runs All indexers share 24 hours of cumulative runtime per service in each 24-hour UTC window Check indexersRuntime in servicestats (preview). Plan the build across windows.
Commands return 401 or 403 with a valid Entra ID token Role-based access is not enabled on the service, or the role assignment has not taken effect yet Enable Role-based access control or Both under Settings > Keys. Role assignments can take 5-10 minutes to apply.
"Another indexer invocation is currently in progress" Indexers are single-instance Wait for the current run. Do not call Run in a loop.
Chunks of deleted source files still appear The parent was deleted with no deletion policy; index projections only remove chunks of deleted parents when a dataDeletionDetectionPolicy detects the deletion Use Path B with the deletion policy set from the new indexer's first run, or delete the chunks with the Documents API.
Deleted source files still searchable No deletion policy at the first run, or a one-to-many parsing mode, which soft-delete policies do not cover Build a new index with a new indexer and the deletion policy set from the first run. For one-to-many parsing modes, delete the documents with the Documents API.
Chunk keys change after a source document changes Index projections give the remaining chunks a new hash in their key when the parent changes Expected. Do not store chunk keys outside the index; look chunks up by the parent key field.
Restored soft-deleted blob is not reindexed LastModified did not change on restore Re-save the blob's metadata to update LastModified, then run.
Vectors vanish after a partial update Vector field has stored: false and the update omitted it Always send the vector in partial updates for such fields.
207 or 503 responses on push, or throttled queries during the build Service under indexing load Retry with exponential back-off, use smaller batches (up to 1,000 documents or about 16 MB), index off-peak, or add replicas.
"The cognitive service for skill ... has been throttled", or a 429 from a skill or vectorizer Quota of the Azure OpenAI or Foundry resource the skill calls, not the search service Lower batchSize, raise the quota of the Foundry or Azure OpenAI resource, then run again.
Indexing fails once vectors reach the limit Vector index quota is a hard limit per partition Add partitions, use quantisation or narrower types, or free space. Plan this in step 2.
422 "index is temporarily unavailable ... allowIndexDowntime" Someone updated the index with allowIndexDowntime=true Retry after a short wait. Use Path B for analyser additions in production.
Documents missing although the indexer reports success Change-tracking high-water mark is wrong (for example a future date), or a field mapping changed the document shape Compare initialTrackingState and finalTrackingState in the status response. Check mappings. Look up the key directly.
Processed counts and skill bills higher than the number of changed documents Indexers use a 30-second look-back buffer, so rapid on-demand runs reprocess documents Space on-demand runs at least 5 minutes apart, or rely on the schedule.
Document count lower than expected right after indexing Statistics are collected periodically, and deletes are physically removed by a background process Wait a few minutes, then use $count queries and lookups rather than portal statistics.

10. Cost and capacity notes

  • Second copy of the data. Path B needs storage and vector quota for two indexes until decommission. If that forces an extra partition, you pay for the extra search units for the build and watch window. Check the current price before approval.
  • Re-enrichment is re-billed. A full run calls every skill again: Document Intelligence or Content Understanding extraction, and embedding calls to Azure OpenAI for every chunk. Estimate it as corpus tokens multiplied by the embedding price, plus extraction pages multiplied by the extraction price. The Document Layout skill is billable above 20 documents per indexer per day. The Content Understanding skill has no free allotment.
  • Enrichment cache (preview) can avoid repeat skill calls for unchanged documents on Path A. It needs a general-purpose v2 storage account, and the search service identity needs Storage Blob Data Contributor and Storage Table Data Contributor on it. It is not a backup, and Microsoft does not recommend preview features for production. For large data sources with long-running skills or repeated interruptions, a cache can increase total reprocessing. Do not expect a new indexer on Path B to reuse the old indexer's cache.
  • Throughput. One indexer job runs per search unit. More partitions or replicas raise parallelism for multiple indexers, not for one indexer. Splitting the source into folders with parallel indexers into the same index is the documented way to go faster.
  • Semantic ranker is billed per query when queryType=semantic has a non-empty search string. Golden-query runs count.
  • Time. Measure documents per hour in the sandbox and scale linearly as a first estimate. Do not quote durations to the client from this runbook.

11. AWS and open-source equivalents

Platform Equivalent Key differences
Amazon OpenSearch Service and OpenSearch Index aliases. POST _aliases with remove and add actions in one request. Creating and updating aliases is atomic. _reindex copies documents between indexes. Mapping changes to existing fields also need a new index. If embeddings change, re-ingest from source rather than _reindex, because _reindex copies the old vectors. An alias can point to several indexes, so be explicit.
Amazon Bedrock Knowledge Bases Data source sync (StartIngestionJob) is incremental: it processes added, modified and deleted files since the last sync. There is no alias. The embedding model is configured when the knowledge base is created. For a model or chunking change, plan a new knowledge base and switch the application configuration, and check the current docs.
PostgreSQL with pgvector Build a new table or column, then CREATE INDEX CONCURRENTLY to avoid blocking writes. Switch with a view or a rename in one transaction. You manage the switch yourself. Watch pg_stat_progress_create_index. HNSW on the vector type supports up to 2,000 dimensions. Size maintenance_work_mem to fit the graph without exhausting server memory.

OpenSearch switch:

{
  "actions": [
    { "remove": { "index": "kb-v1", "alias": "kb" } },
    { "add": { "index": "kb-v2", "alias": "kb" } }
  ]
}

Send it as POST _aliases.

pgvector switch:

CREATE TABLE kb_chunks_v2 (LIKE kb_chunks_v1 INCLUDING DEFAULTS);
-- load and embed into kb_chunks_v2, then:
CREATE INDEX CONCURRENTLY kb_chunks_v2_embedding_hnsw
    ON kb_chunks_v2 USING hnsw (embedding vector_cosine_ops);

BEGIN;
CREATE OR REPLACE VIEW kb_chunks AS SELECT * FROM kb_chunks_v2;
COMMIT;

CREATE OR REPLACE VIEW only works if the new table has the same columns in the same order. If the columns change, drop and recreate the view in the same transaction.

12. Checklist

Pre-flight

  • Change ticket approved, with cut-over window, rollback owner and sign-off owner named.
  • Path chosen using section 5 and written in the ticket with the reason.
  • Current definitions exported and committed (step 1).
  • New index, skillset, indexer and data source JSON reviewed in a pull request.
  • Capacity check passed with at least 20% headroom (step 2).
  • Application queries the alias (step 3), confirmed in logs.
  • Baseline golden-query results and latency recorded (step 4).
  • Deletion policy present on the data source before the new indexer's first run, if needed.
  • Push writers dual-write or have a replay plan (step 7).
  • Alerts in section 7 created and tested with a test notification.
  • Sandbox dry-run done, with timings in the validation record.
  • Preview features (reset docs, reset skills, resync, enrichment cache) noted in the ticket if used.

Post-cut-over

  • Alias shows exactly [${NEW_INDEX}].
  • Probe showed no non-2xx responses across the switch.
  • Sentinel or new-field query through the alias succeeded.
  • Golden queries through the alias match the pre-switch run.
  • Latency and throttling within thresholds for the first hour.
  • Old index and indexer kept for the watch window.
  • Watch window ended without a rollback trigger, and sign-off recorded.
  • Old objects deleted, temporary partitions removed, cost change confirmed.
  • Runbook updated with anything that surprised you.

13. Validation record

Pending. Filled in by the dry-runner (see docs/validation-protocol.md).

Sandbox scenario:

  1. A Standard-tier sandbox service with a blob container of at least 2,000 mixed documents (PDF, DOCX, HTML), an indexer, a skillset with embeddings, and alias kb pointing at kb-v1.
  2. A load script sends 2 queries per second through the alias for the whole exercise.
  3. Change requested: switch a filterable attribute on an existing field, change chunk size, and add a native blob soft delete policy. All three force Path B.
  4. Delete 20 blobs during the build. Restore one soft-deleted blob.
  5. Run steps 1-13, including a deliberate rollback after step 11 and a second switch forward.

Probe used in steps 9-11:

"""Query an alias once a second and log status and timing."""
import datetime as dt
import os
import time

import requests
from azure.identity import DefaultAzureCredential

endpoint = os.environ["SEARCH_ENDPOINT"]
alias = os.environ["ALIAS"]
url = f"{endpoint}/indexes/{alias}/docs/search?api-version=2026-04-01"
credential = DefaultAzureCredential()

while True:
    token = credential.get_token("https://search.azure.com/.default").token
    started = time.perf_counter()
    response = requests.post(
        url,
        json={"search": "*", "top": 1, "count": True},
        headers={"Authorization": f"Bearer {token}"},
        timeout=10,
    )
    elapsed_ms = (time.perf_counter() - started) * 1000
    count = response.json().get("@odata.count") if response.ok else None
    print(f"{dt.datetime.now(dt.timezone.utc).isoformat()} {response.status_code} {elapsed_ms:.0f}ms count={count}")
    time.sleep(1)

Pass criteria:

  • Zero non-2xx responses from the probe across both switches and the rollback.
  • After the switch, the 20 deleted documents are absent from kb-v2, and the restored blob is present after its metadata is re-saved.
  • No orphaned chunks: the count of chunk documents matches the expected count for the new chunk size within 1%.
  • Rollback completes in under 5 minutes from decision to verified.
  • Every command in this runbook ran unchanged, or the runbook was corrected in the same pull request.

Record

Item Detail
Playbook PB-RAG-09-01 at commit [hash]
Dry-run by [Name] (must not be author or reviewer)
Date YYYY-MM-DD
Sandbox [Resource group or tag, tier and region]
Scenario [Scenario followed]
Time taken [Hands-on time] hands-on, [waiting time] waiting
Result [PASS / PASS WITH NOTES / FAIL]
Notes 1. [Step number: defect or explanation; blocking/non-blocking; fixed/logged]
Signed off by [Reviewer name and date]
Corpus size [Documents, chunks, GB]
API versions used [API versions used during dry-run]
Path tested B (plus A for reset docs)
Build timings [Build duration and documents per hour]
Switch and rollback timings [Switch and rollback timings]
Probe result [Non-2xx count across switch and rollback]
IndexName_s shows alias or index? [Answer from section 7: records alias or index name]

14. Sources

Checked on 2 November 2026.

  • PB-RAG-09-02: upgrade the embedding model first, then use this runbook for each index.
  • PB-RAG-08-02: build the golden query set and relevance labels used in steps 4 and 8.