perf: batch page embeddings via /api/embed and upsert before pruning stale points
- embed_batch now issues one batched /api/embed request (the old loop made one /api/embeddings round-trip per chunk) with a per-text fallback that preserves None-for-failed semantics - update_from_page embeds all chunks in that single call and stores them in one Qdrant batch upsert (upsert_points) - reindex order reversed: upsert new points first, then prune stale ids (deterministic uuid5 ids make overwrite safe) so a mid-way failure no longer leaves the page with zero vectors - VectorUpdateSummary gains status (success/partial/failed) and chunks_skipped; all-embeddings-failed keeps old vectors and reports failure instead of success=True Measured on a 7-chunk page ingest (local server, llm_tester): ~375ms -> ~181ms median over 3 runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
@@ -41,6 +41,9 @@ def mock_qdrant():
|
||||
qdrant.search_vectors = AsyncMock(return_value=[])
|
||||
qdrant.search_with_expiry_filter = AsyncMock(return_value=[])
|
||||
qdrant.upsert_vector = AsyncMock(return_value=True)
|
||||
qdrant.upsert_points = AsyncMock(side_effect=lambda collection_name, points: len(points))
|
||||
qdrant.scroll_all_points = AsyncMock(return_value=[])
|
||||
qdrant.delete_by_ids = AsyncMock(return_value=0)
|
||||
qdrant.delete_by_filter = AsyncMock(return_value=0)
|
||||
return qdrant
|
||||
|
||||
@@ -49,6 +52,9 @@ def mock_qdrant():
|
||||
def mock_ollama():
|
||||
ollama = MagicMock()
|
||||
ollama.embed = AsyncMock(return_value=[0.1] * 768)
|
||||
ollama.embed_batch = AsyncMock(
|
||||
side_effect=lambda texts, **kw: [[0.1] * 768 for _ in texts]
|
||||
)
|
||||
ollama.embed_text = AsyncMock(return_value=[0.1] * 768)
|
||||
ollama.generate_text = AsyncMock(return_value="{}")
|
||||
return ollama
|
||||
@@ -148,8 +154,8 @@ class TestVectorLegScoping:
|
||||
|
||||
assert summary.success is False
|
||||
assert "outside user" in (summary.error_message or "")
|
||||
mock_qdrant.upsert_vector.assert_not_awaited()
|
||||
mock_qdrant.delete_by_filter.assert_not_awaited()
|
||||
mock_qdrant.upsert_points.assert_not_awaited()
|
||||
mock_qdrant.delete_by_ids.assert_not_awaited()
|
||||
|
||||
async def test_update_from_page_rejects_sibling_prefix_namespace(
|
||||
self, vector_service, mock_qdrant, mock_wiki
|
||||
@@ -163,7 +169,7 @@ class TestVectorLegScoping:
|
||||
summary = await vector_service.update_from_page(page_id=43, user=TENANT)
|
||||
|
||||
assert summary.success is False
|
||||
mock_qdrant.upsert_vector.assert_not_awaited()
|
||||
mock_qdrant.upsert_points.assert_not_awaited()
|
||||
|
||||
async def test_update_from_page_accepts_own_namespace(
|
||||
self, vector_service, mock_qdrant, mock_wiki
|
||||
@@ -177,7 +183,7 @@ class TestVectorLegScoping:
|
||||
|
||||
assert summary.success is True
|
||||
assert (
|
||||
mock_qdrant.upsert_vector.await_args.kwargs["collection_name"]
|
||||
mock_qdrant.upsert_points.await_args.kwargs["collection_name"]
|
||||
== TENANT_COLLECTION
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user