feat: implement vector sync scanner and processor (ADR-007 Phase 2)
Implements background vector database synchronization using anyio TaskGroups for BasicAuth mode with single-user credentials. Scanner Implementation: - Periodic document discovery (hourly, configurable) - Timestamp-based change detection (Nextcloud vs Qdrant) - Wake event for immediate scanning on-demand - Supports both initial sync (all docs) and incremental sync (changes only) - Detects deleted documents and queues for removal Processor Implementation: - Concurrent document processing pool (3 workers default) - I/O-bound embedding generation via Ollama API - Retry logic with exponential backoff (3 retries) - Document chunking (512 words, 50-word overlap) - Handles both index and delete operations - Upserts vectors to Qdrant with rich metadata App Lifespan Integration: - Extended AppContext with background task state - Modified app_lifespan_basic() to start tasks via anyio TaskGroups - Graceful shutdown with coordinated task cancellation - Only activates when VECTOR_SYNC_ENABLED=true Embedding Service: - OllamaEmbeddingProvider with TLS support - Singleton pattern for shared client instances - Batch embedding support for efficiency - Auto-detects embedding dimension (768 for nomic-embed-text) Qdrant Client: - Async client wrapper with singleton pattern - Auto-creates collection on first use - COSINE distance metric for semantic similarity - Integrates with embedding service for dimension detection Health Check Enhancement: - Added Qdrant status check to /health/ready endpoint - Only checks when VECTOR_SYNC_ENABLED=true - 2-second timeout for health probe - Reports connection errors with details Configuration: - VECTOR_SYNC_ENABLED: Enable background sync - VECTOR_SYNC_SCAN_INTERVAL: Scanner frequency (3600s default) - VECTOR_SYNC_PROCESSOR_WORKERS: Concurrent processors (3 default) - QDRANT_URL, QDRANT_API_KEY, QDRANT_COLLECTION: Vector DB config - OLLAMA_BASE_URL, OLLAMA_EMBEDDING_MODEL: Embedding service config Dependencies Added: - qdrant-client>=1.7.0: Vector database client Docker Compose: - Added Qdrant service with health check - Exposed ports 6333 (REST) and 6334 (gRPC) - Configured MCP service with vector sync environment - Added qdrant-data volume for persistence Known Issue: - FastMCP lifespan not triggering for streamable-http transport - Background tasks will start once lifespan integration is complete - Lifespan triggers on MCP session establishment, not server startup Related: ADR-007 Background Vector Database Synchronization 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""Qdrant client wrapper."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from qdrant_client import AsyncQdrantClient
|
||||
from qdrant_client.models import Distance, VectorParams
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_qdrant_client: AsyncQdrantClient | None = None
|
||||
|
||||
|
||||
async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
"""
|
||||
Get singleton Qdrant client instance.
|
||||
|
||||
Automatically creates collection on first use if it doesn't exist.
|
||||
|
||||
Returns:
|
||||
Configured AsyncQdrantClient instance
|
||||
|
||||
Raises:
|
||||
Exception: If Qdrant connection fails or collection creation fails
|
||||
"""
|
||||
global _qdrant_client
|
||||
|
||||
if _qdrant_client is None:
|
||||
url = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
||||
api_key = os.getenv("QDRANT_API_KEY")
|
||||
|
||||
_qdrant_client = AsyncQdrantClient(
|
||||
url=url,
|
||||
api_key=api_key,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# Ensure collection exists
|
||||
collection_name = os.getenv("QDRANT_COLLECTION", "nextcloud_content")
|
||||
|
||||
# Import here to avoid circular dependency
|
||||
from nextcloud_mcp_server.embedding import get_embedding_service
|
||||
|
||||
embedding_service = get_embedding_service()
|
||||
dimension = embedding_service.get_dimension()
|
||||
|
||||
try:
|
||||
await _qdrant_client.get_collection(collection_name)
|
||||
logger.info(f"Using existing Qdrant collection: {collection_name}")
|
||||
except Exception:
|
||||
# Collection doesn't exist, create it
|
||||
await _qdrant_client.create_collection(
|
||||
collection_name=collection_name,
|
||||
vectors_config=VectorParams(
|
||||
size=dimension,
|
||||
distance=Distance.COSINE,
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
f"Created Qdrant collection: {collection_name} "
|
||||
f"(dimension={dimension}, distance=COSINE)"
|
||||
)
|
||||
|
||||
return _qdrant_client
|
||||
Reference in New Issue
Block a user