themis / phase1 /scripts /18_query_escr.py
vg15o2's picture
Moonley backend (HF Space build)
1d9bd9b
Raw
History Blame Contribute Delete
2.76 kB
"""Run ONE query through the eSCR retrieval pipeline (loads the persisted index on Thor).
dense (BGE bf16, numpy cosine) + BM25 -> RRF -> cross-encoder rerank -> top-K docs.
Prints top-K as JSON: case_name, neutral_citation, date, disposition, bench_strength, passage.
Run on Thor: Q="your query" python3 18_query_escr.py
"""
import json, os, re, time
import numpy as np, torch
from sentence_transformers import SentenceTransformer, CrossEncoder
from rank_bm25 import BM25Okapi
QUERY = os.environ["Q"]; 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()
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
print(f"{len(chunks)} chunks, {len(meta)} docs", flush=True)
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"bm25 built in {time.time()-t:.0f}s", flush=True)
qv = st.encode(BGE_Q + QUERY, 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(QUERY))
b_top = sorted(range(len(bs)), key=lambda i: bs[i], reverse=True)[:60]
from collections import defaultdict
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([(QUERY, 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"),
"bench_strength": m.get("bench_strength"), "rr": round(s, 2),
"passage": re.sub(r"\s+", " ", texts[ci])[:300]})
print("RESULTS_JSON:" + json.dumps(out, ensure_ascii=False))
for i, o in enumerate(out):
print(f"{i+1}. [{o['neutral_citation']}] {o['case_name']} ({o['date']}, {o['disposition']}) rr={o['rr']}")