Humanoids / data /vector_store.py
MuhammedSuhaib's picture
Upload folder using huggingface_hub
2b3dbce verified
Raw
History Blame Contribute Delete
13.4 kB
"""
Module for interacting with Qdrant vector database.
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from typing import List, Dict, Any, Optional
import logging
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import Distance, VectorParams
from pydantic import BaseModel
import os
from dotenv import load_dotenv
load_dotenv()
class DocumentChunk(BaseModel):
chunk_id: str
content: str
doc_path: str
embedding: List[float]
metadata: Dict[str, Any] = {}
class VectorStore:
def __init__(self,
collection_name: str = "Humanoids",
timeout: int = 30,
grpc_port: int = 6334,
prefer_grpc: bool = False,
recreate_collection: bool = False):
"""
Initialize the vector store with Qdrant client.
Args:
collection_name: Name of the Qdrant collection to use
timeout: Timeout for Qdrant API requests in seconds
grpc_port: gRPC port for Qdrant communication
prefer_grpc: Whether to prefer gRPC communication (faster than HTTP)
recreate_collection: Whether to recreate the collection if it exists
"""
try:
qdrant_url = os.getenv("QDRANT_URL")
qdrant_api_key = os.getenv("QDRANT_API_KEY")
if not qdrant_url or not qdrant_api_key:
raise ValueError("QDRANT_URL and QDRANT_API_KEY must be set in environment variables")
self.client = QdrantClient(
url=qdrant_url,
api_key=qdrant_api_key,
timeout=timeout,
grpc_port=grpc_port,
prefer_grpc=prefer_grpc
)
self.collection_name = collection_name
self.timeout = timeout
self.recreate_collection = recreate_collection
# Create or verify the collection exists
self._ensure_collection_exists()
except Exception as e:
logging.error(f"Failed to initialize Qdrant client: {e}")
raise
def check_connection(self):
"""
Check if the Qdrant connection is working.
Returns:
True if connection is successful, False otherwise
"""
try:
# Try to get collections to verify connection
# Note: get_collections doesn't accept timeout parameter, but uses the client's global timeout
collections = self.client.get_collections()
logging.info("Qdrant connection test succeeded")
return True
except Exception as e:
logging.error(f"Qdrant connection test failed: {e}")
return False
def _ensure_collection_exists(self):
"""
Ensure the Qdrant collection exists with proper configuration.
"""
try:
# Check if collection exists
# Note: get_collections doesn't accept timeout parameter, but uses the client's global timeout
collections = self.client.get_collections().collections
collection_names = [c.name for c in collections]
collection_exists = self.collection_name in collection_names
if collection_exists and self.recreate_collection:
# Delete and recreate the collection
self.client.delete_collection(collection_name=self.collection_name, timeout=self.timeout)
collection_exists = False
logging.info(f"Deleted Qdrant collection '{self.collection_name}' for recreation")
if not collection_exists:
# Create collection with 384-dimensional vectors and cosine similarity
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
timeout=self.timeout
)
logging.info(f"Created Qdrant collection '{self.collection_name}' with 384-dim vectors and cosine similarity")
else:
# Verify collection configuration matches expected settings
# Note: get_collection doesn't accept timeout parameter, but uses the client's global timeout
collection_info = self.client.get_collection(
collection_name=self.collection_name
)
expected_size = 384
expected_distance = Distance.COSINE
# Check if vector configuration matches expectations
if hasattr(collection_info.config.params, 'vectors'):
vec_params = collection_info.config.params.vectors
if hasattr(vec_params, 'size') and vec_params.size != expected_size:
logging.warning(f"Collection '{self.collection_name}' has unexpected vector size: {vec_params.size}, expected: {expected_size}")
if hasattr(vec_params, 'distance') and vec_params.distance != expected_distance:
logging.warning(f"Collection '{self.collection_name}' has unexpected distance metric: {vec_params.distance}, expected: {expected_distance}")
logging.info(f"Qdrant collection '{self.collection_name}' already exists with proper configuration")
except Exception as e:
logging.error(f"Failed to create or verify Qdrant collection: {e}")
raise
def validate_embedding_dimensions(self, embedding: List[float]) -> bool:
"""Validate that the embedding has the correct dimensions for this collection."""
expected_size = 384 # As configured in the collection
if len(embedding) != expected_size:
raise ValueError(f"Embedding dimension mismatch: got {len(embedding)}, expected {expected_size}")
return True
def store_document_chunk(self, chunk: DocumentChunk) -> bool:
"""
Store a document chunk in the vector database.
Args:
chunk: DocumentChunk object containing content and embedding
Returns:
True if successful, False otherwise
"""
try:
# Validate embedding dimensions
self.validate_embedding_dimensions(chunk.embedding)
# Ensure chunk_id is a proper integer or UUID
# Convert string IDs to integer if possible, otherwise generate an integer ID
try:
point_id = int(chunk.chunk_id) if chunk.chunk_id.isdigit() else hash(chunk.chunk_id) % (10**9)
except (ValueError, AttributeError):
# Fallback to hash of content
import hashlib
point_id = int(hashlib.md5(chunk.content.encode()).hexdigest(), 16) % (10**9)
# Prepare the point for Qdrant
points = [
models.PointStruct(
id=point_id,
vector=chunk.embedding,
payload={
"content": chunk.content,
"doc_path": chunk.doc_path,
"metadata": chunk.metadata
}
)
]
# Upload the point to Qdrant
# Note: upsert doesn't accept timeout parameter, but uses the client's global timeout
self.client.upsert(
collection_name=self.collection_name,
points=points
)
return True
except Exception as e:
logging.error(f"Failed to store document chunk: {e}")
return False
def store_document_chunks(self, chunks: List[DocumentChunk]) -> bool:
"""
Store multiple document chunks in the vector database using batch operations.
Args:
chunks: List of DocumentChunk objects to store
Returns:
True if successful, False otherwise
"""
try:
if not chunks:
logging.warning("No chunks to store")
return True
# Validate all embeddings have correct dimensions
for chunk in chunks:
self.validate_embedding_dimensions(chunk.embedding)
# Prepare the points for Qdrant
points = []
for chunk in chunks:
# Ensure chunk_id is a proper integer or UUID
try:
point_id = int(chunk.chunk_id) if chunk.chunk_id.isdigit() else hash(chunk.chunk_id) % (10**9)
except (ValueError, AttributeError):
# Fallback to hash of content
import hashlib
point_id = int(hashlib.md5(chunk.content.encode()).hexdigest(), 16) % (10**9)
points.append(
models.PointStruct(
id=point_id,
vector=chunk.embedding,
payload={
"content": chunk.content,
"doc_path": chunk.doc_path,
"metadata": chunk.metadata
}
)
)
# Upload the points to Qdrant in batches for better performance
batch_size = 64 # Recommended batch size for performance
for i in range(0, len(points), batch_size):
batch = points[i:i + batch_size]
# Note: upsert doesn't accept timeout parameter, but uses the client's global timeout
self.client.upsert(
collection_name=self.collection_name,
points=batch
)
logging.info(f"Successfully stored {len(chunks)} document chunks in batch")
return True
except Exception as e:
logging.error(f"Failed to store document chunks: {e}")
return False
def search(self, query_embedding: List[float], limit: int = 5) -> List[Dict[str, Any]]:
"""
Search for similar document chunks based on the query embedding.
Args:
query_embedding: 384-dimensional embedding vector to search for
limit: Maximum number of results to return
Returns:
List of documents with similarity scores
"""
try:
# Validate embedding dimensions
self.validate_embedding_dimensions(query_embedding)
# Perform the search in Qdrant with timeout
# Use query_points method which is the new universal method for searching
search_results = self.client.query_points(
collection_name=self.collection_name,
query=query_embedding,
limit=limit,
timeout=self.timeout
)
# Format the results
results = []
for result in search_results.points:
results.append({
"content": result.payload["content"],
"doc_path": result.payload["doc_path"],
"metadata": result.payload.get("metadata", {}),
"score": result.score
})
return results
except Exception as e:
logging.error(f"Failed to search in vector store: {e}")
return []
def delete_collection(self) -> bool:
"""
Delete the entire collection (useful for re-indexing).
Returns:
True if successful, False otherwise
"""
try:
self.client.delete_collection(collection_name=self.collection_name, timeout=self.timeout)
logging.info(f"Deleted Qdrant collection '{self.collection_name}'")
return True
except Exception as e:
logging.error(f"Failed to delete collection: {e}")
return False
def count_documents(self) -> int:
"""
Count the total number of documents in the collection.
Returns:
Total number of documents in the collection
"""
try:
# Note: get_collection doesn't accept timeout parameter, but uses the client's global timeout
collection_info = self.client.get_collection(
collection_name=self.collection_name
)
return collection_info.points_count
except Exception as e:
logging.error(f"Failed to count documents in collection: {e}")
return 0
def health_check(self) -> Dict[str, Any]:
"""
Perform a health check on the Qdrant instance.
Returns:
Health status information
"""
try:
# Use info() method which provides version and other information about the instance
# Note: info() doesn't accept timeout parameter, but uses the client's global timeout
info = self.client.info()
return {
"status": "healthy",
"version": getattr(info, 'version', 'unknown'),
"commit": getattr(info, 'commit', 'unknown')
}
except Exception as e:
logging.error(f"Qdrant health check failed: {e}")
return {
"status": "unhealthy",
"error": str(e)
}