Spaces:
Sleeping
Sleeping
| """Measure the retriever against the generated dataset. | |
| Scores every retrieval mode on hit rate and MRR. | |
| Free to run, because embedding is local and no API calls are made, so we can | |
| re-run it after every retriever change. | |
| uv run python -m eval.run_eval | |
| uv run python -m eval.run_eval --retriever hybrid --retriever bm25 | |
| """ | |
| import argparse | |
| import json | |
| import statistics | |
| import sys | |
| from llama_index.core.evaluation import HitRate, MRR | |
| from rag.config import CHUNKS_PATH, EVAL_DATASET_PATH, RETRIEVAL_MODE, RETRIEVAL_TOP_K | |
| from rag.index import RETRIEVAL_MODES, load_retriever, make_embed_model | |
| from rag.types import EvalRow | |
| # What counts as correct: only the chunk the question came from, or any chunk of | |
| # the same section. We score both, because a neighbouring chunk often answers the | |
| # question just as well and a citation points at the section anyway. | |
| LEVELS = ("strict", "source") | |
| METRICS = tuple(f"{level}_{metric}" for level in LEVELS for metric in ("found", "rank")) | |
| # One score per question, per metric, averaged once every question is scored. | |
| Scores = dict[str, list[float]] | |
| HIT_RATE, RECIPROCAL_RANK = HitRate(), MRR() | |
| def rank_all(retriever, rows: list[EvalRow]) -> list[list[str]]: | |
| return [[node.node_id for node in retriever.retrieve(row["question"])] for row in rows] | |
| def same_source_ids() -> dict[str, list[str]]: | |
| by_url: dict[str, list[str]] = {} | |
| ids: dict[str, str] = {} | |
| with CHUNKS_PATH.open(encoding="utf-8") as handle: | |
| for line in handle: | |
| chunk = json.loads(line) | |
| url = chunk["metadata"]["url"] | |
| ids[chunk["id"]] = url | |
| by_url.setdefault(url, []).append(chunk["id"]) | |
| return {chunk_id: by_url[url] for chunk_id, url in ids.items()} | |
| def score_questions( | |
| rows: list[EvalRow], rankings: list[list[str]], expand: dict[str, list[str]] | |
| ) -> Scores: | |
| scores: Scores = {metric: [] for metric in METRICS} | |
| for row, ranked in zip(rows, rankings): | |
| retrieved = ranked[:RETRIEVAL_TOP_K] | |
| expected = {"strict": [row["chunk_id"]], "source": expand[row["chunk_id"]]} | |
| for level in LEVELS: | |
| ids = expected[level] | |
| scores[f"{level}_found"].append( | |
| HIT_RATE.compute(expected_ids=ids, retrieved_ids=retrieved).score | |
| ) | |
| scores[f"{level}_rank"].append( | |
| RECIPROCAL_RANK.compute(expected_ids=ids, retrieved_ids=retrieved).score | |
| ) | |
| return scores | |
| def report(scores: dict[str, Scores]) -> None: | |
| print(f"\n{'':12}{'exact':>18}{'section':>20}") | |
| print(f"{'':12}{'hit_rate':>9}{'mrr':>9}{'hit_rate':>11}{'mrr':>9}") | |
| for mode, metrics in scores.items(): | |
| marker = "*" if mode == RETRIEVAL_MODE else " " | |
| cells = "".join( | |
| f"{statistics.fmean(metrics[m]):>9.2f}" + (" " if m == "strict_rank" else "") | |
| for m in METRICS | |
| ) | |
| print(f"{marker} {mode:10}{cells}") | |
| print(f"\n* scored on the top {RETRIEVAL_TOP_K} results") | |
| def main(argv: list[str]) -> int: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument( | |
| "--retriever", action="append", choices=RETRIEVAL_MODES, | |
| help="modes to measure; repeatable, defaults to all", | |
| ) | |
| parser.add_argument("--rerank-model", help="override the cross-encoder for rerank mode") | |
| args = parser.parse_args(argv[1:]) | |
| modes = args.retriever or list(RETRIEVAL_MODES) | |
| if not EVAL_DATASET_PATH.exists(): | |
| print("No dataset. Run: uv run python -m eval.build_dataset", file=sys.stderr) | |
| return 2 | |
| rows = json.loads(EVAL_DATASET_PATH.read_text(encoding="utf-8")) | |
| # Same embedding model and query instruction the app uses | |
| print(f"{len(rows)} questions | query instruction: {make_embed_model().query_instruction!r}") | |
| expand = same_source_ids() | |
| scores: dict[str, Scores] = {} | |
| for mode in modes: | |
| retriever = load_retriever(mode, RETRIEVAL_TOP_K, None, args.rerank_model) | |
| scores[mode] = score_questions(rows, rank_all(retriever, rows), expand) | |
| print(f" scored {mode}", flush=True) | |
| report(scores) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main(sys.argv)) | |