"""Vector store module for ChromaDB operations.""" import logging from pathlib import Path from typing import Any import chromadb from langchain_chroma import Chroma from langchain_core.documents import Document from langchain_huggingface import HuggingFaceEmbeddings from .config_loader import get_config logger = logging.getLogger(__name__) class VectorStoreManager: """Manage ChromaDB vector store for document embeddings.""" def __init__(self): """Initialize vector store manager with configuration.""" self.config = get_config() # Get embedding configuration embedding_model = self.config.get( "embeddings.model_name", "sentence-transformers/all-MiniLM-L6-v2" ) device = self.config.get("embeddings.device", "cpu") # Initialize embeddings # low_cpu_mem_usage=False avoids "Cannot copy out of meta tensor" error when # transformers/accelerate loads models with meta device placement (Docker/constrained envs) logger.info(f"Initializing embeddings model: {embedding_model}") self.embeddings = HuggingFaceEmbeddings( model_name=embedding_model, model_kwargs={ "device": device, "model_kwargs": {"low_cpu_mem_usage": False}, }, encode_kwargs={"normalize_embeddings": True}, ) # Get vector store configuration self.persist_directory = self.config.get_env("CHROMA_PERSIST_DIR", "./chroma_db") self.collection_name = self.config.get("vectorstore.collection_name", "profile_documents") # Ensure persist directory exists Path(self.persist_directory).mkdir(parents=True, exist_ok=True) self.vectorstore: Chroma | None = None def create_vectorstore(self, documents: list[Document]) -> Chroma: """Create a new vector store from documents. Args: documents: List of Document objects to embed Returns: Chroma vector store instance """ if not documents: msg = "No documents provided to create vector store" raise ValueError(msg) logger.info(f"Creating vector store with {len(documents)} documents...") try: self.vectorstore = Chroma.from_documents( documents=documents, embedding=self.embeddings, collection_name=self.collection_name, persist_directory=self.persist_directory, ) logger.info(f"Vector store created successfully at {self.persist_directory}") return self.vectorstore except Exception as e: logger.error(f"Error creating vector store: {e}") raise def load_vectorstore(self) -> Chroma: """Load existing vector store from disk. Returns: Chroma vector store instance """ persist_dir = Path(self.persist_directory) if not persist_dir.exists(): msg = f"Vector store not found at {self.persist_directory}" raise FileNotFoundError(msg) logger.info(f"Loading vector store from {self.persist_directory}...") try: self.vectorstore = Chroma( collection_name=self.collection_name, embedding_function=self.embeddings, persist_directory=self.persist_directory, ) # Verify the vector store has documents collection_count = self.vectorstore._collection.count() logger.info(f"Vector store loaded with {collection_count} embeddings") return self.vectorstore except Exception as e: logger.error(f"Error loading vector store: {e}") raise def get_retriever(self, **kwargs: Any) -> Any: """Get retriever for the vector store. Args: **kwargs: Additional arguments for the retriever. ``search_type`` is accepted but must not be forwarded into Chroma ``search_kwargs`` (newer chromadb rejects it). Returns: Retriever instance """ if self.vectorstore is None: self.load_vectorstore() # search_type belongs to as_retriever(), not Collection.query() search_type = kwargs.pop( "search_type", self.config.get("retrieval.vector.search_type", "similarity"), ) search_kwargs_config = self.config.get("retrieval.vector.search_kwargs", {}) k = self.config.get("retrieval.vector.k", 4) # Build search_kwargs based on search_type if search_type == "mmr": filtered_kwargs = { "k": k, "fetch_k": search_kwargs_config.get("fetch_k", 20), "lambda_mult": search_kwargs_config.get("lambda_mult", 0.5), } else: filtered_kwargs = {"k": k} # Override with provided kwargs (search_type already popped) filtered_kwargs.update(kwargs) filtered_kwargs.pop("search_type", None) logger.debug( f"Creating retriever with search_type={search_type}, search_kwargs={filtered_kwargs}" ) return self.vectorstore.as_retriever(search_type=search_type, search_kwargs=filtered_kwargs) def add_documents(self, documents: list[Document]) -> None: """Add new documents to existing vector store. Args: documents: List of Document objects to add """ if self.vectorstore is None: self.load_vectorstore() logger.info(f"Adding {len(documents)} documents to vector store...") try: self.vectorstore.add_documents(documents) logger.info("Documents added successfully") except Exception as e: logger.error(f"Error adding documents: {e}") raise def delete_collection(self) -> None: """Delete the entire collection.""" logger.warning(f"Deleting collection: {self.collection_name}") try: client = chromadb.PersistentClient(path=self.persist_directory) client.delete_collection(name=self.collection_name) self.vectorstore = None logger.info("Collection deleted successfully") except Exception as e: logger.error(f"Error deleting collection: {e}") raise def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]: """Perform similarity search. Args: query: Query string k: Number of documents to return **kwargs: Additional search parameters Returns: List of similar documents """ if self.vectorstore is None: self.load_vectorstore() return self.vectorstore.similarity_search(query, k=k, **kwargs) def get_vectorstore_manager() -> VectorStoreManager: """Get singleton vector store manager instance.""" return VectorStoreManager()