fix: await ensure_collection and survive partial embed failures in document sync
- ensure_collection was called without await, so the coroutine never ran and fresh tenants had no collection when the upsert hit Qdrant - a single None entry from embed_batch poisoned the point batch and aborted the whole document upsert; failed chunks are now skipped with a warning (all-failed raises and the IndexResult reports failure) - raw client.delete/client.upsert calls now go through the async wrapper (delete_by_filter and the new batch upsert_points method) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
@@ -467,6 +467,43 @@ class QdrantClientWrapper:
|
||||
logger.error(f"Failed to upsert vector: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def upsert_points(
|
||||
self,
|
||||
collection_name: str,
|
||||
points: List[Dict[str, Any]]
|
||||
) -> int:
|
||||
"""
|
||||
Upsert a batch of vector points in a single request.
|
||||
|
||||
Args:
|
||||
collection_name: Collection name
|
||||
points: List of {"id": str, "vector": List[float], "payload": dict}
|
||||
|
||||
Returns:
|
||||
Number of points upserted
|
||||
|
||||
Raises:
|
||||
Exception: If the upsert fails (callers decide how to degrade)
|
||||
"""
|
||||
if not points:
|
||||
return 0
|
||||
|
||||
structs = [
|
||||
PointStruct(
|
||||
id=p["id"],
|
||||
vector=p["vector"],
|
||||
payload=p.get("payload", {})
|
||||
)
|
||||
for p in points
|
||||
]
|
||||
|
||||
await self.client.upsert(
|
||||
collection_name=collection_name,
|
||||
points=structs
|
||||
)
|
||||
logger.info(f"Upserted {len(structs)} points into {collection_name}")
|
||||
return len(structs)
|
||||
|
||||
async def delete_by_filter(
|
||||
self,
|
||||
collection_name: str,
|
||||
|
||||
@@ -194,19 +194,15 @@ class DocumentSyncService:
|
||||
) -> int:
|
||||
"""Create vector embeddings for document content."""
|
||||
collection = get_qdrant_collection_name(user)
|
||||
self.qdrant.ensure_collection(collection)
|
||||
await self.qdrant.ensure_collection(collection)
|
||||
|
||||
# Delete existing chunks for this document
|
||||
# Delete existing chunks for this document (via the async wrapper)
|
||||
try:
|
||||
await self.qdrant.client.delete(
|
||||
await self.qdrant.delete_by_filter(
|
||||
collection_name=collection,
|
||||
points_selector={
|
||||
"filter": {
|
||||
"must": [
|
||||
{"key": "doc_type", "match": {"value": "document"}},
|
||||
{"key": "paperless_id", "match": {"value": document_id}},
|
||||
]
|
||||
}
|
||||
filter_conditions={
|
||||
"doc_type": "document",
|
||||
"paperless_id": document_id,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -217,12 +213,22 @@ class DocumentSyncService:
|
||||
if not chunks:
|
||||
return 0
|
||||
|
||||
# Generate embeddings
|
||||
# Generate embeddings (embed_batch returns None for failed chunks)
|
||||
embeddings = await self.ollama.embed_batch(chunks)
|
||||
|
||||
# Build points
|
||||
# Build points, skipping chunks whose embedding failed. Previously a
|
||||
# single None embedding poisoned the batch and aborted the whole
|
||||
# document upsert.
|
||||
points = []
|
||||
skipped = 0
|
||||
for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
|
||||
if embedding is None:
|
||||
skipped += 1
|
||||
logger.warning(
|
||||
f"Skipping chunk {i} of document {document_id}: embedding failed"
|
||||
)
|
||||
continue
|
||||
|
||||
point_id = str(uuid.uuid4())
|
||||
content_hash = hashlib.md5(chunk.encode()).hexdigest()
|
||||
|
||||
@@ -240,9 +246,19 @@ class DocumentSyncService:
|
||||
}
|
||||
})
|
||||
|
||||
# Upsert to Qdrant
|
||||
if skipped and not points:
|
||||
raise RuntimeError(
|
||||
f"All {skipped} chunk embeddings failed for document {document_id}"
|
||||
)
|
||||
if skipped:
|
||||
logger.warning(
|
||||
f"Document {document_id}: {skipped}/{len(chunks)} chunks skipped "
|
||||
f"(embedding failures); indexing the remaining {len(points)}"
|
||||
)
|
||||
|
||||
# Upsert to Qdrant in one batch via the async wrapper
|
||||
if points:
|
||||
await self.qdrant.client.upsert(
|
||||
await self.qdrant.upsert_points(
|
||||
collection_name=collection,
|
||||
points=points
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user