All courses · Course overview · Lesson sequence
Skip to lesson textproject/retrieval.py
"""Inspectable local retrieval baseline. No models, APIs, or network calls."""
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
import json
import math
from pathlib import Path
import re
STOP_WORDS = frozenset("a an and are as at be can do for from how i in is it may of on or the to what when where which with".split())
def tokens(text: str) -> list[str]:
"""ASCII English words, lowercased; no stemming or semantic understanding."""
return [word for word in re.findall(r"[a-z0-9]+", text.lower()) if word not in STOP_WORDS]
@dataclass(frozen=True)
class Document:
id: str
title: str
text: str
source: str
version: str
status: str
audience: str
def load_documents(path: Path) -> list[Document]:
records = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(records, list):
raise ValueError("corpus must be a JSON list")
documents = []
fields = set(Document.__dataclass_fields__)
for record in records:
if not isinstance(record, dict) or set(record) != fields:
raise ValueError("each document must have exactly the documented fields")
if any(not isinstance(value, str) or not value.strip() for value in record.values()):
raise ValueError("document fields must be non-empty strings")
if record["status"] not in {"current", "archived"}:
raise ValueError("unknown document status")
if record["audience"] not in {"public", "staff"}:
raise ValueError("unknown audience")
documents.append(Document(**record))
if len({document.id for document in documents}) != len(documents):
raise ValueError("document IDs must be unique")
return documents
@dataclass(frozen=True)
class Hit:
document: Document
score: float
class Index:
"""TF-IDF and cosine over current public passages only.
This metadata policy is a toy corpus filter, not authentication or an
authorization system. Do not place private data in this learning project.
"""
def __init__(self, documents: list[Document]):
if len({document.id for document in documents}) != len(documents):
raise ValueError("document IDs must be unique")
self.documents = tuple(document for document in documents
if document.status == "current" and document.audience == "public")
bags = [Counter(tokens(document.text)) for document in self.documents]
frequency = Counter(term for bag in bags for term in bag)
self.idf = {term: math.log((len(bags) + 1) / (count + 1)) + 1
for term, count in frequency.items()}
self.vectors = tuple(self._vector(bag) for bag in bags)
def _vector(self, bag: Counter) -> dict[str, float]:
weights = {term: count * self.idf[term] for term, count in bag.items() if term in self.idf}
length = math.sqrt(sum(weight * weight for weight in weights.values()))
return {term: weight / length for term, weight in weights.items()} if length else {}
def search(self, question: str, k: int = 2) -> list[Hit]:
if isinstance(k, bool) or not isinstance(k, int) or k < 1:
raise ValueError("k must be a positive integer")
query = self._vector(Counter(tokens(question)))
hits = []
for document, vector in zip(self.documents, self.vectors):
score = sum(weight * vector.get(term, 0.0) for term, weight in query.items())
if score > 0:
hits.append(Hit(document, score))
return sorted(hits, key=lambda hit: (-hit.score, hit.document.id))[:k]
def evidence_packet(hits: list[Hit], max_quote_chars: int = 1200) -> dict:
"""Copy whole passages within a quote-character budget; never summarize."""
if isinstance(max_quote_chars, bool) or not isinstance(max_quote_chars, int) or max_quote_chars < 1:
raise ValueError("max_quote_chars must be a positive integer")
evidence = []
used = 0
for hit in hits:
document = hit.document
if used + len(document.text) > max_quote_chars:
continue
evidence.append({"id": document.id, "title": document.title, "quote": document.text,
"source": document.source, "version": document.version,
"retrieval_score": round(hit.score, 6)})
used += len(document.text)
return {"mode": "quote_only", "model_used": False,
"status": "evidence_found" if evidence else "no_evidence",
"warning": "A retrieved passage may not answer the question. A score is not confidence.",
"quote_characters": used, "evidence": evidence}
def retrieval_metrics(ranked_ids: list[str], relevant_ids: set[str], k: int) -> dict:
"""Recall@k and reciprocal rank@k for a question with labeled evidence."""
if isinstance(k, bool) or not isinstance(k, int) or k < 1:
raise ValueError("k must be a positive integer")
if not relevant_ids:
raise ValueError("retrieval metrics need at least one relevant document")
if len(set(ranked_ids)) != len(ranked_ids):
raise ValueError("ranked IDs must be unique")
top = ranked_ids[:k]
first = next((rank for rank, doc_id in enumerate(top, 1) if doc_id in relevant_ids), None)
return {"recall_at_k": len(set(top) & relevant_ids) / len(relevant_ids),
"reciprocal_rank_at_k": 1 / first if first else 0.0}
def evaluate(index: Index, cases: list[dict], k: int = 2) -> dict:
"""Evaluate retrieval, separately accounting for questions with no evidence."""
# Validate k even when cases is empty.
index.search("", k)
ids = set()
eligible = {document.id for document in index.documents}
rows = []
for case in cases:
if (not isinstance(case, dict) or set(case) != {"id", "question", "relevant_ids", "note"}
or not isinstance(case["id"], str) or not case["id"]
or not isinstance(case["question"], str) or not case["question"].strip()
or not isinstance(case["note"], str)
or not isinstance(case["relevant_ids"], list)
or any(not isinstance(item, str) for item in case["relevant_ids"])):
raise ValueError("invalid evaluation case")
if case["id"] in ids:
raise ValueError("evaluation case IDs must be unique")
ids.add(case["id"])
relevant = set(case["relevant_ids"])
if len(relevant) != len(case["relevant_ids"]) or not relevant <= eligible:
raise ValueError("relevance labels must be unique eligible document IDs")
ranked = [hit.document.id for hit in index.search(case["question"], k)]
row = {"id": case["id"], "question": case["question"], "relevant_ids": sorted(relevant),
"retrieved_ids": ranked, "note": case["note"]}
if relevant:
row.update(retrieval_metrics(ranked, relevant, k))
else:
row["correct_retrieval_abstention"] = not ranked
rows.append(row)
answerable = [row for row in rows if row["relevant_ids"]]
absent = [row for row in rows if not row["relevant_ids"]]
return {"kind": "synthetic_offline_retrieval_evaluation", "k": k,
"answerable_count": len(answerable), "no_evidence_count": len(absent),
"mean_recall_at_k": (sum(row["recall_at_k"] for row in answerable) / len(answerable)
if answerable else None),
"mean_reciprocal_rank_at_k": (
sum(row["reciprocal_rank_at_k"] for row in answerable) / len(answerable)
if answerable else None),
"correct_retrieval_abstentions": sum(row["correct_retrieval_abstention"] for row in absent),
"false_evidence_returns": sum(not row["correct_retrieval_abstention"] for row in absent),
"cases": rows}