| """ |
| Vector store management for document retrieval. |
| """ |
| import chromadb |
| from chromadb.config import Settings |
| from typing import List, Dict, Optional, Any |
| import uuid |
| from pathlib import Path |
| import json |
|
|
| class VectorStore: |
| """ |
| ChromaDB-based vector store for medical knowledge. |
| """ |
| |
| def __init__( |
| self, |
| collection_name: str = "medical_knowledge", |
| persist_directory: str = "data/knowledge_base", |
| embedding_function = None |
| ): |
| self.persist_directory = Path(persist_directory) |
| self.persist_directory.mkdir(parents=True, exist_ok=True) |
| |
| |
| try: |
| |
| self.client = chromadb.PersistentClient( |
| path=str(self.persist_directory) |
| ) |
| except (AttributeError, TypeError): |
| try: |
| |
| self.client = chromadb.PersistentClient( |
| path=str(self.persist_directory), |
| settings=Settings( |
| anonymized_telemetry=False, |
| allow_reset=True |
| ) |
| ) |
| except AttributeError: |
| |
| self.client = chromadb.Client(Settings( |
| chroma_db_impl="duckdb+parquet", |
| persist_directory=str(self.persist_directory), |
| anonymized_telemetry=False |
| )) |
| |
| self.collection_name = collection_name |
| |
| |
| |
| if embedding_function is None: |
| from chromadb.utils import embedding_functions |
| self.embedding_function = embedding_functions.SentenceTransformerEmbeddingFunction( |
| model_name="all-MiniLM-L6-v2" |
| ) |
| else: |
| self.embedding_function = embedding_function |
| |
| |
| self.collection = self.client.get_or_create_collection( |
| name=collection_name, |
| embedding_function=self.embedding_function, |
| metadata={"hnsw:space": "cosine"} |
| ) |
| |
| print(f"✅ Vector store initialized: {collection_name}") |
| print(f"📊 Documents in collection: {self.collection.count()}") |
| |
| def add_documents( |
| self, |
| documents: List[str], |
| embeddings: List[List[float]], |
| metadatas: Optional[List[Dict[str, Any]]] = None, |
| ids: Optional[List[str]] = None |
| ) -> List[str]: |
| """Add documents with embeddings to the store.""" |
| if ids is None: |
| ids = [str(uuid.uuid4()) for _ in documents] |
| |
| if metadatas is None: |
| metadatas = [{}] * len(documents) |
| |
| |
| clean_metadatas = [] |
| for meta in metadatas: |
| clean_meta = {} |
| for k, v in meta.items(): |
| if isinstance(v, (str, int, float, bool)): |
| clean_meta[k] = v |
| elif v is None: |
| clean_meta[k] = "" |
| else: |
| clean_meta[k] = str(v) |
| clean_metadatas.append(clean_meta) |
| |
| self.collection.add( |
| ids=ids, |
| embeddings=embeddings, |
| documents=documents, |
| metadatas=clean_metadatas |
| ) |
| |
| return ids |
| |
| def search( |
| self, |
| query_embedding: List[float], |
| n_results: int = 10, |
| filter_metadata: Optional[Dict] = None |
| ) -> Dict: |
| """Search for similar documents.""" |
| results = self.collection.query( |
| query_embeddings=[query_embedding], |
| n_results=n_results, |
| where=filter_metadata, |
| include=["documents", "metadatas", "distances"] |
| ) |
| return results |
| |
| def mmr_search( |
| self, |
| query_embedding: List[float], |
| k: int = 10, |
| fetch_k: int = 20, |
| lambda_mult: float = 0.5, |
| filter_metadata: Optional[Dict] = None |
| ) -> Dict: |
| """ |
| Maximal Marginal Relevance search for diverse results. |
| |
| MMR balances relevance to query with diversity among results. |
| |
| Args: |
| query_embedding: Query vector |
| k: Number of documents to return |
| fetch_k: Number of documents to fetch before MMR |
| lambda_mult: Balance between relevance and diversity |
| (0 = max diversity, 1 = max relevance) |
| filter_metadata: Optional metadata filter |
| |
| Returns: |
| Dict with documents, metadatas, and distances |
| """ |
| import numpy as np |
| |
| |
| results = self.collection.query( |
| query_embeddings=[query_embedding], |
| n_results=fetch_k, |
| where=filter_metadata, |
| include=["documents", "metadatas", "distances", "embeddings"] |
| ) |
| |
| if not results["documents"] or not results["documents"][0]: |
| return {"documents": [[]], "metadatas": [[]], "distances": [[]]} |
| |
| documents = results["documents"][0] |
| metadatas = results["metadatas"][0] |
| distances = results["distances"][0] |
| embeddings = results.get("embeddings", [[]])[0] |
| |
| if not embeddings or len(embeddings) == 0: |
| |
| return self.search(query_embedding, n_results=k, filter_metadata=filter_metadata) |
| |
| |
| query_vec = np.array(query_embedding) |
| doc_embeddings = np.array(embeddings) |
| |
| |
| query_similarities = 1 - np.array(distances) |
| |
| |
| selected_indices = [] |
| remaining_indices = list(range(len(documents))) |
| |
| while len(selected_indices) < k and remaining_indices: |
| mmr_scores = [] |
| |
| for idx in remaining_indices: |
| |
| relevance = query_similarities[idx] |
| |
| |
| if selected_indices: |
| selected_embeddings = doc_embeddings[selected_indices] |
| similarities = np.dot(selected_embeddings, doc_embeddings[idx]) |
| max_sim_to_selected = np.max(similarities) |
| else: |
| max_sim_to_selected = 0 |
| |
| |
| mmr_score = (lambda_mult * relevance - |
| (1 - lambda_mult) * max_sim_to_selected) |
| mmr_scores.append((idx, mmr_score)) |
| |
| |
| best_idx = max(mmr_scores, key=lambda x: x[1])[0] |
| selected_indices.append(best_idx) |
| remaining_indices.remove(best_idx) |
| |
| |
| return { |
| "documents": [[documents[i] for i in selected_indices]], |
| "metadatas": [[metadatas[i] for i in selected_indices]], |
| "distances": [[distances[i] for i in selected_indices]] |
| } |
| |
| def get_stats(self) -> Dict: |
| """Get statistics about the collection.""" |
| return { |
| "name": self.collection_name, |
| "count": self.collection.count() |
| } |
| |
| self.client.delete_collection(self.collection_name) |
| print(f"🗑️ Deleted collection: {self.collection_name}") |
|
|
| class QdrantVectorStore: |
| def __init__(self, url, api_key, collection_name="medical_knowledge"): |
| from qdrant_client import QdrantClient |
| |
| self.client = QdrantClient(url=url, api_key=api_key) |
| self.collection_name = collection_name |
| print(f"✅ Context: Connected to Qdrant Cloud: {collection_name}") |
| |
| def search(self, query_embedding, n_results=5, filter_metadata=None): |
| |
| response = self.client.query_points( |
| collection_name=self.collection_name, |
| query=query_embedding, |
| limit=n_results |
| ) |
| |
| |
| docs = [] |
| metadatas = [] |
| distances = [] |
| |
| for res in response.points: |
| docs.append(res.payload.get("page_content", "")) |
| metadatas.append({k:v for k,v in res.payload.items() if k != "page_content"}) |
| |
| |
| |
| distances.append(1 - res.score) |
| |
| return { |
| "documents": [docs], |
| "metadatas": [metadatas], |
| "distances": [distances] |
| } |
| |
| def get_stats(self): |
| try: |
| count = self.client.count(self.collection_name).count |
| return {"name": self.collection_name, "count": count} |
| except: |
| return {"name": self.collection_name, "count": 0} |
|
|
| def get_vector_store(): |
| |
| import os |
| if os.getenv("VECTOR_DB_TYPE") == "qdrant": |
| return QdrantVectorStore( |
| url=os.getenv("QDRANT_URL"), |
| api_key=os.getenv("QDRANT_API_KEY") |
| ) |
| return VectorStore() |
|
|