| import os |
| from langchain_chroma import Chroma |
| from langchain_ollama import OllamaEmbeddings |
| from langchain_community.retrievers import BM25Retriever |
| from langchain_classic.retrievers import EnsembleRetriever |
|
|
| class ChromaVectorStore: |
| """ |
| Concrete implementation of the VectorStore protocol using ChromaDB. |
| Handles semantic embedding generation and hybrid retrieval (BM25 + MMR) |
| for pulling clinical context during background analysis. |
| """ |
| def __init__(self, host: str, embedding_model: str, persist_dir: str, collection_name: str): |
| self.embeddings = OllamaEmbeddings( |
| model=embedding_model, |
| base_url=host, |
| ) |
|
|
| self.vectorstore = Chroma( |
| collection_name=collection_name, |
| embedding_function=self.embeddings, |
| persist_directory=persist_dir, |
| ) |
| count = self.vectorstore._collection.count() |
| if count == 0: |
| print( |
| "[RAG WARNING] ChromaDB collection is empty. " |
| "Run `python build_vectorDB.py` to populate it before starting a session." |
| ) |
| self.ensemble_retriever = None |
| else: |
| print("[RAG] Building BM25 index for Hybrid Retrieval...") |
| import pickle |
| bm25_cache_path = os.path.join(persist_dir, "bm25_index.pkl") |
| |
| bm25_retriever = None |
| if os.path.exists(bm25_cache_path): |
| print("[RAG] Loading BM25 index from local cache...") |
| try: |
| with open(bm25_cache_path, "rb") as f: |
| bm25_retriever = pickle.load(f) |
| except Exception as e: |
| print(f"[RAG] Failed to load BM25 cache: {e}. Rebuilding...") |
| bm25_retriever = None |
|
|
| if bm25_retriever is None: |
| print("[RAG] Building BM25 index from ChromaDB (this may take a moment for large databases)...") |
| |
| documents = [] |
| batch_size = 5000 |
| offset = 0 |
| while True: |
| batch = self.vectorstore.get(limit=batch_size, offset=offset) |
| batch_docs = batch.get("documents", []) |
| if not batch_docs: |
| break |
| documents.extend(batch_docs) |
| offset += batch_size |
| |
| if documents: |
| bm25_retriever = BM25Retriever.from_texts(documents) |
| bm25_retriever.k = min(8, len(documents)) |
| |
| |
| try: |
| with open(bm25_cache_path, "wb") as f: |
| pickle.dump(bm25_retriever, f) |
| print("[RAG] BM25 index cached successfully.") |
| except Exception as e: |
| print(f"[RAG WARNING] Failed to cache BM25 index: {e}") |
| |
| if bm25_retriever: |
| |
| mmr_retriever = self.vectorstore.as_retriever( |
| search_type="mmr", |
| search_kwargs={ |
| "k": bm25_retriever.k, |
| "fetch_k": max(20, bm25_retriever.k * 3), |
| "lambda_mult": 0.7 |
| } |
| ) |
| |
| |
| self.ensemble_retriever = EnsembleRetriever( |
| retrievers=[bm25_retriever, mmr_retriever], |
| weights=[0.5, 0.5] |
| ) |
| else: |
| self.ensemble_retriever = None |
|
|
| def retrieve(self, query: str, k: int = 8) -> str: |
| if not query or not query.strip(): |
| return "" |
|
|
| try: |
| if self.ensemble_retriever: |
| results = self.ensemble_retriever.invoke(query) |
| |
| |
| return "\n\n".join([doc.page_content for doc in results[:k]]) |
| |
| |
| count = self.vectorstore._collection.count() |
| if count == 0: |
| return "" |
| |
| actual_fetch_k = min(30, count) |
| actual_k = min(k, actual_fetch_k) |
|
|
| results = self.vectorstore.max_marginal_relevance_search( |
| query, |
| k=actual_k, |
| fetch_k=actual_fetch_k, |
| lambda_mult=0.7, |
| ) |
| return "\n\n".join([doc.page_content for doc in results]) |
|
|
| except Exception as e: |
| print(f"[RAG ERROR] Hybrid search failed: {e}. Attempting similarity fallback.") |
| try: |
| results = self.vectorstore.similarity_search(query, k=k) |
| return "\n\n".join([doc.page_content for doc in results]) |
| except Exception as e2: |
| print(f"[RAG ERROR] Similarity fallback also failed: {e2}") |
| return "" |
|
|