Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Evaluation harness — measures Precision@K for the hybrid RAG retrieval pipeline. | |
| Usage: | |
| python eval/eval_precision.py # default k=5 | |
| python eval/eval_precision.py --k 3 # precision at 3 | |
| python eval/eval_precision.py --verbose # show chunk previews for each query | |
| """ | |
| import json | |
| import sys | |
| import time | |
| import argparse | |
| from pathlib import Path | |
| ROOT = Path(__file__).parent.parent | |
| sys.path.insert(0, str(ROOT / "src")) | |
| from dotenv import load_dotenv | |
| import os | |
| load_dotenv(ROOT / ".env") | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") | |
| CROSS_ENCODER_MODEL = os.getenv("CROSS_ENCODER_MODEL", "cross-encoder/ms-marco-TinyBERT-L-2-v2") | |
| DENSE_K = int(os.getenv("DENSE_K", "10")) | |
| SPARSE_K = int(os.getenv("SPARSE_K", "10")) | |
| from search import HybridSearchIndex | |
| def load_queries(path: Path): | |
| queries = [] | |
| with open(path) as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| queries.append(json.loads(line)) | |
| return queries | |
| def run_evaluation(k: int = 5, verbose: bool = False): | |
| print("=" * 60) | |
| print("Production RAG — Retrieval Precision Evaluation") | |
| print("=" * 60) | |
| print(f"Model: {CROSS_ENCODER_MODEL}") | |
| print(f"dense_k: {DENSE_K} | sparse_k: {SPARSE_K} | rerank top_k: {k}") | |
| print() | |
| print("Loading index (warm start)...") | |
| index = HybridSearchIndex( | |
| persist_directory=str(ROOT / "chroma_store"), | |
| openai_api_key=OPENAI_API_KEY, | |
| cross_encoder_model=CROSS_ENCODER_MODEL, | |
| rerank_top_k=k, | |
| ) | |
| index.build_bm25_from_collection() | |
| total_chunks = index._collection.count() | |
| print(f"Index ready: {total_chunks} chunks\n") | |
| queries = load_queries(Path(__file__).parent / "eval_queries.jsonl") | |
| print(f"Running {len(queries)} evaluation queries (Precision@{k})\n") | |
| print("-" * 60) | |
| hits = 0 | |
| rerank_times = [] | |
| results = [] | |
| for q in queries: | |
| query = q["query"] | |
| expected_source = q["expected_source"] | |
| expected_fragment = q["expected_fragment"].lower() | |
| description = q.get("description", "") | |
| t0 = time.perf_counter() | |
| candidates = index.hybrid_search(query, dense_k=DENSE_K, sparse_k=SPARSE_K) | |
| t1 = time.perf_counter() | |
| top_chunks = index.re_rank(query, candidates) | |
| rerank_ms = (time.perf_counter() - t1) * 1000 | |
| rerank_times.append(rerank_ms) | |
| # HIT = at least one top-k chunk is from the expected source AND contains the expected fragment | |
| hit = any( | |
| c["source"] == expected_source and expected_fragment in c["text"].lower() | |
| for c in top_chunks | |
| ) | |
| hits += hit | |
| status = "HIT" if hit else "MISS" | |
| top_src = top_chunks[0]["source"] if top_chunks else "none" | |
| top_score = top_chunks[0].get("rerank_score", 0) if top_chunks else 0 | |
| print(f"[{status}] {query}") | |
| print(f" {description}") | |
| if not hit: | |
| print(f" Expected: {expected_source}") | |
| print(f" Got: {top_src} (score={top_score:.3f})") | |
| if verbose and top_chunks: | |
| print(f" Top chunk preview: \"{top_chunks[0]['text'][:100]}...\"") | |
| print(f" Rerank: {rerank_ms:.0f}ms | Candidates: {len(candidates)}") | |
| print() | |
| results.append({ | |
| "query": query, | |
| "description": description, | |
| "hit": hit, | |
| "rerank_ms": round(rerank_ms, 1), | |
| "top_source": top_src, | |
| }) | |
| precision = hits / len(queries) * 100 | |
| avg_rerank_ms = sum(rerank_times) / len(rerank_times) | |
| max_rerank_ms = max(rerank_times) | |
| print("=" * 60) | |
| print(f"RESULTS") | |
| print("=" * 60) | |
| print(f"Precision@{k}: {hits}/{len(queries)} = {precision:.1f}%") | |
| print(f"Target: >= 92.0%") | |
| precision_status = "PASS" if precision >= 92.0 else "BELOW TARGET" | |
| print(f"Precision status: {precision_status}") | |
| print() | |
| print(f"Avg rerank latency: {avg_rerank_ms:.0f}ms") | |
| print(f"Max rerank latency: {max_rerank_ms:.0f}ms") | |
| print(f"Latency target: <= 100ms") | |
| latency_status = "PASS" if max_rerank_ms <= 100 else f"{max_rerank_ms:.0f}ms > 100ms (above target)" | |
| print(f"Latency status: {latency_status}") | |
| print("=" * 60) | |
| return precision, avg_rerank_ms, results | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Evaluate RAG retrieval precision") | |
| parser.add_argument("--k", type=int, default=5, help="Top-k cutoff (default: 5)") | |
| parser.add_argument("--verbose", action="store_true", help="Show chunk text previews") | |
| args = parser.parse_args() | |
| run_evaluation(k=args.k, verbose=args.verbose) | |