| |
| """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() |
|
|