lumen-rag / scripts /benchmark.py
WickTech's picture
Space README + Gradio entrypoint
fd3cc1a verified
Raw
History Blame Contribute Delete
2.8 kB
#!/usr/bin/env python
"""Benchmark retrieval quality across pipeline configurations.
Runs the same labelled eval set (data/eval.jsonl, 20 docs / 19 questions)
through three configurations to quantify what chunking and hybrid retrieval
actually buy you:
1. naive — whole document as one chunk, vector search only
2. + chunking — sentence-aware chunking (size=120, overlap=20), vector only
3. + hybrid — same chunking, + BM25 and Reciprocal Rank Fusion
Prints a markdown table (paste straight into the README) and writes
benchmark_results.json alongside it.
Usage:
python scripts/benchmark.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
from lumen_rag.embeddings import get_embedder
from lumen_rag.eval import evaluate
from lumen_rag.eval.harness import load_cases
from lumen_rag.ingestion.loaders import _LOADERS, load_file
from lumen_rag.ingestion.pipeline import ingest_documents
from lumen_rag.retrieval import Retriever
from lumen_rag.store import VectorStore
DOCS_DIR = ROOT / "data" / "docs"
EVAL_PATH = ROOT / "data" / "eval.jsonl"
K = 5
CONFIGS = [
{"name": "naive (1 chunk/doc, vector-only)", "chunk_size": 100_000, "overlap": 0, "mode": "vector"},
{"name": "+ sentence chunking (vector-only)", "chunk_size": 120, "overlap": 20, "mode": "vector"},
{"name": "+ hybrid (BM25 + RRF)", "chunk_size": 120, "overlap": 20, "mode": "hybrid"},
]
def build_retriever(chunk_size: int, overlap: int) -> Retriever:
embedder = get_embedder()
store = VectorStore(dim=embedder.dim)
docs = [load_file(p) for p in sorted(DOCS_DIR.iterdir()) if p.suffix.lower() in _LOADERS]
ingest_documents(docs, store=store, embedder=embedder, chunk_size=chunk_size, overlap=overlap)
return Retriever(store, embedder)
def main() -> None:
cases = load_cases(EVAL_PATH)
rows = []
for cfg in CONFIGS:
retriever = build_retriever(cfg["chunk_size"], cfg["overlap"])
report = evaluate(retriever, cases, k=K)
d = report.as_dict()
rows.append({"config": cfg["name"], **d})
header = ["Configuration", "recall@5", "precision@5", "MRR", "nDCG@5", "hit rate"]
lines = [
"| " + " | ".join(header) + " |",
"|" + "---|" * len(header),
]
for r in rows:
lines.append(
"| {config} | {recall@k:.2f} | {precision@k:.2f} | {mrr:.2f} | {ndcg@k:.2f} | {hit_rate:.2f} |".format(
**r
)
)
table = "\n".join(lines)
print(table)
out = ROOT / "benchmark_results.json"
out.write_text(json.dumps({"k": K, "n_cases": len(cases), "results": rows}, indent=2))
print(f"\nWrote {out}")
if __name__ == "__main__":
main()