ChromaDB for comparison

A fair comparison needs a working opponent. We index the same text into ChromaDB with local embeddings so every question you ask the graph can also be asked of a vector retriever. The vector code here is intentionally minimal. A full reranking pipeline lives in the sibling rag-reranking-chromadb workshop.

Vector baseline pipeline

The same text that fed the graph also feeds the vector store, so every comparison starts from identical source material.

vector_store.py
python
import chromadb
from sentence_transformers import SentenceTransformer
from langchain_text_splitters import RecursiveCharacterTextSplitter

class VectorStore:
    def __init__(self, persist_directory=None, collection_name=None, embedding_model=None,
                 chunk_size=500, chunk_overlap=50):
        self.persist_directory = persist_directory or os.getenv("CHROMA_PATH", "chroma_db")
        self.collection_name = collection_name or os.getenv("CHROMA_COLLECTION", "graph_rag_baseline")
        self.embedding_model_name = embedding_model or os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")

        self._client = chromadb.PersistentClient(path=self.persist_directory)
        self._collection = self._client.get_or_create_collection(name=self.collection_name)
        self._embedder = SentenceTransformer(self.embedding_model_name)
        self._splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=["\n\n", "\n", " ", ""],
        )

all-MiniLM-L6-v2 is a small, fast Sentence Transformers model. It runs on CPU, produces 384-dim vectors, and is good enough for a baseline. Bigger models are better for production vector RAG; that is not what we are proving here.

vector_store.py
python
def index_text(self, text, source=None):
    chunks = self._splitter.split_text(text)
    if not chunks:
        return 0
    embeddings = self._embedder.encode(chunks, convert_to_tensor=False).tolist()
    base = f"{source or 'doc'}"
    offset = self._collection.count()
    ids = [f"{base}_{offset + i}" for i in range(len(chunks))]
    metadatas = [{"source": source or "", "chunk_index": i} for i in range(len(chunks))]
    self._collection.add(ids=ids, embeddings=embeddings, documents=chunks, metadatas=metadatas)
    return len(chunks)

def query(self, question, top_k=5):
    if self._collection.count() == 0:
        return []
    query_embedding = self._embedder.encode([question], convert_to_tensor=False).tolist()[0]
    results = self._collection.query(query_embeddings=[query_embedding], n_results=top_k)
    docs = results.get("documents") or [[]]
    return docs[0] if docs else []

Index and query are deliberately simple. No reranking, no hybrid search, no metadata filters. The point is a clean baseline so you can measure what the graph side adds on top.

Quiz: Quiz

Loading practice…