| """Benchmark harness — Themis side. Run all bench_queries through the eSCR pipeline |
| (loads the persisted index ONCE), save top-K per query for scoring against CaseMine. |
| |
| Run on Thor: python3 19_benchmark_themis.py (expects bench_queries.json alongside) |
| Out: themis_bench_results.json [{id,intent,query,results:[{neutral_citation,case_name,date,disposition,rr,passage}]}] |
| """ |
| import json, os, re, time |
| from collections import defaultdict |
| import numpy as np, torch |
| from sentence_transformers import SentenceTransformer, CrossEncoder |
| from rank_bm25 import BM25Okapi |
|
|
| TOPK = int(os.getenv("TOPK", "10")); K = 60; CAND = 40 |
| BGE_Q = "Represent this sentence for searching relevant passages: " |
| def tok(s): return re.sub(r"[^a-z0-9 ]", " ", (s or "").lower()).split() |
|
|
| queries = json.load(open("bench_queries.json")) |
| print("loading index...", flush=True) |
| chunks = [json.loads(l) for l in open("escr_chunks.jsonl")] |
| texts = [c["text"] for c in chunks]; chunk_doc = [c["doc_id"] for c in chunks] |
| M = np.load("escr_vectors.npy") |
| meta = {} |
| for l in open("escr_meta.jsonl"): |
| m = json.loads(l); meta[m["doc_id"]] = m |
| st = SentenceTransformer("BAAI/bge-small-en-v1.5", device="cuda", model_kwargs={"torch_dtype": torch.bfloat16}) |
| ce = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2", device="cuda") |
| t = time.time(); bm25 = BM25Okapi([tok(t_) for t_ in texts]); print(f"index+bm25 ready ({len(chunks)} chunks) in {time.time()-t:.0f}s", flush=True) |
|
|
| def search(q): |
| qv = st.encode(BGE_Q + q, normalize_embeddings=True, convert_to_numpy=True).astype(np.float32) |
| dense = M @ qv |
| d_top = np.argpartition(-dense, 60)[:60]; d_top = d_top[np.argsort(-dense[d_top])] |
| bs = bm25.get_scores(tok(q)) |
| b_top = sorted(range(len(bs)), key=lambda i: bs[i], reverse=True)[:60] |
| sc = defaultdict(float) |
| for r, ci in enumerate(d_top): sc[int(ci)] += 1.0 / (K + r) |
| for r, ci in enumerate(b_top): |
| if bs[ci] > 0: sc[ci] += 1.0 / (K + r) |
| cand = [ci for ci, _ in sorted(sc.items(), key=lambda x: x[1], reverse=True)[:CAND]] |
| rr = ce.predict([(q, texts[ci]) for ci in cand]) |
| best = {} |
| for ci, s in zip(cand, rr): |
| d = chunk_doc[ci] |
| if d not in best or s > best[d][0]: best[d] = (float(s), ci) |
| out = [] |
| for d, (s, ci) in sorted(best.items(), key=lambda x: x[1][0], reverse=True)[:TOPK]: |
| m = meta.get(d, {}) |
| out.append({"neutral_citation": m.get("neutral_citation"), "case_name": m.get("case_name"), |
| "date": m.get("date"), "disposition": m.get("disposition"), |
| "rr": round(s, 2), "passage": re.sub(r"\s+", " ", texts[ci])[:350]}) |
| return out |
|
|
| results = [] |
| for qd in queries: |
| res = search(qd["query"]) |
| results.append({**qd, "results": res}) |
| print(f" {qd['id']:12s} top1: {res[0]['case_name'][:50] if res else '-'}", flush=True) |
| json.dump(results, open("themis_bench_results.json", "w"), ensure_ascii=False, indent=1) |
| print("wrote themis_bench_results.json", flush=True) |
|
|