Skip to content

fix(rag-worker): clear stale doc embeddings before upsert to prevent orphaned vectors - #701

Open
lakshayyy10 wants to merge 1 commit into
rowboatlabs:mainfrom
lakshayyy10:fix/rag-worker-orphaned-vectors
Open

fix(rag-worker): clear stale doc embeddings before upsert to prevent orphaned vectors#701
lakshayyy10 wants to merge 1 commit into
rowboatlabs:mainfrom
lakshayyy10:fix/rag-worker-orphaned-vectors

Conversation

@lakshayyy10

Copy link
Copy Markdown

Problem

Fixes #603

The three ingestion pipelines (file / text / URL) in rag-worker.ts generate Qdrant point IDs with crypto.randomUUID(), upsert the points, and then update the doc record in MongoDB. These are separate network calls with no cleanup between attempts.

Because docs with status pending or error are re-polled, any retry of a failed or partially-completed run (e.g. Qdrant upsert succeeded but the Mongo updateByVersion failed) re-embeds the doc and upserts a full duplicate set of chunks under fresh random IDs. The vectors from the previous attempt are never removed, so duplicates accumulate on every retry — bloating the collection and polluting RAG search results with duplicate/stale chunks.

Fix

  • Add a deleteDocEmbeddings(projectId, sourceId, docId) helper using the same filtered-delete pattern the worker already uses in runDeletionPipeline.
  • Call it immediately before the Qdrant upsert in all three pipelines, making doc processing idempotent — a retry now replaces the previous attempt's points instead of stacking on top of them.
  • Refactor runDeletionPipeline to reuse the helper (removes the duplicated inline filter).

Why delete-before-upsert rather than deterministic point IDs

Deterministic IDs (e.g. hashing docId + chunk index) would make retries overwrite in place, but if a doc is later re-processed and yields fewer chunks, the leftover higher-index points would still linger. The scoped filtered delete handles every case (retry, re-process, content change) and reuses an existing pattern in the codebase.

Testing notes

  • Verified the filter shape matches the existing deletion paths (projectId + sourceId + docId must-clauses).
  • esbuild parse check passes on the modified file.
  • Happy to add a worker-level test if you can point me at the preferred harness for this script.

…orphaned vectors

Point IDs are generated with crypto.randomUUID(), so any retry of a
failed or partially-completed doc run upserts a full duplicate set of
chunks alongside the old ones. Delete the doc's existing points
(scoped by projectId + sourceId + docId) before upserting in all three
ingestion pipelines, and reuse the same helper in the deletion
pipeline.

Fixes rowboatlabs#603
@lakshayyy10

Copy link
Copy Markdown
Author

Ready for review whenever you get a chance — happy to adjust the approach or add a worker-level test if you can point me at the preferred harness for this script. cc @ramnique

brianlane added a commit to brianlane/newCoworker that referenced this pull request Jul 22, 2026
…F, rag-worker) (#838)

Bumps the Rowboat pin bb32686b -> f422019e (brianlane/rowboat
newcoworker/upgrades-jul-2026), which cherry-picks three unmerged
upstream community fixes onto our hardened fork branch:

- rowboatlabs/rowboat#776: path traversal in /api/uploads/[fileId]
  (Rowboat :3000 is published through the tenant tunnel; the route has
  no auth, so traversal was reachable)
- rowboatlabs/rowboat#547: SSRF guard on webhook + custom MCP server
  URLs (blocks private/internal IP targets like loopback services)
- rowboatlabs/rowboat#701: rag-worker clears stale doc embeddings
  before upsert (KVM8 jobs-worker + qdrant)

Upstream main itself has zero apps/rowboat commits since our pin base
(all new work is the apps/x desktop app), so this is a cherry-pick
bump, not an upstream merge.

Also fixes the integration Mongo seed: the agent 'model' field was
dropped in April (1de8587), and Rowboat's createAgent has no model
fallback - every turn crashed with "Cannot read properties of
undefined (reading 'startsWith')", so the kvm suites failed on main
with the OLD pin too. Restoring the field (production deploy-client.sh
seeds always set it) makes the suites pass again.

Validated: agent-tool-seed-parity, test:integration:kvm2 and
test: integration:kvm8 (real stacks built from the new SHA) all green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Orphaned vectors piling up in Qdrant every time the RAG worker retries a failed doc

1 participant