Skip to content

Large document metadata, tag, and sharing changes don't scale to AI Search chunks #1657

Closed

Description

@paullizer

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

  1. Upload a document that produces thousands of chunks to a group or public workspace.
  2. Add a tag or edit metadata and save. Alternatively, rename or delete a tag used by many documents, or bulk-tag several large documents.
  3. 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() plus propagate_tags_to_chunks()) in personal, group, and public workspaces. The group and public metadata PATCH routes call update_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_ids changes made through update_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's shared_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_ids or shared_group_ids value 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 by id, exact workspace scope filter) and merge_documents batches 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.py retries 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 its update_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 Development and paullizer-react-v2-ui.

Refs #1645

Activity

  1. added
    bugSomething isn't working
    security_improvementThis issue results in an improvement to security
    P1Priority 1: urgent, address before P2 work
    on Oct 6, 2026
  2. paullizer commented on Oct 6, 2026

    @paullizer
    ContributorAuthor

    Development PR is up: #1658 (0.261.052).

    • What changed:
      • Chunk updates now use key-paged merge_documents batches that send only the changed fields (about 500x fewer Search calls; no embedding round-trips).
      • Metadata saves record a durable sync request and return immediately; a per-document leased worker plus a reconciler loop retry with backoff until every chunk matches the document.
      • Sharing changes (shared_user_ids / shared_group_ids) are projected before they're saved and fail closed, fixing group sharing changes that never reached Search.
      • Duplicate tag passes are removed, and Batch large document metadata chunk sync #1645's browser continuation loop is retired.
    • Validation: 23 new and updated functional tests pass. Route policy (12/12), swagger, XSS-sink, broken-access-control, logging-tag, and docs checks pass. The pre-existing failures in the swagger integration count tests and one dispatcher test are identical on the base commit. The new Playwright UI test needs a live app and wasn't run.
    • Remaining: port to paullizer-react-v2-ui as a separate PR. V2 needs adaptation for its strict/etag update_document(), content screening, native group/public document APIs, and v2_ui receipts.
  3. paullizer commented on Oct 6, 2026

    @paullizer
    ContributorAuthor

    #1658 merged into Development (900a3e431, 0.261.052). All 15 CI checks passed, including CodeQL.

    The merge includes a hardening commit:

    • Syncs are matched to their document save by a per-request token, which closes a stale-revision race.
    • Saves retry when a finishing worker removes the sync record.
    • Search writes frozen by a Data Management migration retry every 5 minutes without escalating the backoff.
    • Only real access-list changes are projected.

    The local suites pass (27 sync tests, plus the route policy, docs, and logging checks).

    Follow-ups (not in this change):

    • Backfill existing group chunk access lists. Group shares approved before this fix are projected the next time that document's sharing changes.
    • Serialize concurrent access changes per document. The read-modify-write race in update_document() predates this fix.
    • Move set_document_chunk_visibility() (revision archive/restore) to merges.

    Remaining: React v2 port (separate PR into paullizer-react-v2-ui), in progress.

  4. paullizer commented on Oct 6, 2026

    @paullizer
    ContributorAuthor

    Both branches now have the fix.

    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):

    • Backfill existing group chunk access lists. Group shares approved before this fix are projected the next time that document's sharing changes, or on re-approval.
    • Serialize concurrent access changes per document in Development. The read-modify-write race in update_document() predates this fix.
    • Move 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.
    • V2: nothing reconciles projection-fence claims left uncertain after ambiguous Search errors. This predates the port.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

P1Priority 1: urgent, address before P2 workbugSomething isn't workingsecurity_improvementThis issue results in an improvement to security

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions