perf: switch Qdrant to AsyncQdrantClient with explicit timeout
Every vector call ran on the sync QdrantClient inside async wrapper methods, blocking the FastAPI event loop per Qdrant round-trip. The wrapper now holds an AsyncQdrantClient (timeout via QDRANT_TIMEOUT, default 30s) and awaits all client calls; the wrapper API is unchanged. Call sites off the wrapper were fixed too: the HybridRAG document leg now uses the async search_vectors wrapper instead of the deprecated raw client.search (also fixing its call to the nonexistent ollama.embed_text which made the leg permanently report 'failed'), the health check awaits get_collections, and document_sync's raw delete/upsert calls are awaited (routed through wrappers in the next commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
@@ -8,7 +8,7 @@ Provides async vector operations with:
|
||||
- Similarity queries
|
||||
"""
|
||||
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client import AsyncQdrantClient
|
||||
from qdrant_client.models import (
|
||||
Distance, VectorParams, PointStruct,
|
||||
Filter, FieldCondition, MatchValue, Range
|
||||
@@ -30,17 +30,22 @@ class QdrantClientWrapper:
|
||||
Each user has isolated vector collection for their documents.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str, embedding_dim: int = 768):
|
||||
def __init__(self, url: str, embedding_dim: int = 768, timeout: float = 30.0):
|
||||
"""
|
||||
Initialize Qdrant client.
|
||||
|
||||
Uses AsyncQdrantClient so vector calls never block the FastAPI
|
||||
event loop, with an explicit timeout so a hung Qdrant cannot
|
||||
stall requests indefinitely.
|
||||
|
||||
Args:
|
||||
url: Qdrant server URL (e.g., "http://qdrant:6333")
|
||||
embedding_dim: Vector embedding dimension (default 768 for nomic-embed-text)
|
||||
timeout: Per-request timeout in seconds (default 30.0)
|
||||
"""
|
||||
self.client = QdrantClient(url=url)
|
||||
self.client = AsyncQdrantClient(url=url, timeout=timeout)
|
||||
self.embedding_dim = embedding_dim
|
||||
logger.info(f"Initialized Qdrant client: {url}")
|
||||
logger.info(f"Initialized async Qdrant client: {url} (timeout={timeout}s)")
|
||||
|
||||
def get_collection_name(self, user: str) -> str:
|
||||
"""
|
||||
@@ -62,12 +67,12 @@ class QdrantClientWrapper:
|
||||
collection_name: Collection name
|
||||
"""
|
||||
try:
|
||||
collections = self.client.get_collections()
|
||||
collections = await self.client.get_collections()
|
||||
existing = [c.name for c in collections.collections]
|
||||
|
||||
if collection_name not in existing:
|
||||
logger.info(f"Creating Qdrant collection: {collection_name}")
|
||||
self.client.create_collection(
|
||||
await self.client.create_collection(
|
||||
collection_name=collection_name,
|
||||
vectors_config=VectorParams(
|
||||
size=self.embedding_dim,
|
||||
@@ -90,7 +95,7 @@ class QdrantClientWrapper:
|
||||
True if collection exists
|
||||
"""
|
||||
try:
|
||||
collections = self.client.get_collections()
|
||||
collections = await self.client.get_collections()
|
||||
existing = [c.name for c in collections.collections]
|
||||
return collection_name in existing
|
||||
except Exception as e:
|
||||
@@ -161,7 +166,7 @@ class QdrantClientWrapper:
|
||||
))
|
||||
|
||||
try:
|
||||
self.client.upsert(
|
||||
await self.client.upsert(
|
||||
collection_name=collection_name,
|
||||
points=points
|
||||
)
|
||||
@@ -213,7 +218,7 @@ class QdrantClientWrapper:
|
||||
query_filter = Filter(must=conditions)
|
||||
|
||||
try:
|
||||
response = self.client.query_points(
|
||||
response = await self.client.query_points(
|
||||
collection_name=collection_name,
|
||||
query=query_vector,
|
||||
limit=limit,
|
||||
@@ -251,7 +256,7 @@ class QdrantClientWrapper:
|
||||
collection_name = self.get_collection_name(user)
|
||||
|
||||
try:
|
||||
self.client.delete(
|
||||
await self.client.delete(
|
||||
collection_name=collection_name,
|
||||
points_selector=Filter(
|
||||
must=[
|
||||
@@ -287,7 +292,7 @@ class QdrantClientWrapper:
|
||||
|
||||
try:
|
||||
# Scroll through points with doc_id filter
|
||||
points, _ = self.client.scroll(
|
||||
points, _ = await self.client.scroll(
|
||||
collection_name=collection_name,
|
||||
scroll_filter=Filter(
|
||||
must=[
|
||||
@@ -329,7 +334,7 @@ class QdrantClientWrapper:
|
||||
|
||||
try:
|
||||
# Get collection info
|
||||
collection_info = self.client.get_collection(collection_name)
|
||||
collection_info = await self.client.get_collection(collection_name)
|
||||
# This gives total points, not unique docs
|
||||
# For unique docs, would need to aggregate by doc_id
|
||||
return collection_info.points_count
|
||||
@@ -352,7 +357,7 @@ class QdrantClientWrapper:
|
||||
collection_name = self.get_collection_name(user)
|
||||
|
||||
try:
|
||||
self.client.delete_collection(collection_name)
|
||||
await self.client.delete_collection(collection_name)
|
||||
logger.warning(f"Deleted collection: {collection_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -382,7 +387,7 @@ class QdrantClientWrapper:
|
||||
|
||||
try:
|
||||
# Get source document chunks with vectors
|
||||
source_points, _ = self.client.scroll(
|
||||
source_points, _ = await self.client.scroll(
|
||||
collection_name=collection_name,
|
||||
scroll_filter=Filter(
|
||||
must=[
|
||||
@@ -404,7 +409,7 @@ class QdrantClientWrapper:
|
||||
# (could aggregate multiple chunks for better results)
|
||||
first_vector = source_points[0].vector
|
||||
|
||||
response = self.client.query_points(
|
||||
response = await self.client.query_points(
|
||||
collection_name=collection_name,
|
||||
query=first_vector,
|
||||
limit=limit * 2, # Get more to filter out same doc
|
||||
@@ -449,7 +454,7 @@ class QdrantClientWrapper:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
self.client.upsert(
|
||||
await self.client.upsert(
|
||||
collection_name=collection_name,
|
||||
points=[PointStruct(
|
||||
id=vector_id,
|
||||
@@ -487,7 +492,7 @@ class QdrantClientWrapper:
|
||||
query_filter = Filter(must=conditions)
|
||||
|
||||
# Delete points
|
||||
result = self.client.delete(
|
||||
result = await self.client.delete(
|
||||
collection_name=collection_name,
|
||||
points_selector=query_filter
|
||||
)
|
||||
@@ -532,7 +537,7 @@ class QdrantClientWrapper:
|
||||
query_filter = Filter(must=conditions)
|
||||
|
||||
try:
|
||||
response = self.client.query_points(
|
||||
response = await self.client.query_points(
|
||||
collection_name=collection_name,
|
||||
query=query_vector,
|
||||
limit=limit,
|
||||
@@ -589,7 +594,7 @@ class QdrantClientWrapper:
|
||||
|
||||
try:
|
||||
while True:
|
||||
points, next_offset = self.client.scroll(
|
||||
points, next_offset = await self.client.scroll(
|
||||
collection_name=collection_name,
|
||||
scroll_filter=scroll_filter,
|
||||
limit=batch_size,
|
||||
@@ -636,7 +641,7 @@ class QdrantClientWrapper:
|
||||
return 0
|
||||
|
||||
try:
|
||||
self.client.delete(
|
||||
await self.client.delete(
|
||||
collection_name=collection_name,
|
||||
points_selector=point_ids
|
||||
)
|
||||
@@ -655,13 +660,13 @@ class QdrantClientWrapper:
|
||||
List of collection info dictionaries
|
||||
"""
|
||||
try:
|
||||
collections = self.client.get_collections()
|
||||
collections = await self.client.get_collections()
|
||||
result = []
|
||||
|
||||
for coll in collections.collections:
|
||||
# Get detailed collection info
|
||||
try:
|
||||
info = self.client.get_collection(coll.name)
|
||||
info = await self.client.get_collection(coll.name)
|
||||
result.append({
|
||||
"name": coll.name,
|
||||
"vectors_count": info.vectors_count or 0,
|
||||
@@ -717,7 +722,7 @@ class QdrantClientWrapper:
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.client.query_points(
|
||||
response = await self.client.query_points(
|
||||
collection_name=collection_name,
|
||||
query=query_vector,
|
||||
limit=limit,
|
||||
@@ -768,7 +773,7 @@ class QdrantClientWrapper:
|
||||
count = 0
|
||||
offset = None
|
||||
while True:
|
||||
points, next_offset = self.client.scroll(
|
||||
points, next_offset = await self.client.scroll(
|
||||
collection_name=collection_name,
|
||||
scroll_filter=expiry_filter,
|
||||
limit=100,
|
||||
@@ -784,7 +789,7 @@ class QdrantClientWrapper:
|
||||
return 0
|
||||
|
||||
# Delete expired points
|
||||
self.client.delete(
|
||||
await self.client.delete(
|
||||
collection_name=collection_name,
|
||||
points_selector=expiry_filter
|
||||
)
|
||||
@@ -804,7 +809,7 @@ class QdrantClientWrapper:
|
||||
List of volatile collection names
|
||||
"""
|
||||
try:
|
||||
collections = self.client.get_collections()
|
||||
collections = await self.client.get_collections()
|
||||
return [
|
||||
c.name for c in collections.collections
|
||||
if c.name.startswith("volatile_")
|
||||
|
||||
Reference in New Issue
Block a user