Repository navigation
Large document metadata, tag, and sharing changes don't scale to AI Search chunks #1657
Description
Issue
Saving document metadata or tags does work proportional to the document's chunk count inside the HTTP request. For every chunk, update_document() → update_chunk_metadata() performs an Azure AI Search GET of the full chunk (including its embedding vector), a full upload_documents rewrite, and a Data Management Search write-fence slot acquire/release in Cosmos. Large documents (large XML, long PDFs) exceed the front-end request timeout, and the browser receives an HTML error page that surfaces as an "invalid JSON" error.
#1645 mitigated the personal workspace Save Metadata dialog by splitting the same per-chunk work across browser-driven PATCH continuations (75 chunks per request). The per-chunk cost, the other entry points, and the failure handling are unchanged.
Steps to Reproduce
- Upload a document that produces thousands of chunks to a group or public workspace.
- Add a tag or edit metadata and save. Alternatively, rename or delete a tag used by many documents, or bulk-tag several large documents.
- The request runs until it times out, and some chunks keep the old metadata.
Expected Behavior
- Metadata and tag saves return quickly regardless of document size.
- Search chunks converge to the saved metadata automatically, including after transient failures or worker restarts.
- Sharing approvals and revocations reach the search index, and revocations are enforced before success is reported.
Actual Behavior
- Per-chunk round-trips run inside the request. Bulk tag, tag rename, and tag delete sync tags twice (
update_document()pluspropagate_tags_to_chunks()) in personal, group, and public workspaces. The group and public metadata PATCH routes callupdate_document()once per field, so a multi-field save makes several full passes. update_document()swallows chunk sync errors and reports success, and partially synced documents are never retried. The Batch large document metadata chunk sync #1645 continuation state lives only in the browser.- Group
shared_group_idschanges made throughupdate_document()(share, approve, unshare, remove-self) are never projected to chunks, because the sync loop skips the chunk write when no non-ACL field changed. Chunks rebuilt by reprocessing or a new revision copy the document'sshared_group_ids, so a later unshare can leave a revoked group with search access, and approved shares may never become searchable. - The full-document GET + upload pattern can re-upload a stale
shared_user_idsorshared_group_idsvalue if it races with a concurrent sharing change.
Impact: what users will notice
The biggest wins are for large documents, large workspaces, and group sharing. Small documents mostly gain reliability.
| Area | Today | After the fix |
|---|---|---|
| Azure load | Every chunk's embedding is downloaded and re-uploaded just to change a tag (roughly hundreds of MB for a 10k-chunk document). Each chunk also costs about 2 AI Search calls and 4 Cosmos write-fence operations, which comes to about 20,000 Search calls and 40,000 Cosmos operations for 10k chunks. Bulk tag, tag rename, and tag delete double that. | Merge payloads are a few hundred bytes per chunk, and a 10k-chunk document takes about 40 Search calls. That's roughly 500x fewer Search calls and fence operations, with far less AI Search indexing pressure, Cosmos RU consumption, and network transfer. |
| Save metadata or tags on a large document | Personal: the dialog spins through many sequential requests (#1645). Group and public: the request times out and shows "invalid JSON". | The dialog closes immediately, with a brief "search index updating" notice. |
| Bulk tag, tag rename, tag delete in large workspaces | Each document's chunks are synced twice inside one request, which times out. | Fast. Only Cosmos writes happen in the request; chunk updates run in the background. |
| Chat tag filters | Filters match document_tags on each chunk, so a partially synced document is only partly visible (or invisible) to tag-filtered chat. Failures are never retried. |
Every chunk converges, with automatic retries, so tag filters see the whole document. |
| Citations | Title, author, and classification come from the chunk and can show stale values, such as the old classification label after a document is reclassified. | Citations reflect the edit. |
| Group sharing in search | Approvals and revocations never reach the chunks. Approved shares may not be searchable, and an unshare can leave a revoked group with search access. | Approved shares become searchable, and revocations are enforced before success is reported. |
| Personal share approval | A chunk-update failure is swallowed, and a retry reports "Already approved", so the document stays unsearchable for the recipient. | Fails closed with a clear error, so a retry works. |
| Other users on the same server | A large tag save holds the process-wide Search write lock for tens of thousands of writes, so other indexing on that worker queues behind it. | A few dozen writes, so uploads and indexing aren't blocked by someone else's tag save. |
Unchanged: search ranking and relevance, embeddings, semantic and hybrid search behavior, upload-time chunking and ingestion speed, and the UI layout.
Trade-offs:
- Chat tag filters can lag a few seconds after a save, up to a minute or two on very large documents.
- Sharing actions can now return an error instead of a silent success when Search writes fail or are frozen during a Data Management migration.
Notes
Proposed fix, delivered as two PRs (Development and paullizer-react-v2-ui):
- Replace per-chunk GET + upload with key-only keyset paging (
select=id, ordered byid, exact workspace scope filter) andmerge_documentsbatches through the existing fenced Search writer. Only the changed fields are sent; embeddings never round-trip. - Add a durable background sync for non-ACL metadata (title, authors, file name, classification, tags). A per-document sync revision and status is stored on the Cosmos document in the same write as the edit; a background worker is serialized per document; and a reconciler loop in
background_tasks.pyretries with backoff. - Keep ACL projections (
shared_user_ids,shared_group_ids) synchronous and fail-closed, using the same merge engine. - Consolidate the metadata PATCH routes into a single
update_document()call, remove the duplicate tag passes, move blob-metadata tag updates into the sync worker, and supersede the Batch large document metadata chunk sync #1645 client continuation protocol. - The React v2 port preserves the
strict/expected_etag/operation_guard/ content-screening semantics of itsupdate_document()and the V2 UI receipt contracts.
Acceptance Criteria
- No per-chunk Search or Cosmos work happens inside metadata or tag save requests.
- Chunk metadata converges automatically; failures are retried with backoff and visible on the document.
- Personal and group share approvals and revocations are projected to search, and revocations fail closed.
- Bulk tag, tag rename, and tag delete no longer double-sync.
- Functional tests, fix documentation, and release notes on each branch.
- Separate PRs merged into
Developmentandpaullizer-react-v2-ui.
Refs #1645
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsReleased
Activity
paullizer commentedon Oct 6, 2026
Development PR is up: #1658 (
0.261.052).merge_documentsbatches that send only the changed fields (about 500x fewer Search calls; no embedding round-trips).shared_user_ids/shared_group_ids) are projected before they're saved and fail closed, fixing group sharing changes that never reached Search.paullizer-react-v2-uias a separate PR. V2 needs adaptation for its strict/etagupdate_document(), content screening, native group/public document APIs, andv2_uireceipts.Harden document search metadata sync against races and freezes
paullizer commentedon Oct 6, 2026
#1658 merged into
Development(900a3e431,0.261.052). All 15 CI checks passed, including CodeQL.The merge includes a hardening commit:
The local suites pass (27 sync tests, plus the route policy, docs, and logging checks).
Follow-ups (not in this change):
update_document()predates this fix.set_document_chunk_visibility()(revision archive/restore) to merges.Remaining: React v2 port (separate PR into
paullizer-react-v2-ui), in progress.Sync document search metadata in the background with batched merges
Clear cached search results after a background metadata sync
paullizer commentedon Oct 6, 2026
Both branches now have the fix.
Development:900a3e431(0.261.052), includes the hardening commit9c156a429.ab21fbbb2(0.261.053), clears cached search results for the workspace and for approved share recipients after a background sync changes chunks.paullizer-react-v2-ui: Port durable batched search metadata sync to React v2 #1674, merged as7bc21b612(0.261.268). It's a re-implementation against V2's strict, etag, screening, and projection-fence semantics and the native group and public document APIs, not a cherry-pick. It also fixes a V2 bug where strict native group metadata edits failed on any document with more than one chunk.All CI checks passed on all three PRs, including CodeQL. The V2 base-versus-PR suite sweep showed no new failures.
Follow-ups (not in these changes):
Development. The read-modify-write race inupdate_document()predates this fix.set_document_chunk_visibility()(revision archive/restore) and V2's native_search_acl(collaboration) onto the batched merge helper. Both write all of a document's chunks in one batch, which AI Search caps at 1,000.Port durable batched search metadata sync to React v2