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:
2026-07-14 13:09:37 +02:00
co-authored by Claude Fable 5
parent c35d3c1fa9
commit 8348b4bf92
6 changed files with 246 additions and 38 deletions
+31 -1
View File
@@ -33,6 +33,7 @@ class OllamaClient:
self.base_url = base_url.rstrip("/")
self.model = model
self.embeddings_url = f"{self.base_url}/api/embeddings"
self.embed_url = f"{self.base_url}/api/embed"
self.generate_url = f"{self.base_url}/api/generate"
self.tags_url = f"{self.base_url}/api/tags"
self.client = httpx.AsyncClient(timeout=120.0) # Embeddings can be slow
@@ -106,8 +107,37 @@ class OllamaClient:
>>> len(embeddings)
3
"""
embeddings = []
if not texts:
return []
# Single batched request via Ollama's /api/embed (the old
# implementation looped one /api/embeddings call per text).
try:
response = await self.client.post(
self.embed_url,
json={"model": self.model, "input": texts}
)
response.raise_for_status()
data = response.json()
embeddings = data.get("embeddings")
if embeddings is not None and len(embeddings) == len(texts):
if show_progress:
logger.info(f"Batched embedding complete: {len(embeddings)}/{len(texts)}")
return embeddings
logger.warning(
f"Batched embed returned {len(embeddings or [])} vectors for "
f"{len(texts)} inputs, falling back to per-text embedding"
)
except Exception as e:
logger.warning(
f"Batched embed failed ({e}), falling back to per-text embedding"
)
# Fallback: per-text embedding preserves partial-success semantics
# (None entries for texts that failed to embed).
embeddings = []
for i, text in enumerate(texts):
if show_progress and i % 10 == 0:
logger.info(f"Embedding progress: {i}/{len(texts)}")