local-rag-chatbot / vectorstore.py
Julien Simon
fix: Batch document additions to avoid ChromaDB size limit
35ad3d5
Raw
History Blame Contribute Delete
5.74 kB
"""Vectorstore management for document storage and retrieval."""
import glob
import os
from langchain_chroma import Chroma
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from config import CHROMA_PATH, CHUNK_OVERLAP, CHUNK_SIZE, PDF_PATH
def get_text_splitter():
"""Create text splitter with optimal settings.
Returns:
RecursiveCharacterTextSplitter: Configured text splitter
"""
return RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
add_start_index=True,
)
def get_pdf_files():
"""Get list of PDF files from the specified directory.
Returns:
List[str]: List of PDF file paths
"""
if not os.path.exists(PDF_PATH):
os.makedirs(PDF_PATH)
return []
return list(glob.glob(os.path.join(PDF_PATH, "*.pdf")))
def filter_metadata(doc):
"""Filter out unwanted sections from documents.
Args:
doc: Document object with metadata
Returns:
bool: True if document should be kept, False if filtered out
"""
skip_sections = {"references", "acknowledgments", "appendix"}
section = doc.metadata.get("section", "").lower()
return not any(s in section for s in skip_sections)
def process_documents(documents, text_splitter):
"""Process and filter documents into chunks.
Args:
documents: List of document objects
text_splitter: Text splitter instance
Returns:
List[Document]: Filtered document chunks
"""
chunks = text_splitter.split_documents(documents)
return [chunk for chunk in chunks if filter_metadata(chunk)]
def load_or_create_vectorstore(embeddings):
"""Load existing vectorstore or create a new one.
Args:
embeddings: Embedding model instance
Returns:
Chroma: Loaded or newly created vectorstore
"""
if os.path.exists(CHROMA_PATH):
return handle_existing_vectorstore(embeddings)
return create_new_vectorstore(embeddings)
def handle_existing_vectorstore(embeddings):
"""Handle loading and updating existing vectorstore.
Args:
embeddings: Embedding model instance
Returns:
Chroma: Loaded and potentially updated vectorstore
Exits if no PDF files are found.
"""
print("Loading existing Chroma database...")
vectorstore = Chroma(persist_directory=CHROMA_PATH, embedding_function=embeddings)
current_pdfs = get_pdf_files()
if not current_pdfs:
raise FileNotFoundError("No PDF files found in directory.")
collection = vectorstore.get()
if not collection or not collection.get("metadatas"):
processed_files = set()
else:
processed_files = {
meta.get("source")
for meta in collection["metadatas"]
if meta and meta.get("source")
}
new_pdfs = [pdf for pdf in current_pdfs if pdf not in processed_files]
if new_pdfs:
update_vectorstore(vectorstore, new_pdfs, processed_files)
else:
print("No new PDF files to process.")
return vectorstore
def add_documents_in_batches(vectorstore, documents, batch_size=5000):
"""Add documents to vectorstore in batches to avoid exceeding ChromaDB limits.
Args:
vectorstore: Chroma vectorstore instance
documents: List of documents to add
batch_size: Maximum documents per batch (ChromaDB limit is 5461)
"""
total = len(documents)
for i in range(0, total, batch_size):
batch = documents[i : i + batch_size]
print(f"Adding batch {i // batch_size + 1}/{(total + batch_size - 1) // batch_size} ({len(batch)} documents)...")
vectorstore.add_documents(batch)
def update_vectorstore(vectorstore, new_pdfs, processed_files):
"""Update existing vectorstore with new documents.
Args:
vectorstore: Existing Chroma vectorstore
new_pdfs: List of new PDF file paths
processed_files: Set of already processed file paths
"""
print(f"Found {len(new_pdfs)} new PDF files to process...")
loader = DirectoryLoader(PDF_PATH, glob="**/*.pdf", loader_cls=PyPDFLoader)
documents = loader.load()
new_documents = [
doc for doc in documents if doc.metadata.get("source") not in processed_files
]
filtered_chunks = process_documents(new_documents, get_text_splitter())
if filtered_chunks:
print(f"Adding {len(filtered_chunks)} new document chunks to existing database...")
add_documents_in_batches(vectorstore, filtered_chunks)
print("Database updated successfully!")
def create_new_vectorstore(embeddings):
"""Create a new vectorstore from documents.
Args:
embeddings: Embedding model instance
Returns:
Chroma: Newly created vectorstore
Exits if no PDF files are found.
"""
print("Creating new Chroma database...")
pdf_files = get_pdf_files()
if not pdf_files:
raise FileNotFoundError(
f"No PDF files found in '{PDF_PATH}' directory. "
f"Please add PDF files and run again."
)
print(f"Found {len(pdf_files)} PDF files to process...")
print("(This may take a while as documents need to be processed and embedded)")
os.makedirs(CHROMA_PATH, exist_ok=True)
loader = DirectoryLoader(PDF_PATH, glob="**/*.pdf", loader_cls=PyPDFLoader)
documents = loader.load()
filtered_chunks = process_documents(documents, get_text_splitter())
return Chroma.from_documents(
documents=filtered_chunks,
embedding=embeddings,
persist_directory=CHROMA_PATH,
)