fix: re-index after an embedding model switch runs with the previous model - #40965
Conversation
The worker re-reads the datasets row in its own session, so firing the celery task while the request transaction is still open lets the re-index run with the previously declared embedding model. Dispatch on after_commit instead, cancelled on rollback.
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-08-19 12:52:52.035989962 +0000
+++ /tmp/pyrefly_pr.txt 2026-08-19 12:52:44.427993925 +0000
@@ -8617,7 +8617,7 @@
ERROR Object of class `object` has no attribute `kw` [missing-attribute]
--> tests/unit_tests/services/test_clear_free_plan_tenant_expired_logs.py:757:12
ERROR Object of class `NoneType` has no attribute `keyword_number` [missing-attribute]
- --> tests/unit_tests/services/test_dataset_service_dataset.py:976:16
+ --> tests/unit_tests/services/test_dataset_service_dataset.py:1091:16
ERROR Object of class `FakeAccount` has no attribute `id` [missing-attribute]
--> tests/unit_tests/services/test_dataset_service_document.py:144:9
ERROR Object of class `FakeAccount` has no attribute `current_tenant_id` [missing-attribute]
|
Pyrefly Type Coverage
|
|
I think there may be a RAG_PIPELINE edge case with the current listener registration order.
This means the dataset/model update may already be committed before the listener exists. If something later in the controller fails after that internal commit, the outer The resulting sequence would be:
leaving the committed dataset configuration potentially inconsistent with the existing vectors again. Would it be safer to register the Then:
A regression test where the RAG helper commits and then a later operation fails would help lock down this ordering. |
For RAG_PIPELINE datasets _update_pipeline_knowledge_base_node_data commits internally. Registering the after_commit listener only after the helper returned meant that internal commit never dispatched the re-index, and a later outer rollback would cancel it entirely, leaving the committed dataset configuration inconsistent with the existing vectors. Register the listener before the helper instead: RAG pipelines dispatch on the helper's internal commit, normal datasets on the outer commit, and once=True prevents a double dispatch. The integration tests now commit before asserting dispatch, matching the after_commit contract, and a regression test locks down the RAG_PIPELINE ordering.
|
Good catch, that ordering was wrong. In e3bbfb5 the listener is now registered before _update_pipeline_knowledge_base_node_data() runs, so a RAG pipeline dispatches on the helper's internal commit and normal datasets still dispatch on the outer with_session commit. once=True keeps the later outer commit from firing a second dispatch. Added a regression test (test_update_internal_dataset_dispatches_on_rag_pipeline_internal_commit) that commits inside the helper and asserts the task fires exactly once across both commits. Also fixed the three integration tests that asserted inline dispatch (they now commit before asserting) and retitled the PR for the title check. |
Important
Fixes #<issue number>.Summary
Switching a knowledge base between two embedding models looks fine in the console, but every chunk ends up re-embedded with the model that was declared before the PATCH. The declaration and the stored vectors silently land in different vector spaces, and retrieval quality drops with no error anywhere. Fixes #40961.
The ordering in
_update_internal_datasetis the problem: it flushes the newembedding_modelonto thedatasetsrow and then callsdeal_dataset_vector_index_task.delay(...)while the request transaction is still open (the commit only happens later in thewith_sessiondecorator; theflush()in between is deliberate, see #39191). The worker opens its own session, reads the pre-update row, and re-embeds with the old model. The task publish pretty much always wins that race against the request commit.The issue itself suggests deferring the dispatch until after commit, so that's what I went with, it's also the smaller change. Both celery calls now run inside an
after_commithook on the session (same idiom asregister_new_agent_beta_publish_after_commit), with anafter_rollbackguard so a rolled-back request can't leave a stale listener that fires on some later commit.regenerate_summary_index_taskis dispatched from the same spot and reads the same row, so it goes through the hook too.For RAG-pipeline knowledge bases nothing really changes:
_update_pipeline_knowledge_base_node_dataalready commits mid-request, which is why they were mostly spared. The hook just fires at the decorator's commit instead.One thing I didn't do: the issue also floats a way to force a re-index once declaration and vectors have already diverged. There's no dispatch path to reuse for that today, so it felt like a separate feature and I left it out.
Tests: extended the SQLite-backed update tests in
test_dataset_service_dataset.py. They now assert the tasks are not published during_update_internal_datasetitself, that they fire on the followingcommit()with the right arguments foradd/remove/update, that a rollback suppresses them even if the session commits again later, and that no action means no dispatch. Ran the update tests, ruff, pyrefly and mypy locally.Not 100% sure the hook belongs as a static method on
DatasetServicevs a small helper in the tasks module, happy to move it if there's a house preference.Screenshots
Checklist
make lint && make type-check(backend) andvp staged(frontend) to appease the lint gods