Deletion and permission change propagation checklist¶
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¶
Use this checklist when a document is withdrawn, a legal takedown arrives, or document-level access permissions change in a source repository. It ensures updates propagate across every storage layer: the source document store, every chunk in the Azure AI Search index, and every application cache in front of the model. Skipping any layer causes withdrawn or restricted content to leak into generated answers.
Facts checked against official documentation on 3 and 4 November 2026. Data-plane operations use stable REST API 2026-04-01. Resync with permissions, query-time ACL enforcement and Reset Documents are preview, under API version 2026-08-01-preview. Do not rely on preview features in production without an agreed fallback.
2. When to use and when not to¶
Use this checklist when:
- A document is withdrawn or archived, or a GDPR right-to-erasure or copyright takedown arrives.
- Source document permissions change, including access control lists, Entra ID groups, or classification tags.
- You verify that an indexer run or direct purge removed all vector chunks from the search index.
- You perform a scheduled access review to confirm restricted content does not leak into answers.
Do not use it when:
- You must apply an immediate emergency permission revocation. Follow PB-RAG-04-03 first to restrict queries, then verify with this checklist.
- You are triaging an active permission leakage incident across users. Follow PB-RAG-04-04 instead.
- You are configuring recurring indexer schedules or deletion detection policies. Follow PB-RAG-09-04 or PB-RAG-05-03 instead.
- You are rebuilding an entire index or modifying its schema. Follow PB-RAG-09-01 instead.
3. What you must know first¶
- Indexers do not track object deletions automatically. Data sources require a deletion detection policy (
dataDeletionDetectionPolicy): native blob soft delete (NativeBlobSoftDeleteDeletionDetectionPolicy) or a soft-delete column (SoftDeleteColumnDeletionDetectionPolicy). Without a policy, deleted source files leave orphan documents in the index. - Deletion policies must exist on initial indexer creation. Documents deleted before a policy was configured remain as index orphans, even after an indexer reset. Native blob soft delete is unsupported when blob versioning is enabled. The soft-delete retention window must exceed the indexer run interval.
- One parent file maps to many vector chunks. With index projections (skillset chunking), the indexer deletes a file's chunks only when a deletion detection policy detects that the file was deleted. Without a policy, you must delete the chunks yourself. With one-to-many parsing modes (JSON lines, JSON array or delimited text), neither soft-delete policy applies, so you must always delete those documents with a delete request.
- Security filters differ from native ACL trimming. Security filters (generally available,
2026-04-01) evaluate application-managed group identifiers stored in a collection field (${ACL_FIELD}). Native ACL trimming (preview,2026-08-01-preview) ingests hierarchical ADLS Gen2 permissions and evaluates them against the user bearer token in thex-ms-query-source-authorizationheader. Both require index updates before permission changes reflect in search results. - Resync with permissions (preview) targets native ACLs. Updating an ADLS Gen2 ACL does not modify a blob's
LastModifiedtimestamp, so normal incremental indexer runs skip the file. The Resync REST API with"options": ["permissions"]marks ACL metadata for re-ingestion without re-extracting text or regenerating vector embeddings. - Deletes are idempotent and background-purged. A delete request returns HTTP 200 even if the target key does not exist. Physical purge runs asynchronously in the background, so document counts and storage usage metrics lag behind. Verify deletion by looking up the key (expect HTTP 404) and querying the parent source filter (expect zero documents), never by total document counts.
- Caches operate independently of search indexes. Semantic vector caches, prompt response caches, API gateways, and LLM conversational session state continue returning cached content derived from deleted documents. You must purge caches explicitly during takedowns.
Prerequisites and access¶
| Requirement | Detail |
|---|---|
| Role assignments | Search Index Data Contributor to submit delete and merge batches. Search Service Contributor to run indexers and trigger resync. Storage Blob Data Contributor to update blob metadata or soft-delete source files. Storage Blob Data Owner to change ADLS Gen2 ACLs on files you do not own. |
| Tooling | Azure CLI (az), curl, jq, Python 3 with azure-search-documents and azure-identity. |
| Index fields | ${PARENT_FIELD} is filterable and holds the same value as ${SOURCE_PATH} on every chunk. ${KEY_FIELD} and ${ACL_FIELD} are retrievable. With index projections, the parent key field holds the parent document's key, which may not be the file path: look up one chunk first. |
| Script assets | Batch chunk deletion script in snippets/ops/delete_by_source.py and REST requests in snippets/ops/requests/resync-permissions.http. |
| Operational checklist | Sign-off artifact in snippets/checklists/deletion-permission-change.md. |
Export operational variables and obtain a data-plane access token:
export SEARCH_ENDPOINT="https://${SEARCH_SERVICE}.search.windows.net"
export INDEX_NAME="${INDEX_NAME}"
export INDEXER_NAME="${INDEXER_NAME}"
export KEY_FIELD="id"
export PARENT_FIELD="source_path"
export TOKEN="$(az account get-access-token --scope https://search.azure.com/.default --query accessToken -o tsv)"
Verify: echo "${TOKEN}" displays a non-empty JSON Web Token.
4. Checklist¶
Stage 1: Request intake and source update¶
- Record ticket details (DevOps engineer). Log the change ticket ID, requester, target source URIs (
${SOURCE_PATH}), compliance deadline (${TAKEDOWN_DEADLINE}), and reason. Attachsnippets/checklists/deletion-permission-change.mdto the ticket. Verify: All ticket fields are populated. - Select the propagation path (RAG Lead). Compare
${TAKEDOWN_DEADLINE}with the scheduled indexer run interval (${INDEXER_SCHEDULE}). If the deadline is shorter than the interval, if the data source has no deletion detection policy, or if the indexer uses a one-to-many parsing mode (JSON lines, JSON array or delimited text), choose direct deletion (Stage 2a). Otherwise, choose indexer-driven deletion (Stage 2b). For permission updates, follow Stage 2c. Verify: The chosen path is recorded in the ticket.
These steps remove content or access at source
Record the current state in the ticket first: the blob's metadata, and its ACLs or group assignments. Rollback: restore the blob while it is still in its soft-delete retention period, set the metadata flag back, or put back the recorded permissions. Then re-index: see the rollback items in Stage 2c (permissions) and Stage 4 (deleted files).
- Native blob soft-delete: delete the blob at source (DevOps engineer). For native soft-delete policies, delete the blob in Azure Storage so it enters the soft-deleted state. Verify: Azure Storage confirms the blob status is soft-deleted.
- Custom metadata policy: flag the file at source (DevOps engineer). For metadata policies, set
${SOFT_DELETE_COLUMN}to${SOFT_DELETE_MARKER}. Do not delete the file yet: the indexer must read the flag first. Delete the file only after the indexer has run and the Stage 4 checks pass (see Stage 4). Verify: Blob metadata shows the updated deletion marker. - Permission changes: update source access controls (DevOps engineer). Update the Entra ID group assignments, ADLS Gen2 ACLs, or container classification tags at source. Do not alter indexer managed identity access to the storage account. Verify: Azure Storage ACL check confirms the updated permissions.
Stage 2a: Direct deletion (urgent takedown or chunk projections)¶
Irreversible action: purges vector chunks directly
Deleting documents directly from the search index cannot be undone via API rollback. Confirm the chunk key list contains only chunks derived from the target source document before executing batch deletion. If chunks are deleted by mistake, restore the file at source if needed, re-save its metadata (this updates LastModified) and run the indexer. Reset Documents (preview 2026-08-01-preview) is an alternative.
- List every chunk of the file, without deleting (DevOps engineer). Run
snippets/ops/delete_by_source.pyin dry-run mode. It writes one key per line and deletes nothing:
python snippets/ops/delete_by_source.py --dry-run "${SOURCE_PATH}" > chunks_to_delete.txt
wc -l < chunks_to_delete.txt
Without Python, list the keys with a search request. A search returns 50 results unless you set top, and at most 1,000 per page, so compare the count with the number of keys listed:
jq -n --arg f "${PARENT_FIELD} eq '${SOURCE_PATH}'" --arg s "${KEY_FIELD}" '{search: "*", filter: $f, select: $s, count: true, top: 1000}' |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/search?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d @- > chunks.json
jq -r --arg k "${KEY_FIELD}" '.value[] | .[$k]' chunks.json > chunks_to_delete.txt
jq '{count: ."@odata.count", listed: (.value | length)}' chunks.json
Verify: chunks_to_delete.txt holds at least one key, and a lookup of a few keys shows ${PARENT_FIELD} equal to ${SOURCE_PATH}. Without Python, count and listed are equal; if count is more than 1,000, use the script. If no keys are found, check the Index fields row in Prerequisites before you decide there is nothing to delete.
- Delete exactly the listed keys (DevOps engineer). Delete only the keys you checked, in batches of at most 1,000 documents:
python snippets/ops/delete_by_source.py --keys-file chunks_to_delete.txt
Without Python, send the listed keys as one delete batch (at most 1,000 keys and 16 MB per request):
jq -R -s --arg k "${KEY_FIELD}" '{value: [splits("\r?\n") | select(length > 0) | {"@search.action": "delete", ($k): .}]}' chunks_to_delete.txt |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/index?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d @- |
jq '[.value[] | select(.status | not)] | length'
Verify: the script prints Successfully deleted N of N chunks, where N is the number of keys in chunks_to_delete.txt. Without Python, the output is 0 (no failed items). A 207 response means some items failed; inspect each item's statusCode (see Known issues). A 200 does not prove the key existed, so always run the Stage 4 checks.
Stage 2b: Indexer-driven deletion (scheduled batch sync)¶
- Trigger on-demand indexer run (DevOps engineer). If indexer schedule allows, start an on-demand indexer run:
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/run?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}"
Verify: HTTP 202 Accepted response. If the call fails with "Another indexer invocation is currently in progress", wait for the running invocation to finish before you start it again.
- Confirm indexer run completion (DevOps engineer). Query indexer status:
curl -sS "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/status?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" | jq '.lastResult | {status, itemsProcessed, itemsFailed}'
Verify: status is "success" and itemsFailed is 0. The status has no count of deleted documents, so confirm the deletion with the Stage 4 checks.
Stage 2c: Permission change propagation¶
Use the merge items below only when your application writes ${ACL_FIELD} itself. If an indexer fills it from the source, change the permission at source and re-index the file instead: the indexer overwrites manual changes the next time the file changes.
A merge can cut access for the wrong people
A merge replaces the whole ${ACL_FIELD} list. It does not add to it or remove from it. Send the complete new list of groups, not only the group that changed. Save the old lists first (next item). Rollback: merge the saved lists back (the item after the merge), then repeat the access test in Stage 4.
- Security filters: save the current group lists (DevOps engineer). Save the key and
${ACL_FIELD}of every chunk of the file:
jq -n --arg f "${PARENT_FIELD} eq '${SOURCE_PATH}'" --arg s "${KEY_FIELD},${ACL_FIELD}" \
'{search: "*", filter: $f, select: $s, count: true, top: 1000}' |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/search?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d @- > acl_before.json
jq --arg a "${ACL_FIELD}" '{count: ."@odata.count", saved: (.value | length), missing_acl: ([.value[] | select(has($a) | not)] | length)}' acl_before.json
Verify: count and saved are equal and more than 0, and missing_acl is 0. If missing_acl is more than 0, ${ACL_FIELD} is not retrievable: stop, and get the old lists from the source system instead. If count is more than 1,000, stop: this step handles at most 1,000 chunks per file, so ask the RAG Lead how to proceed.
- Security filters: merge the complete new list into every chunk (DevOps engineer).
${NEW_GROUP_IDS}is the complete new list as a JSON array, for example["${GROUP_ID_1}", "${GROUP_ID_2}"]:
jq --arg k "${KEY_FIELD}" --arg a "${ACL_FIELD}" --argjson g "${NEW_GROUP_IDS}" \
'{value: [.value[] | {"@search.action": "merge", ($k): .[$k], ($a): $g}]}' acl_before.json |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/index?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d @- |
jq '[.value[] | select(.status | not)] | length'
Verify: the output is 0 (no failed items). A lookup of one chunk shows exactly the new list in ${ACL_FIELD}.
- Only if the access test in Stage 4 fails: put the old lists back (DevOps engineer). Merge the saved lists back into every chunk. Also undo the change at source (Stage 1), or the next indexer run or push brings the new list back:
jq --arg k "${KEY_FIELD}" --arg a "${ACL_FIELD}" \
'{value: [.value[] | {"@search.action": "merge", ($k): .[$k], ($a): .[$a]}]}' acl_before.json |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/index?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d @- |
jq '[.value[] | select(.status | not)] | length'
Verify: the output is 0, and the Stage 4 access test gives the same results as before the change.
- Native ACL updates: resync indexer permissions (DevOps engineer). For ADLS Gen2 native permissions (preview
2026-08-01-preview), execute the requests insnippets/ops/requests/resync-permissions.http:
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/resync?api-version=2026-08-01-preview" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"options": ["permissions"]}'
Verify: HTTP 204 No Content response.
- Execute two-phase indexer runs for resync (DevOps engineer). Run the indexer once to process the resync request, verify status transitions to
indexingResync, then run it a second time once status reverts toindexingAllDocs:
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/run?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}"
Verify: Get Indexer Status with api-version=2026-08-01-preview shows currentState.mode as indexingResync during the first run and indexingAllDocs after it, and lastResult.itemsFailed is 0.
Stage 3: Cache invalidation¶
- Purge answer and semantic caches (GenAI engineer). Invalidate semantic caches and prompt completion caches storing answers derived from the withdrawn document IDs. Verify: Repeating a query that previously returned the content yields a fresh answer without the withdrawn content.
- Purge API gateway and reverse proxy caches (DevOps engineer). Clear response cache keys on Azure API Management, Azure Front Door, or reverse proxies. Verify: Proxy returns cache-miss headers (
x-cache: MISS) on subsequent queries. - Invalidate conversational session memory (GenAI engineer). Expire active chat sessions and agent conversation threads that contain context citations from the affected file. Verify: A new session query returns answers without referencing the purged content.
Stage 4: Verification, rollback readiness, and ticket closure¶
- Look up every deleted chunk key (GenAI engineer). Execute a direct key lookup for each purged chunk:
curl -sS -i "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs('${CHUNK_KEY}')?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}"
Verify: HTTP 404 Not Found response for every deleted chunk key.
- Execute parent filter query (GenAI engineer). Query the index filtering by the parent source path:
curl -sS "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs?api-version=2026-04-01&\$filter=${PARENT_FIELD}+eq+'${SOURCE_PATH}'&\$count=true" \
-H "Authorization: Bearer ${TOKEN}" | jq '{count: ."@odata.count", results: .value}'
Verify: Output confirms count: 0 and results: [].
- Search as a user who lost access and as a user who kept it (GenAI engineer). For permission changes only. Limit each query to the changed file, so other files do not change the count. Do not test with your own token alone: with native ACLs, a query without a user token returns only public documents, so it shows 0 and looks like a pass.
With security filters, send the test user's groups in the same filter the application builds (PB-RAG-04-02). ${USER_GROUP_IDS} is the user's group IDs, separated by commas:
jq -n --arg f "${PARENT_FIELD} eq '${SOURCE_PATH}' and ${ACL_FIELD}/any(g: search.in(g, '${USER_GROUP_IDS}', ','))" \
'{search: "*", count: true, top: 0, filter: $f}' |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/search?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d @- | jq '."@odata.count"'
With native ACLs (preview 2026-08-01-preview), send the test user's own token in x-ms-query-source-authorization. Get ${USER_TOKEN} by signing in as the test user and running the token command from Prerequisites:
jq -n --arg f "${PARENT_FIELD} eq '${SOURCE_PATH}'" '{search: "*", count: true, top: 0, filter: $f}' |
curl -sS -X POST "${SEARCH_ENDPOINT}/indexes/${INDEX_NAME}/docs/search?api-version=2026-08-01-preview" \
-H "Authorization: Bearer ${TOKEN}" -H "x-ms-query-source-authorization: ${USER_TOKEN}" \
-H "Content-Type: application/json" -d @- | jq '."@odata.count"'
Verify: run each query twice. The user who lost access gets 0. The user who kept access gets the file's chunk count (more than 0). If both get 0, the change removed too much access: stop and put the old access back (Stage 2c).
- Custom metadata policy only: delete the file at source (DevOps engineer). After the checks above pass, delete the flagged file in Azure Storage. A flagged file left at source comes back if the flag is ever cleared. Verify: the file no longer exists in the container.
- Monitor subsequent scheduled indexer run (DevOps engineer). Observe the indexer execution following the takedown window. Verify: The indexer run completes with
status: "success", the withdrawn document is not reintroduced into the index, and the file is gone, or still flagged, at source. - Rollback verification if takedown was disputed or executed in error (DevOps engineer). If the source document was deleted or flagged in error, restore the blob in Azure Storage, or set the metadata flag back. Then re-index it: re-save the blob's metadata (this updates
LastModified) and run the indexer. Reset Documents (preview2026-08-01-preview) is an alternative. Use the file's ID in the data source (datasourceDocumentIds), not the old chunk keys: the chunks were deleted, and keys that do not exist are ignored. Reset the file, run the indexer, then run it a second time to continue from the last high-water mark:
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/resetdocs?api-version=2026-08-01-preview" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"datasourceDocumentIds": ["'"${DATASOURCE_DOCUMENT_ID}"'"]}'
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/run?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}"
# When that run has finished, run once more to continue from the last high-water mark
curl -sS -X POST "${SEARCH_ENDPOINT}/indexers/${INDEXER_NAME}/run?api-version=2026-04-01" \
-H "Authorization: Bearer ${TOKEN}"
Verify: the reset returns HTTP 204 No Content, and the Stage 4 filter query for ${SOURCE_PATH} returns the file's chunks again (count more than 0).
- Sign off and close ticket (RAG Lead). Complete the sign-off table in
snippets/checklists/deletion-permission-change.md. Attach the 404 lookup logs, zero-count query evidence, and cache invalidation confirmations to the change ticket. Verify: Change ticket is marked resolved within${TAKEDOWN_DEADLINE}.
5. Known issues and fixes¶
| Symptom | Likely cause | Fix |
|---|---|---|
| A deleted document reappears in the index after a later indexer run | The source file was not removed or flagged at source; re-indexing picked it up | Delete or flag the file in Azure Storage, then purge its chunks from the index using snippets/ops/delete_by_source.py. |
| Deleted source files remain in the index after indexer execution | No deletion detection policy configured, or policy was added after the first indexer run | Add the deletion detection policy. Purge existing orphan chunks by key, or build a new index with a new indexer that has the policy from the first run (PB-RAG-09-01). |
| The indexer never purges a soft-deleted blob | Blob soft-delete retention expired before indexer ran, or Azure Blob versioning is active | Increase storage account soft-delete retention beyond the indexer run interval. Native soft-delete policies are unsupported when blob versioning is enabled; switch to metadata column policy. |
| A restored document is missing from subsequent indexer runs | Restoring a soft-deleted blob does not update its LastModified timestamp; the indexer skips it |
Touch the blob metadata in storage to refresh LastModified, or invoke Reset Documents (preview 2026-08-01-preview) with the file's data source ID (datasourceDocumentIds) before running the indexer. |
| Chunks of a deleted file remain searchable | No deletion detection policy, or a one-to-many parsing mode, which the soft-delete policies do not cover | Locate child chunks using filter=${PARENT_FIELD} eq '${SOURCE_PATH}' and delete all child keys directly via Stage 2a. |
| Withdrawn content continues appearing in answers after chunk deletion | Upstream answer cache, semantic vector cache, or conversational thread memory serves stale answers | Purge semantic cache, flush API reverse proxy caches, and terminate active conversational sessions (Stage 3). |
| Batch delete operation returns HTTP 207 Multi-Status | Partial failure within the document batch (e.g. throttling or transient index partition unavailability) | Inspect individual statusCode fields in the response array. Retry HTTP 503 (service too busy) and HTTP 422 (index temporarily unavailable) with exponential back-off; correct HTTP 400 invalid key requests. |
| Starting the indexer fails with "Another indexer invocation is currently in progress" | A scheduled or previous on-demand indexer run is actively executing | Wait for the active run to finish using Get Indexer Status, then reissue the run request. |
| A restricted user still retrieves content after permission update | Permission collection field was not updated across all child chunks, or native ACL resync was not executed | Update ${ACL_FIELD} in every child chunk, or invoke Resync with permissions option and trigger two indexer runs (Stage 2c). |
| Document count and storage usage metrics do not decrease immediately | Physical deletion in Azure AI Search runs asynchronously in the background every few minutes | Rely on direct key lookups (expect HTTP 404) and filter queries (expect 0 count) rather than aggregate service document counts. |
6. Monitoring and alerts¶
The thresholds below are illustrative team defaults, not platform recommendations. Tune them against a measured baseline. Set ${THROTTLE_THRESHOLD} to the value your team agrees for each system.
| Signal | Source | Alert when | Action |
|---|---|---|---|
| Takedown ticket SLA | Service desk API | Change ticket open past ${TAKEDOWN_DEADLINE} |
Escalate ticket to RAG Lead and initiate Stage 2a direct purge. |
| Orphan chunks | Nightly reconciliation script | Keys exist in index where source blob is missing > 0 | Run direct batch purge via snippets/ops/delete_by_source.py and verify data source deletion policy. |
| Unfiltered queries | Application telemetry | Retrieval query executed without security filter > 0 | Block query path immediately and enforce security filter injection in retrieval module. |
| Failed documents in indexer run | Azure Monitor metric DocumentsProcessedCount with Failed dimension |
Metric value > 0 failed items in indexer run | Inspect Get Indexer Status error array and follow PB-RAG-05-02 to remediate failures. |
| Throttled search queries during bulk purge | Azure Monitor metric ThrottledSearchQueriesPercentage |
Above ${THROTTLE_THRESHOLD} during the takedown window |
Slow down: set a smaller BATCH_SIZE for delete_by_source.py and retry with exponential back-off. |
| Stale cache hit rate | Application telemetry | Cache hit on invalidated document ID > 0 | Flush application cache partition and verify cache key invalidation logic. |
7. AWS and open-source equivalents¶
| Platform | Equivalent | Key differences |
|---|---|---|
| Amazon Bedrock Knowledge Bases | Data source sync (StartIngestionJob) and document deletion API (DeleteKnowledgeBaseDocuments) |
Bedrock sync removes deleted S3 objects on the next incremental ingestion run. For immediate takedowns without waiting for a sync, call DeleteKnowledgeBaseDocuments (Agents for Amazon Bedrock API, at most 10 documents per call) to delete specific documents. Access control uses metadata filters at query time (vectorSearchConfiguration.filter in the Retrieve request). |
| Amazon OpenSearch Service | Delete-by-Query API (POST /<index>/_delete_by_query) and Document-Level Security (DLS) |
Child chunks are deleted with _delete_by_query matching the parent document URI. The Security plugin's document-level security (DLS) sets a query on each role that limits which documents the role can read; the query can use the user's backend roles. |
| PostgreSQL with pgvector | Cascading foreign keys and Row-Level Security (RLS) | Child chunk records are purged automatically upon parent record removal using foreign key constraints with ON DELETE CASCADE. Row-Level Security (CREATE POLICY ... ON chunks FOR SELECT USING (...)) enforces access permissions dynamically at SQL query execution time. |
8. Validation record¶
Pending. Filled in by the dry-runner (see docs/validation-protocol.md).
Sandbox scenario: A dry-run engineer withdraws three sandbox documents in Azure Blob Storage, updates permissions on a fourth document, and follows this checklist. The engineer executes direct batch deletion for one document (Stage 2a), runs indexer-driven sync for two documents (Stage 2b), triggers native ACL resync for the permission change (Stage 2c), purges application caches (Stage 3), and restores one withdrawn document to test the rollback (Stage 4).
Pass criteria:
- Key lookups for every chunk of withdrawn documents return HTTP 404 within
${TAKEDOWN_DEADLINE}. - Search queries filtering by parent source path return zero documents.
- Users who lost access cannot retrieve the changed document, while users who kept access can.
- Application caches return fresh answers without withdrawn content.
- The restored document is searchable again after the Stage 4 rollback item.
Record¶
| Item | Detail |
|---|---|
| Playbook | PB-RAG-09-05 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] |
9. Sources¶
Checked on 3 November 2026.
- Microsoft Learn, Document-level access control in Azure AI Search: security filters (generally available), native ACLs and RBAC scopes (preview),
x-ms-query-source-authorizationheader, and query propagation timing. - Microsoft Learn, Query-time ACL and RBAC enforcement in Azure AI Search (checked on 4 November 2026): the user's token in
x-ms-query-source-authorizationon a POST search with2026-08-01-preview, and that a query without a user token returns only public documents. - Microsoft Learn, Change and delete detection using indexers for Azure Storage: native blob soft delete, custom metadata column policy, first-run policy rule, blob versioning constraints, soft-delete retention requirements, the custom metadata order (index first, then delete the file), restored blobs and
LastModified, and the one-to-many limitation. - Microsoft Learn, Run or reset indexers, skills, or documents: Run Indexer, Get Indexer Status, Reset Documents (preview
2026-08-01-preview), Resync with permissions option (preview2026-08-01-preview), and concurrent run conflict handling. - Microsoft Learn, Delete documents in a search index: delete action payloads, idempotent 200 responses, 207 Multi-Status handling, key lookups for 404 verification, and asynchronous physical cleanup.
- Microsoft Learn, Documents - Index (REST API
2026-04-01) (checked on 4 November 2026): amergereplaces every field it sends, including collections; per-itemstatusandstatusCodevalues. - Microsoft Learn, Shape search results in Azure AI Search (checked on 4 November 2026): 50 results by default, at most 1,000 per page.
- Microsoft Learn, Define index projections (checked on 4 November 2026): chunks are deleted with their parent only when a deletion detection policy detects the deletion; the parent key field must be
filterable; manual index updates are overwritten on the next pipeline run. - Microsoft Learn, Indexers - Run, Indexers - Get Status and Indexers - Resync (preview) (checked on 4 November 2026): 202 Accepted for Run,
itemsProcessedanditemsFailedin the status, 204 No Content for Resync. - Microsoft Learn, Indexers - Reset Docs (preview) (checked on 4 November 2026): the body takes
documentKeysordatasourceDocumentIds; 204 No Content. - Microsoft Learn, Access control model for Azure Data Lake Storage (checked on 4 November 2026): Storage Blob Data Owner can change the ACLs of all items; Storage Blob Data Contributor only of items it owns.
- Microsoft Learn, Azure AI Search monitoring data reference:
DocumentsProcessedCountmetric withFaileddimension, andThrottledSearchQueriesPercentage. - Microsoft Learn, Service limits for Azure AI Search: batch limits of at most 1,000 documents per batch and 16 MB per request payload.
- AWS Documentation, Sync your data with your Amazon Bedrock knowledge base: S3 data source synchronization, incremental deletion handling, and metadata query filtering.
- AWS Documentation, DeleteKnowledgeBaseDocuments (checked on 4 November 2026): part of the Agents for Amazon Bedrock API, at most 10 documents per call.
- AWS Documentation, KnowledgeBaseVectorSearchConfiguration (checked on 4 November 2026): the metadata
filterused byRetrieve. - OpenSearch Documentation, Delete by query - OpenSearch Documentation: document purge using the
_delete_by_queryAPI. - OpenSearch Documentation, Document-level security (checked on 4 November 2026): DLS limits the documents a role can read, set as a query on the role, which can use the user's backend roles.
- PostgreSQL Documentation, PostgreSQL Row Security Policies: row-level security policy syntax.
- PostgreSQL Documentation, Constraints (checked on 4 November 2026):
ON DELETE CASCADEdeletes the referencing rows when the referenced row is deleted.
Related playbooks¶
- PB-RAG-04-03: Apply an urgent permission change or revocation
- PB-RAG-04-04: Permission leakage test and incident checklist
- PB-RAG-05-02: Recover from indexer and skillset failures and throttling
- PB-RAG-05-03: Handle source deletions, duplicates and scheduled sync
- PB-RAG-09-01: Reindex an Azure AI Search index without downtime
- PB-RAG-09-04: Set up incremental and event-driven refresh with change and deletion detection