Download rag/vectorstore.py from ShaunGves/smartrag: direct link, hf CLI and curl.
- Browser
- Download file 5.32 kB
-
https://huggingface.co/spaces/ShaunGves/smartrag/resolve/main/rag/vectorstore.py
- Command line
-
hf download hf://spaces/ShaunGves/smartrag/rag/vectorstore.py
-
curl -L -o vectorstore.py https://huggingface.co/spaces/ShaunGves/smartrag/resolve/main/rag/vectorstore.py
5.32 kB
| """ | |
| rag/vectorstore.py | |
| Handles document ingestion, chunking, embedding, and retrieval | |
| using ChromaDB as the vector store. | |
| """ | |
| import logging | |
| from pathlib import Path | |
| from typing import List, Optional | |
| from langchain_core.documents import Document | |
| from langchain_text_splitters import RecursiveCharacterTextSplitter | |
| from langchain_community.document_loaders import ( | |
| DirectoryLoader, | |
| PyPDFLoader, | |
| TextLoader, | |
| WebBaseLoader, | |
| ) | |
| from langchain_chroma import Chroma | |
| from langchain_huggingface import HuggingFaceEmbeddings | |
| import sys | |
| sys.path.append(str(Path(__file__).parent.parent)) | |
| from config import cfg | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| log = logging.getLogger(__name__) | |
| def get_embedding_model() -> HuggingFaceEmbeddings: | |
| """Load the sentence-transformer embedding model.""" | |
| log.info(f"Loading embedding model: {cfg.model.embedding_model_id}") | |
| return HuggingFaceEmbeddings( | |
| model_name=cfg.model.embedding_model_id, | |
| model_kwargs={"device": "cpu"}, | |
| encode_kwargs={"normalize_embeddings": True}, # Cosine similarity ready | |
| ) | |
| def load_documents(source: str) -> List[Document]: | |
| """ | |
| Load documents from various sources. | |
| Args: | |
| source: Path to directory, PDF file, text file, or URL | |
| Returns: | |
| List of LangChain Document objects | |
| """ | |
| source_path = Path(source) | |
| if source_path.is_dir(): | |
| log.info(f"Loading documents from directory: {source}") | |
| loader = DirectoryLoader( | |
| str(source_path), | |
| glob="**/*.{txt,pdf,md}", | |
| loader_cls=TextLoader, | |
| show_progress=True, | |
| ) | |
| elif source_path.suffix == ".pdf": | |
| log.info(f"Loading PDF: {source}") | |
| loader = PyPDFLoader(str(source_path)) | |
| elif source_path.suffix in [".txt", ".md"]: | |
| log.info(f"Loading text file: {source}") | |
| loader = TextLoader(str(source_path)) | |
| elif source.startswith("http"): | |
| log.info(f"Loading URL: {source}") | |
| loader = WebBaseLoader(source) | |
| else: | |
| raise ValueError(f"Unsupported source: {source}") | |
| docs = loader.load() | |
| log.info(f"Loaded {len(docs)} documents") | |
| return docs | |
| def chunk_documents(docs: List[Document]) -> List[Document]: | |
| """Split documents into overlapping chunks for retrieval.""" | |
| splitter = RecursiveCharacterTextSplitter( | |
| chunk_size=cfg.rag.chunk_size, | |
| chunk_overlap=cfg.rag.chunk_overlap, | |
| separators=["\n\n", "\n", ". ", " ", ""], | |
| length_function=len, | |
| ) | |
| chunks = splitter.split_documents(docs) | |
| log.info(f"Split into {len(chunks)} chunks (size={cfg.rag.chunk_size}, overlap={cfg.rag.chunk_overlap})") | |
| return chunks | |
| def build_vectorstore( | |
| sources: Optional[List[str]] = None, | |
| docs: Optional[List[Document]] = None, | |
| ) -> Chroma: | |
| """ | |
| Build and persist a ChromaDB vector store from documents. | |
| Args: | |
| sources: List of file paths or URLs to index | |
| docs: Pre-loaded Document objects (alternative to sources) | |
| Returns: | |
| Initialized Chroma vector store | |
| """ | |
| cfg.ensure_dirs() | |
| if docs is None: | |
| if sources is None: | |
| raise ValueError("Provide either 'sources' or 'docs'") | |
| all_docs = [] | |
| for src in sources: | |
| all_docs.extend(load_documents(src)) | |
| docs = all_docs | |
| chunks = chunk_documents(docs) | |
| embeddings = get_embedding_model() | |
| log.info(f"Embedding {len(chunks)} chunks into ChromaDB...") | |
| vectorstore = Chroma.from_documents( | |
| documents=chunks, | |
| embedding=embeddings, | |
| persist_directory=cfg.rag.chroma_persist_dir, | |
| collection_name=cfg.rag.collection_name, | |
| ) | |
| vectorstore.persist() | |
| log.info(f"✅ Vector store saved to: {cfg.rag.chroma_persist_dir}") | |
| return vectorstore | |
| def load_vectorstore() -> Chroma: | |
| """Load an existing ChromaDB vector store from disk.""" | |
| persist_dir = cfg.rag.chroma_persist_dir | |
| if not Path(persist_dir).exists(): | |
| raise FileNotFoundError( | |
| f"No vector store found at {persist_dir}. " | |
| "Run build_vectorstore() first." | |
| ) | |
| embeddings = get_embedding_model() | |
| vectorstore = Chroma( | |
| persist_directory=persist_dir, | |
| embedding_function=embeddings, | |
| collection_name=cfg.rag.collection_name, | |
| ) | |
| log.info(f"Loaded vector store with {vectorstore._collection.count():,} chunks") | |
| return vectorstore | |
| def retrieve(query: str, vectorstore: Chroma, top_k: Optional[int] = None) -> List[Document]: | |
| """ | |
| Retrieve the most relevant document chunks for a query. | |
| Args: | |
| query: User's question | |
| vectorstore: Initialized Chroma vector store | |
| top_k: Number of chunks to retrieve (defaults to cfg.rag.top_k) | |
| Returns: | |
| List of relevant Document chunks with similarity scores | |
| """ | |
| k = top_k or cfg.rag.top_k | |
| results = vectorstore.similarity_search_with_relevance_scores( | |
| query=query, | |
| k=k, | |
| ) | |
| # Filter by similarity threshold | |
| filtered = [ | |
| doc for doc, score in results | |
| if score >= cfg.rag.similarity_threshold | |
| ] | |
| log.debug(f"Retrieved {len(filtered)}/{k} chunks above threshold={cfg.rag.similarity_threshold}") | |
| return filtered | |