brain / benchmarks /search /cli.py
Leon4gr45's picture
Deploy complete BrainAPI source 81ad5ce1c9c1
eb60b6b verified
Raw History Blame Contribute Delete
58.1 kB
from __future__ import annotations
import argparse
import json
from pathlib import Path
from rich.console import Console
from rich.table import Table
from search.catalog import (
DOWNLOAD_ALL_CATALOGS,
SUPPORTED_CATALOGS,
catalog_jsonl_path,
catalog_overwrite_blocked,
prepare_catalog,
)
from search.client import BrainAPIClient, SearchDisabledError
from search.config import BENCHMARKS_ROOT, DATA_DIR, DEFAULT_BRAIN_ID, Settings, validate_brain_id
from search.dataset import dataset_stats, load_records, split_corpus
from search.evaluate import assert_wandsgraph_node_join, ensure_run_dir, evaluate_search
from search.mapping import load_interaction_rows
from search.report import print_report_table, write_report
console = Console()
def _parse_channels(args: argparse.Namespace) -> list[str] | None:
raw = getattr(args, "channels", None)
if not raw:
return None
items = [item.strip() for item in str(raw).split(",") if item.strip()]
return items or None
def _parse_label_list(raw: str | None) -> list[str] | None:
if not raw:
return None
items = [item.strip() for item in str(raw).split(",") if item.strip()]
return items or None
def _parse_extras(raw: str | None) -> dict[str, str] | None:
if not raw:
return None
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise SystemExit(f"--extras must be a JSON object: {exc}") from exc
if not isinstance(data, dict):
raise SystemExit("--extras must be a JSON object")
return {str(key): str(value) for key, value in data.items() if value is not None} or None
def _resolve_dataset_path(args: argparse.Namespace, settings: Settings) -> Path:
if getattr(args, "dataset", None):
return Path(args.dataset)
return settings.dataset_path
def _skip_enrichment(args: argparse.Namespace) -> bool:
return not bool(getattr(args, "enrich", False))
def _eval_kwargs(args: argparse.Namespace) -> dict:
interactions = None
path = getattr(args, "interactions", None)
if path:
interactions = load_interaction_rows(Path(path))
return {
"fusion": args.fusion,
"fusion_alpha": getattr(args, "fusion_alpha", None),
"rerank": args.rerank,
"mode": getattr(args, "mode", None) or "default",
"channels": _parse_channels(args),
"node_labels": _parse_label_list(getattr(args, "node_labels", None)),
"community_labels": _parse_label_list(getattr(args, "community_labels", None)),
"expand": getattr(args, "expand", None) or "none",
"skip_enrichment": _skip_enrichment(args),
"ingest_graph": bool(getattr(args, "ingest_graph", False)),
"skip_ingest": bool(getattr(args, "skip_ingest", False)),
"rank_pool": bool(getattr(args, "rank_pool", False)),
"personalize": bool(getattr(args, "personalize", False)),
"interactions": interactions,
"extras": _parse_extras(getattr(args, "extras", None)),
}
def cmd_download(args: argparse.Namespace, settings: Settings) -> int:
from search.config import DATA_DIR
from search.finetune_esci_4class import held_out_query_ids
names = [item.strip().lower() for item in str(args.name).split(",") if item.strip()]
if len(names) == 1 and names[0] in {"all", "*"}:
names = list(DOWNLOAD_ALL_CATALOGS)
unknown = [name for name in names if name not in SUPPORTED_CATALOGS]
if unknown:
console.print(
f"[red]Unknown dataset[/red] {unknown!r}. "
f"Supported: {', '.join(SUPPORTED_CATALOGS)}"
)
return 1
holdout: set[str] = set()
holdout_raw = str(getattr(args, "holdout_dataset", None) or "").strip()
if holdout_raw:
holdout_path = Path(holdout_raw)
if not holdout_path.is_absolute():
holdout_path = (BENCHMARKS_ROOT / holdout_path).resolve()
if not holdout_path.exists():
console.print(f"[red]Missing holdout dataset[/red] {holdout_path}")
return 1
holdout = held_out_query_ids(holdout_path)
console.print(f"[cyan]Holdout[/cyan] {holdout_path.name} n={len(holdout)}")
protected = {
(DATA_DIR / "search_esci.jsonl").resolve(),
(DATA_DIR / "search_esci_74.jsonl").resolve(),
}
timeout = 0
for name in names:
try:
default_out = catalog_jsonl_path(name, locale=args.locale)
except ValueError as exc:
console.print(f"[red]{exc}[/red]")
return 1
out = Path(args.out) if args.out and len(names) == 1 else default_out
if not out.is_absolute():
out = (BENCHMARKS_ROOT / out).resolve()
split = str(args.split or "test")
if getattr(args, "dry_stats", False):
if name != "jdsearch":
console.print("[red]--dry-stats is only implemented for --name jdsearch[/red]")
return 1
from search.jdsearch import collect_jdsearch_stats, print_jdsearch_stats
console.print(f"[cyan]JDsearch stats[/cyan] (no JSONL write)")
stats = collect_jdsearch_stats()
print_jdsearch_stats(stats)
timeout += 1
continue
if catalog_overwrite_blocked(out):
console.print(f"[red]Refusing to overwrite[/red] {out}")
return 1
if split == "train" and out.resolve() in protected:
console.print(
f"[red]Refusing to overwrite[/red] {out} with split=train. "
"Pass --out data/search_esci_ltr200.jsonl"
)
return 1
console.print(f"[cyan]Downloading[/cyan] {name} split={split} → {out}")
path = prepare_catalog(
name,
out_path=out,
force=args.force,
max_queries=args.max_queries,
max_docs=args.max_docs,
candidates_per_query=args.candidates_per_query,
locale=args.locale,
split=split,
holdout_qids=holdout or None,
)
rows = load_records(path)
stats = dataset_stats(rows)
console.print(f"[green]Ready[/green] {path}")
console.print({key: value for key, value in stats.items() if key != "doc_ids"})
timeout += 1
return 0 if timeout else 1
def cmd_dataset_stats(args: argparse.Namespace, settings: Settings) -> int:
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
rows = load_records(path)
stats = dataset_stats(rows)
table = Table(title=f"Search dataset — {path.name}")
table.add_column("field")
table.add_column("value")
table.add_row("n_docs", str(stats["n_docs"]))
table.add_row("n_queries", str(stats["n_queries"]))
table.add_row("slices", json.dumps(stats["slices"]))
table.add_row("graded", str(stats.get("graded")))
console.print(table)
return 0
def cmd_smoke(args: argparse.Namespace, settings: Settings) -> int:
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
rows = load_records(path)
docs, queries = split_corpus(rows)
skip_ingest = bool(getattr(args, "skip_ingest", False))
if skip_ingest and settings.brain_id == "searchbenchwandsgraph":
queries = queries[: max(1, min(args.limit_queries, len(queries)))]
else:
docs = docs[: args.limit]
queries = queries[: max(1, min(args.limit_queries, len(queries)))]
limited = [{"type": "doc", **doc} for doc in docs] + [
{"type": "query", **query} for query in queries
]
console.print(
f"[cyan]Smoke[/cyan] brain={settings.brain_id} "
f"docs={len(docs)} queries={len(queries)}"
)
with BrainAPIClient(settings) as client:
try:
result = evaluate_search(
client,
limited,
ks=(5, 10, 20),
timeout_s=args.timeout,
k=args.k,
dataset_name=path.name,
**_eval_kwargs(args),
)
except SearchDisabledError as exc:
console.print(f"[red]{exc}[/red]")
return 1
if result.get("search_error"):
console.print(f"[red]{result['search_error']}[/red]")
return 1
if result.get("ingest", {}).get("status") not in {"completed", "partial_failed"}:
console.print("[red]Smoke ingest failed[/red]")
console.print(result.get("ingest"))
return 1
if int(result.get("n_docs_mapped") or 0) < 1:
console.print("[red]Smoke could not map DOCID markers to chunks[/red]")
return 1
if int(result.get("n_queries") or 0) < 1:
console.print("[red]Smoke search returned no scored queries[/red]")
return 1
try:
join = assert_wandsgraph_node_join(client, result)
except SystemExit as exc:
console.print(f"[red]{exc}[/red]")
return 1
if not join.get("skipped"):
console.print(
f"node_id={join.get('node_id')} neighbors={join.get('status_code')} "
f"count={join.get('neighbor_count')}"
)
first = (result.get("queries") or [{}])[0]
console.print(
f"retrieve/search({first.get('qid')}) -> {first.get('n_hits')} hits "
f"recall@10={((first.get('metrics') or {}).get('recall@10'))}"
)
console.print("[green]Smoke passed[/green]")
return 0
def cmd_backfill_entity_text(args: argparse.Namespace, settings: Settings) -> int:
from search.backfill_entities import apply_entity_text_backfill, refuse_entity_backfill
from search.mapping import catalog_entity_backfill_rows
try:
brain_id = refuse_entity_backfill(settings.brain_id)
except SystemExit as exc:
console.print(f"[red]{exc}[/red]")
return 1
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
docs, _ = split_corpus(load_records(path))
limit = getattr(args, "limit", None)
if limit:
docs = docs[: int(limit)]
rows = catalog_entity_backfill_rows(docs)
console.print(
f"[cyan]ENTITY backfill[/cyan] brain={brain_id} docs={len(rows)}"
)
import os
from dotenv import load_dotenv
for env_path in (
Path.home() / ".brainapi" / "source" / ".env",
Path(__file__).resolve().parents[2] / ".env",
):
if env_path.exists():
load_dotenv(env_path, override=False)
os.environ.setdefault("DATA_DB", "postgresql")
os.environ.setdefault("VECTOR_DB", "postgresql")
os.environ.setdefault("GRAPH_DB", "networkx")
os.environ.setdefault("SEARCH_ENABLED", "true")
os.environ.setdefault("ENV", "development")
from src.core.instances import (
embeddings_adapter,
graph_adapter,
vector_store_adapter,
)
summary = apply_entity_text_backfill(
brain_id=brain_id,
rows=rows,
graph=graph_adapter,
embeddings=embeddings_adapter,
vector_store=vector_store_adapter,
)
console.print(summary)
if int(summary.get("updated") or 0) < 1:
console.print("[red]No ENTITY nodes updated[/red]")
return 1
console.print("[green]ENTITY backfill done[/green]")
return 0
def cmd_evaluate(args: argparse.Namespace, settings: Settings) -> int:
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in args.ks.split(",") if x.strip()) or (5, 10, 20)
console.print(
f"[cyan]Evaluating[/cyan] brain={settings.brain_id} "
f"dataset={path.name} run={run_id} fusion={args.fusion or 'rrf'}"
)
with BrainAPIClient(settings) as client:
try:
result = evaluate_search(
client,
rows,
ks=ks,
timeout_s=args.timeout,
k=args.k,
dataset_name=path.name,
limit_docs=getattr(args, "limit_docs", None),
limit_queries=getattr(args, "limit_queries", None),
**_eval_kwargs(args),
)
except SearchDisabledError as exc:
console.print(f"[red]{exc}[/red]")
return 1
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
if result.get("search_error"):
console.print(f"[red]{result['search_error']}[/red]")
return 0 if report.get("status") == "ok" else 1
def cmd_report(args: argparse.Namespace, settings: Settings) -> int:
run_dir = settings.runs_dir / args.run
eval_path = run_dir / "eval.json"
if not eval_path.exists():
console.print(f"[red]Missing[/red] {eval_path}")
return 1
eval_result = json.loads(eval_path.read_text(encoding="utf-8"))
report = write_report(run_dir, eval_result)
print_report_table(report)
return 0 if report.get("status") == "ok" else 1
def cmd_finetune_ce(args: argparse.Namespace, settings: Settings) -> int:
from search.finetune_esci_ce import finetune
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
out = Path(args.out)
console.print(
f"[cyan]Fine-tune CE[/cyan] dataset={path.name} out={out} "
f"label_mode={args.label_mode} fields={args.fields}"
)
meta = finetune(
jsonl_path=path,
out_dir=out,
max_pairs=int(args.max_pairs),
epochs=int(args.epochs),
batch_size=int(args.batch_size),
label_mode=str(args.label_mode),
fields=str(args.fields),
base=str(args.base),
lr=float(args.lr),
max_length=int(getattr(args, "max_length", 256)),
)
console.print(json.dumps(meta, indent=2))
return 0
def cmd_miss_strata(args: argparse.Namespace, settings: Settings) -> int:
from search.miss_strata import main_classify
path = _resolve_dataset_path(args, settings)
eval_path = settings.runs_dir / args.from_run / "eval.json"
if not eval_path.exists():
console.print(f"[red]Missing[/red] {eval_path}")
return 1
out = Path(args.out) if args.out else settings.runs_dir / args.from_run / "miss_strata.json"
taxonomy = main_classify(eval_path=eval_path, dataset_path=path, out_path=out)
table = Table(title=f"Miss strata {args.from_run}")
table.add_column("field")
table.add_column("value")
for key in (
"n_queries",
"n_gold",
"n_in_top10",
"n_in_11_to_k",
"n_missed",
"stratum_counts",
"n_total_miss",
"n_rewritable_total_miss",
"run_query_side",
"rewritable_qids",
"total_miss_qids",
):
table.add_row(key, str(taxonomy.get(key)))
console.print(table)
console.print(f"[green]Wrote[/green] {out}")
return 0
def cmd_query_rewrite(args: argparse.Namespace, settings: Settings) -> int:
from search.miss_strata import main_classify, write_rewritten_jsonl
path = _resolve_dataset_path(args, settings)
eval_path = settings.runs_dir / args.from_run / "eval.json"
if not eval_path.exists():
console.print(f"[red]Missing[/red] {eval_path}")
return 1
taxonomy = main_classify(eval_path=eval_path, dataset_path=path)
if not taxonomy.get("run_query_side"):
console.print(
"[yellow]No rewritable total-miss qids (skip Task 2).[/yellow] "
f"total_miss={taxonomy.get('total_miss_qids')}"
)
return 0
dest = Path(args.out) if args.out else path.with_name("search_esci_74_qrewrite.jsonl")
rows = load_records(path)
write_rewritten_jsonl(rows, taxonomy, dest)
console.print(
f"[green]Wrote[/green] {dest} qids={taxonomy.get('rewritable_qids')}"
)
return 0
def cmd_spell_normalize(args: argparse.Namespace, settings: Settings) -> int:
from search.miss_strata import write_spell_jsonl
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
dest = Path(args.out) if args.out else path.with_name("search_esci_es_spell.jsonl")
rows = load_records(path)
summary = write_spell_jsonl(rows, dest)
console.print(
f"[green]Wrote[/green] {dest} changed_queries={summary.get('n_queries_changed')}"
)
return 0
def cmd_finetune_dense(args: argparse.Namespace, settings: Settings) -> int:
from search.finetune_esci_dense import finetune_dense
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
eval_path = Path(args.from_eval) if args.from_eval else None
out = Path(args.out)
console.print(
f"[cyan]Fine-tune dense ANCE[/cyan] dataset={path.name} out={out} "
f"(not Reddy 0.857)"
)
meta = finetune_dense(
jsonl_path=path,
out_dir=out,
eval_path=eval_path,
max_pairs=int(args.max_pairs),
epochs=int(args.epochs),
batch_size=int(args.batch_size),
fields=str(args.fields),
base=str(args.base),
lr=float(args.lr),
max_length=int(args.max_length),
)
console.print(json.dumps(meta, indent=2))
return 0
def cmd_local_dense(args: argparse.Namespace, settings: Settings) -> int:
from search.local_dense import run_local_dense
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
model_name = str(args.model)
protocol = None
if "bge" in model_name.lower():
protocol = (
"local BGE-base dual-encoder over search_esci_74.jsonl; "
"not MiniLM ANCE; no query: prefix; not Reddy 0.857"
)
if "paraphrase-multilingual-minilm" in model_name.lower() or "italian_smoke" in path.name:
protocol = (
"local paraphrase-multilingual-MiniLM-L12-v2 over "
"search_italian_smoke.jsonl; pipeline only (hits returned); "
"not text-embedding-3-large quality; not Reddy; not ES n=62"
)
console.print(
f"[cyan]Local dense retrieve[/cyan] dataset={path.name} run={run_id} "
f"model={model_name} brain={settings.brain_id}"
)
result = run_local_dense(
rows,
model_name=str(args.model),
dataset_name=path.name,
k=int(args.k),
ks=ks,
brain_id=settings.brain_id,
protocol=protocol,
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def cmd_mine_retrieved_lists(args: argparse.Namespace, settings: Settings) -> int:
from search.mine_retrieved_lists import mine
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
out = Path(args.out)
console.print(
f"[cyan]Mine retrieved lists[/cyan] dataset={path.name} out={out} "
f"(unlabeled=I; hold out test qids)"
)
meta = mine(
jsonl_path=path,
out_path=out,
max_queries=int(args.max_queries),
k=int(args.k),
seed=int(args.seed),
)
console.print(json.dumps(meta, indent=2))
return 0
def cmd_export_hybrid_lists(args: argparse.Namespace, settings: Settings) -> int:
from search.export_hybrid_lists import PROTECTED_OUT_NAMES, export_hybrid_lists
from search.list_overlap import load_eval_run
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
holdout_raw = str(getattr(args, "holdout_dataset", None) or "").strip()
holdout = Path(holdout_raw) if holdout_raw else DATA_DIR / "search_esci_74.jsonl"
if not holdout.is_absolute():
holdout = (BENCHMARKS_ROOT / holdout).resolve()
if not holdout.exists():
console.print(f"[red]Missing holdout[/red] {holdout}")
return 1
out = Path(args.out)
if not out.is_absolute():
out = (BENCHMARKS_ROOT / out).resolve()
if out.name in PROTECTED_OUT_NAMES:
console.print(f"[red]Refusing to overwrite[/red] {out}")
return 1
run_dir = settings.runs_dir / str(args.from_run)
if not (run_dir / "eval.json").exists():
console.print(f"[red]Missing eval[/red] {run_dir / 'eval.json'}")
return 1
eval_result = load_eval_run(run_dir)
console.print(
f"[cyan]Export hybrid lists[/cyan] from={args.from_run} "
f"dataset={path.name} holdout={holdout.name} out={out} "
f"(unlabeled=I; source=hybrid-k50)"
)
try:
meta = export_hybrid_lists(
eval_result=eval_result,
dataset_path=path,
holdout_path=holdout,
out_path=out,
k=int(args.k),
)
except ValueError as exc:
console.print(f"[red]{exc}[/red]")
return 1
console.print(json.dumps(meta, indent=2))
return 0
def cmd_finetune_4class(args: argparse.Namespace, settings: Settings) -> int:
from search.finetune_esci_4class import finetune
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
lists_path = Path(args.from_lists) if args.from_lists else None
if lists_path is not None and not lists_path.exists():
console.print(f"[red]Missing[/red] {lists_path}")
return 1
out = Path(args.out)
if not out.is_absolute():
out = (BENCHMARKS_ROOT / out).resolve()
base_model = str(args.base)
base_path = Path(base_model)
if not base_path.is_absolute():
maybe_base = (BENCHMARKS_ROOT / base_path).resolve()
if maybe_base.exists():
base_path = maybe_base
base_model = str(base_path)
if out.exists() and base_path.exists() and out.resolve() == base_path.resolve():
console.print(f"[red]Refusing to overwrite base checkpoint[/red] {out}")
return 1
lists_source = str(getattr(args, "lists_source", None) or "").strip()
if lists_path is not None:
source = lists_source or "retrieved-bm25"
else:
source = "pool"
console.print(
f"[cyan]Fine-tune 4-class CE[/cyan] dataset={path.name} out={out} "
f"source={source}"
)
meta = finetune(
jsonl_path=path,
out_dir=out,
max_pairs=int(args.max_pairs),
epochs=int(args.epochs),
batch_size=int(args.batch_size),
max_length=int(args.max_length),
base_model=base_model,
seed=int(args.seed),
ckpt_every=int(args.ckpt_every),
use_class_weights=bool(args.class_weights),
lists_path=lists_path,
lists_source=source if lists_path is not None else None,
)
console.print(json.dumps(meta, indent=2))
return 0
def cmd_colbert_local(args: argparse.Namespace, settings: Settings) -> int:
from search.local_colbert import run_local_colbert
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
console.print(
f"[cyan]Local ColBERT MaxSim[/cyan] dataset={path.name} run={run_id} "
f"(not fused with passages; not Reddy 0.857)"
)
result = run_local_colbert(
rows,
dataset_name=path.name,
k=int(args.k),
ks=ks,
brain_id="harness-local-colbert",
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def _existing_path(path: Path) -> Path:
if path.exists():
return path
from search.config import BENCHMARKS_ROOT
alt = BENCHMARKS_ROOT / path
return alt if alt.exists() else path
def cmd_rank_corpus(args: argparse.Namespace, settings: Settings) -> int:
from search.rank_corpus import DEFAULT_MODEL, load_predict, run_exhaustive_ce
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
model_dir = _existing_path(Path(args.model) if args.model else DEFAULT_MODEL)
if not model_dir.exists():
console.print(f"[red]Missing model[/red] {model_dir}")
return 1
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
console.print(
f"[cyan]Exhaustive 4-class CE[/cyan] dataset={path.name} run={run_id} "
f"model={model_dir} (protocol exhaustive-catalog; not Reddy 0.857)"
)
predict, model_name = load_predict(model_dir, max_length=int(args.max_length))
result = run_exhaustive_ce(
rows,
predict=predict,
model_name=model_name,
dataset_name=path.name,
k=int(args.k),
ks=ks,
brain_id="harness-local-exhaustive",
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def cmd_list_overlap(args: argparse.Namespace, settings: Settings) -> int:
from search.list_overlap import load_eval_run, summarize_overlap
names = [item.strip() for item in str(args.against_runs).split(",") if item.strip()]
passages = load_eval_run(settings.runs_dir / args.passages_run)
passages["run_id"] = args.passages_run
sidecars: dict = {}
for name in names:
eval_result = load_eval_run(settings.runs_dir / name)
eval_result["run_id"] = name
sidecars[name] = eval_result
queries = None
dataset = getattr(args, "dataset", None)
if dataset:
path = Path(dataset)
if not path.exists():
path = _existing_path(path)
if path.exists():
_, queries = split_corpus(load_records(path))
summary = summarize_overlap(
passages,
sidecars,
k=int(args.k),
queries=queries,
)
run_id, run_dir = ensure_run_dir(settings, args.run)
out = run_dir / "overlap.json"
out.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
headline = {
name: {
"unique_gold_hits": row["unique_gold_hits"],
"queries_with_unique": row["queries_with_unique"],
"n_queries": row["n_queries"],
}
for name, row in summary["runs"].items()
}
console.print(json.dumps(headline, indent=2))
console.print(f"[green]Wrote[/green] {out}")
return 0
def cmd_union_lists(args: argparse.Namespace, settings: Settings) -> int:
from search.list_overlap import load_eval_run, run_union
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
names = [item.strip() for item in str(args.from_runs).split(",") if item.strip()]
evals = []
for name in names:
eval_result = load_eval_run(settings.runs_dir / name)
eval_result["run_id"] = name
evals.append(eval_result)
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
console.print(
f"[cyan]Harness RRF union[/cyan] from={names} dataset={path.name} "
f"run={run_id} (not live graph; not Reddy 0.857)"
)
result = run_union(
evals,
rows,
dataset_name=path.name,
k=int(args.k),
ks=ks,
brain_id="harness-local-union",
run_names=names,
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def cmd_cascade_lists(args: argparse.Namespace, settings: Settings) -> int:
from search.list_overlap import load_eval_run, run_cascade
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
passages = load_eval_run(settings.runs_dir / args.passages_run)
passages["run_id"] = args.passages_run
names = [item.strip() for item in str(args.from_runs).split(",") if item.strip()]
sidecars = []
for name in names:
eval_result = load_eval_run(settings.runs_dir / name)
eval_result["run_id"] = name
sidecars.append(eval_result)
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
console.print(
f"[cyan]Frozen-head cascade[/cyan] passages={args.passages_run} "
f"sidecars={names} head_k={args.head_k} dataset={path.name} "
f"run={run_id} (not live graph; not Reddy 0.857)"
)
result = run_cascade(
passages,
sidecars,
rows,
dataset_name=path.name,
k=int(args.k),
head_k=int(args.head_k),
ks=ks,
brain_id="harness-local-cascade",
run_names=[args.passages_run, *names],
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def cmd_ltr_head(args: argparse.Namespace, settings: Settings) -> int:
from search.list_overlap import load_eval_run
from search.ltr_head import HEAD_RANKNET, PAIR_UNLABELED_ZERO, ce_cache_name, run_ltr_head
path = _resolve_dataset_path(args, settings)
if not path.exists():
if not path.is_absolute():
alt = (BENCHMARKS_ROOT / path).resolve()
if alt.exists():
path = alt
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
passages = load_eval_run(settings.runs_dir / args.from_run)
passages["run_id"] = args.from_run
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
policy = str(getattr(args, "pair_policy", None) or PAIR_UNLABELED_ZERO)
head = str(getattr(args, "ltr_model", None) or HEAD_RANKNET)
ce_raw = str(getattr(args, "ce_model", None) or "").strip()
ce_model = Path(ce_raw) if ce_raw else None
if ce_model is not None and not ce_model.is_absolute():
ce_model = (BENCHMARKS_ROOT / ce_model).resolve()
if ce_model is not None and not ce_model.exists():
console.print(f"[red]Missing CE model[/red] {ce_model}")
return 1
ce_cache = None
if ce_model is not None:
ce_cache = settings.runs_dir / args.from_run / ce_cache_name(ce_model)
train_run = str(getattr(args, "train_from_run", None) or "").strip()
train_eval = None
train_rows = None
train_ce_cache = None
train_dataset_name = None
if train_run:
train_ds = str(getattr(args, "train_dataset", None) or "").strip()
if not train_ds:
console.print("[red]--train-dataset is required with --train-from-run[/red]")
return 1
train_path = Path(train_ds)
if not train_path.is_absolute():
train_path = (BENCHMARKS_ROOT / train_path).resolve()
if not train_path.exists():
console.print(f"[red]Missing train dataset[/red] {train_path}")
return 1
train_eval = load_eval_run(settings.runs_dir / train_run)
train_eval["run_id"] = train_run
train_rows = load_records(train_path)
train_dataset_name = train_path.name
if ce_model is not None:
train_ce_cache = settings.runs_dir / train_run / ce_cache_name(ce_model)
mode = "apply" if train_run else "CV"
console.print(
f"[cyan]LTR head {mode}[/cyan] from={args.from_run} dataset={path.name} "
f"train={train_run or 'folds'} {train_dataset_name or ''} "
f"run={run_id} folds={args.folds} pair={policy} head={head} "
f"ce={ce_model or 'none'} (not live; not Reddy 0.857)"
)
result = run_ltr_head(
passages,
rows,
dataset_name=path.name,
k=int(args.k),
ks=ks,
n_folds=int(args.folds),
brain_id="harness-local-ltr",
source_run=args.from_run,
pair_policy=policy,
ce_model=ce_model,
ce_cache_path=ce_cache,
ltr_head=head,
train_eval_result=train_eval,
train_rows=train_rows,
train_source_run=train_run or None,
train_ce_cache_path=train_ce_cache,
)
report = write_report(run_dir, result)
print_report_table(report)
overlap = result.get("overlap_only_metrics") or {}
console.print(
f"overlap-only ndcg@10={overlap.get('ndcg@10')} "
f"recall@10={overlap.get('recall@10')}"
)
weights = result.get("ltr_mean_weights") or {}
if weights:
kind = "gain importances" if head == "lightgbm" else "weights"
console.print(f"mean CV {kind} {weights}")
if result.get("ltr_train_run"):
console.print(
f"trained on {result.get('ltr_n_train_queries')} queries "
f"from {result.get('ltr_train_run')}"
)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def cmd_pool_first_stage(args: argparse.Namespace, settings: Settings) -> int:
from search.pool_first_stage import main as pool_main
argv = ["--dataset", str(_resolve_dataset_path(args, settings))]
if args.run:
argv.extend(["--run", str(args.run)])
if args.variant:
argv.extend(["--variant", str(args.variant)])
if args.expand_out:
argv.extend(["--expand-out", str(args.expand_out)])
return int(pool_main(argv))
def cmd_rerank_retrieved(args: argparse.Namespace, settings: Settings) -> int:
from search.rerank_retrieved import run_ce_on_retrieved
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
source_dir = settings.runs_dir / args.from_run
eval_path = source_dir / "eval.json"
if not eval_path.exists():
console.print(f"[red]Missing[/red] {eval_path}")
return 1
eval_result = json.loads(eval_path.read_text(encoding="utf-8"))
eval_result["run_id"] = args.from_run
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
ks = None
if getattr(args, "ks", None):
ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip())
console.print(
f"[cyan]Harness CE on retrieved hits[/cyan] from={args.from_run} "
f"dataset={path.name} run={run_id} (not Reddy 0.857)"
)
result = run_ce_on_retrieved(
eval_result,
rows,
dataset_name=path.name,
ks=ks,
brain_id=settings.brain_id,
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
return 0 if report.get("status") == "ok" else 1
def cmd_rank_pool_ce(args: argparse.Namespace, settings: Settings) -> int:
from search.rank_pool import run_ce_on_pool
path = _resolve_dataset_path(args, settings)
if not path.exists():
console.print(f"[red]Missing[/red] {path}")
return 1
rows = load_records(path)
run_id, run_dir = ensure_run_dir(settings, args.run)
console.print(
f"[cyan]Ranking-in-pool CE[/cyan] dataset={path.name} run={run_id} "
f"(cite Reddy 0.857 only if protocol is CE-on-pool; n is not ~4477)"
)
result = run_ce_on_pool(
rows,
dataset_name=path.name,
brain_id=settings.brain_id,
)
report = write_report(run_dir, result)
print_report_table(report)
console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
if result.get("ce_model"):
console.print(
f"ce_model={result.get('ce_model')} "
f"missing_text={result.get('ce_missing_text')}"
)
return 0 if report.get("status") == "ok" else 1
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="search",
description=(
"Labeled search eval on searchbench*: ingest docs → "
"POST /retrieve/search (Recall/nDCG/MRR + retrieve latency)"
),
)
parser.add_argument(
"--env-file",
type=Path,
default=None,
help="Optional dotenv path (default benchmarks/.env)",
)
parser.add_argument(
"--brain",
default=None,
help=f"Brain id (default {DEFAULT_BRAIN_ID}; must start with searchbench)",
)
sub = parser.add_subparsers(dest="command", required=True)
p_download = sub.add_parser(
"download",
help="Download Amazon ESCI and/or WANDS into search JSONL",
)
p_download.add_argument(
"--name",
default="all",
help="esci, wands, jdsearch, or all (comma-separated; all = esci,wands)",
)
p_download.add_argument("--out", type=str, default=None)
p_download.add_argument("--force", action="store_true")
p_download.add_argument(
"--dry-stats",
action="store_true",
dest="dry_stats",
help="JDsearch only: print label histogram from the tar/cache without writing JSONL",
)
p_download.add_argument("--max-queries", type=int, default=80)
p_download.add_argument("--max-docs", type=int, default=2000)
p_download.add_argument("--candidates-per-query", type=int, default=40)
p_download.add_argument(
"--locale",
default="us",
help="ESCI locale: us, es, or jp (default us). Not Italian.",
)
p_download.add_argument(
"--split",
default="test",
help="ESCI split (default test; small_version / Task 1 only)",
)
p_download.add_argument(
"--holdout-dataset",
dest="holdout_dataset",
default="",
help="JSONL whose qids are skipped (e.g. data/search_esci_74.jsonl)",
)
p_download.set_defaults(func=cmd_download)
p_stats = sub.add_parser("dataset-stats", help="Summarize search JSONL")
p_stats.add_argument("--dataset", type=str, default=None)
p_stats.set_defaults(func=cmd_dataset_stats)
p_smoke = sub.add_parser(
"smoke", help="Ingest a few docs then POST /retrieve/search"
)
p_smoke.add_argument("--dataset", type=str, default=None)
p_smoke.add_argument("--limit", type=int, default=2)
p_smoke.add_argument("--limit-queries", type=int, default=2)
p_smoke.add_argument("--k", type=int, default=10)
p_smoke.add_argument("--fusion", choices=["rrf", "cc"], default=None)
p_smoke.add_argument("--fusion-alpha", dest="fusion_alpha", type=float, default=None)
p_smoke.add_argument(
"--rerank",
default=None,
help="none or plugin:<name> (e.g. plugin:cross-encoder). Unknown name is 400.",
)
p_smoke.add_argument(
"--channels",
default=None,
help=(
"Comma-separated channels (default passages): "
"passages,entities,events,communities and/or plugin:<name>."
),
)
p_smoke.add_argument("--node-labels", dest="node_labels", default=None)
p_smoke.add_argument("--community-labels", dest="community_labels", default=None)
p_smoke.add_argument(
"--expand",
choices=["none", "neighbors"],
default="none",
)
p_smoke.add_argument(
"--ingest-graph",
action="store_true",
help="Also write deterministic catalog triples (entity uuid = doc_id).",
)
p_smoke.add_argument(
"--interactions",
type=str,
default=None,
help="Optional interaction JSONL (EVENT+happened_at) on this searchbench brain.",
)
p_smoke.add_argument("--timeout", type=float, default=600.0)
p_smoke.add_argument(
"--enrich",
action="store_true",
help="Run Scout/Architect LLM ingest. Default skips enrichment (chunk+embed only).",
)
p_smoke.add_argument(
"--skip-ingest",
action="store_true",
help="Reuse chunks already on this searchbench brain (no POST /ingest/).",
)
p_smoke.add_argument(
"--personalize",
action="store_true",
dest="personalize",
help="Send each query's target field on POST /retrieve/search. Default omit.",
)
p_smoke.add_argument(
"--target-from-query",
action="store_true",
dest="personalize",
help="Alias for --personalize.",
)
p_smoke.set_defaults(func=cmd_smoke)
p_bf = sub.add_parser(
"backfill-entity-text",
help="ENTITY-only search_text + nodes vector refresh (not architect, not frozen brains)",
)
p_bf.add_argument("--dataset", type=str, default="data/search_wands.jsonl")
p_bf.add_argument("--limit", type=int, default=None)
p_bf.set_defaults(func=cmd_backfill_entity_text)
p_eval = sub.add_parser(
"evaluate",
help="Ingest corpus, search all queries, write report + ledger",
)
p_eval.add_argument("--dataset", type=str, default=None)
p_eval.add_argument("--run", type=str, default=None)
p_eval.add_argument("--ks", type=str, default="5,10,20")
p_eval.add_argument("--k", type=int, default=20)
p_eval.add_argument("--fusion", choices=["rrf", "cc"], default=None)
p_eval.add_argument("--fusion-alpha", dest="fusion_alpha", type=float, default=None)
p_eval.add_argument(
"--rerank",
default=None,
help="none or plugin:<name> (e.g. plugin:cross-encoder). Unknown name is 400.",
)
p_eval.add_argument(
"--mode",
choices=["default", "catalog"],
default="default",
help="default keeps RERANK_MAX_K=10. catalog retrieves deeper and reranks up to 50.",
)
p_eval.add_argument(
"--channels",
default=None,
help=(
"Comma-separated channels (default passages): "
"passages,entities,events,communities and/or plugin:<name>."
),
)
p_eval.add_argument("--node-labels", dest="node_labels", default=None)
p_eval.add_argument("--community-labels", dest="community_labels", default=None)
p_eval.add_argument(
"--expand",
choices=["none", "neighbors"],
default="none",
)
p_eval.add_argument(
"--ingest-graph",
action="store_true",
help="Also write deterministic catalog triples (entity uuid = doc_id).",
)
p_eval.add_argument(
"--interactions",
type=str,
default=None,
help="Optional interaction JSONL (EVENT+happened_at) on this searchbench brain.",
)
p_eval.add_argument("--timeout", type=float, default=600.0)
p_eval.add_argument(
"--enrich",
action="store_true",
help="Run Scout/Architect LLM ingest. Default skips enrichment (chunk+embed only).",
)
p_eval.add_argument("--limit-docs", dest="limit_docs", type=int, default=None)
p_eval.add_argument("--limit-queries", dest="limit_queries", type=int, default=None)
p_eval.add_argument(
"--skip-ingest",
action="store_true",
help="Reuse chunks already on this searchbench brain (no POST /ingest/).",
)
p_eval.add_argument(
"--rank-pool",
action="store_true",
dest="rank_pool",
help=(
"Restrict hits to each query's candidate_doc_ids (including I) "
"and score pool nDCG@20. Do not average with shared-corpus nDCG@10."
),
)
p_eval.add_argument(
"--personalize",
action="store_true",
dest="personalize",
help="Send each query's target field on POST /retrieve/search. Default omit.",
)
p_eval.add_argument(
"--target-from-query",
action="store_true",
dest="personalize",
help="Alias for --personalize.",
)
p_eval.add_argument(
"--extras",
default=None,
help='Optional equality filter JSON object, e.g. {"locale":"it"}',
)
p_eval.set_defaults(func=cmd_evaluate)
p_report = sub.add_parser("report", help="Rebuild report from eval.json")
p_report.add_argument("--run", type=str, required=True)
p_report.set_defaults(func=cmd_report)
p_pool = sub.add_parser(
"rank-pool-ce",
help="Score labeled candidate pools with MiniLM CE (no search API)",
)
p_pool.add_argument("--dataset", type=str, default=None)
p_pool.add_argument("--run", type=str, default=None)
p_pool.set_defaults(func=cmd_rank_pool_ce)
p_rr = sub.add_parser(
"rerank-retrieved",
help="Harness CE over stored first-stage hits (not production RERANK_MAX_K)",
)
p_rr.add_argument("--from-run", dest="from_run", type=str, required=True)
p_rr.add_argument("--dataset", type=str, default=None)
p_rr.add_argument("--run", type=str, default=None)
p_rr.add_argument("--ks", type=str, default=None)
p_rr.set_defaults(func=cmd_rerank_retrieved)
p_ft = sub.add_parser(
"finetune-ce",
help="Fine-tune a pool CE on ESCI US train (test qids held out)",
)
p_ft.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_ft.add_argument(
"--out",
type=str,
default="data/models/esci-minilm-graded",
)
p_ft.add_argument("--max-pairs", type=int, default=80000)
p_ft.add_argument("--epochs", type=int, default=1)
p_ft.add_argument("--batch-size", type=int, default=32)
p_ft.add_argument(
"--label-mode",
choices=["binary", "graded", "multiclass"],
default="graded",
)
p_ft.add_argument("--fields", choices=["title", "catalog"], default="catalog")
p_ft.add_argument(
"--base",
default="cross-encoder/ms-marco-MiniLM-L-6-v2",
)
p_ft.add_argument("--lr", type=float, default=7e-6)
p_ft.add_argument("--max-length", type=int, default=256)
p_ft.set_defaults(func=cmd_finetune_ce)
p_ms = sub.add_parser(
"miss-strata",
help="Classify k=50 hits: head-ok / rank-too-low / total-miss",
)
p_ms.add_argument("--from-run", dest="from_run", type=str, required=True)
p_ms.add_argument("--dataset", type=str, default=None)
p_ms.add_argument("--out", type=str, default=None)
p_ms.set_defaults(func=cmd_miss_strata)
p_qw = sub.add_parser(
"query-rewrite",
help="Rewrite pathological total-miss queries only (skip esci-72)",
)
p_qw.add_argument("--from-run", dest="from_run", type=str, required=True)
p_qw.add_argument("--dataset", type=str, default=None)
p_qw.add_argument("--out", type=str, default=None)
p_qw.set_defaults(func=cmd_query_rewrite)
p_sp = sub.add_parser(
"spell-normalize",
help="Harness-only NFKC/punctuation/accent normalize of query text",
)
p_sp.add_argument("--dataset", type=str, default="data/search_esci_es.jsonl")
p_sp.add_argument("--out", type=str, default="data/search_esci_es_spell.jsonl")
p_sp.set_defaults(func=cmd_spell_normalize)
p_ftd = sub.add_parser(
"finetune-dense",
help="Fine-tune a dual encoder with k=50 hard-negative bank (test qids held out)",
)
p_ftd.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_ftd.add_argument(
"--out",
type=str,
default="data/models/esci-minilm-dense-ance",
)
p_ftd.add_argument(
"--from-eval",
type=str,
default="runs/search-esci-74-passages-k50/eval.json",
)
p_ftd.add_argument("--max-pairs", type=int, default=20000)
p_ftd.add_argument("--epochs", type=int, default=1)
p_ftd.add_argument("--batch-size", type=int, default=32)
p_ftd.add_argument("--fields", choices=["title", "catalog"], default="catalog")
p_ftd.add_argument(
"--base",
default="sentence-transformers/all-MiniLM-L6-v2",
)
p_ftd.add_argument("--lr", type=float, default=2e-5)
p_ftd.add_argument("--max-length", type=int, default=256)
p_ftd.set_defaults(func=cmd_finetune_dense)
p_ld = sub.add_parser(
"local-dense",
help="Retrieve the JSONL corpus with a local dual encoder (not BrainAPI)",
)
p_ld.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_ld.add_argument("--run", type=str, default="search-esci-74-dense-ance-k50")
p_ld.add_argument("--model", type=str, required=True)
p_ld.add_argument("--k", type=int, default=50)
p_ld.add_argument("--ks", type=str, default="5,10,20,50")
p_ld.set_defaults(func=cmd_local_dense)
p_mine = sub.add_parser(
"mine-retrieved-lists",
help="BM25 top-k train lists with ESCI labels; unlabeled=I",
)
p_mine.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_mine.add_argument("--out", type=str, default="data/esci_retrieved_lists.jsonl")
p_mine.add_argument("--max-queries", type=int, default=6000)
p_mine.add_argument("--k", type=int, default=50)
p_mine.add_argument("--seed", type=int, default=11)
p_mine.set_defaults(func=cmd_mine_retrieved_lists)
p_exh = sub.add_parser(
"export-hybrid-lists",
help="Export stored hybrid k=50 hits as 4-class rows; unlabeled=I",
)
p_exh.add_argument(
"--from-run",
dest="from_run",
default="search-esci-ltr200-passages-k50",
)
p_exh.add_argument("--dataset", type=str, default="data/search_esci_ltr200.jsonl")
p_exh.add_argument(
"--holdout-dataset",
dest="holdout_dataset",
default="data/search_esci_74.jsonl",
)
p_exh.add_argument("--out", type=str, default="data/esci_hybrid_lists_ltr200.jsonl")
p_exh.add_argument("--k", type=int, default=50)
p_exh.set_defaults(func=cmd_export_hybrid_lists)
p_ft4 = sub.add_parser(
"finetune-4class",
help="Fine-tune 4-class L-12 CE (pool or retrieved-bm25 lists)",
)
p_ft4.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_ft4.add_argument(
"--out",
type=str,
default="data/models/esci-minilm-l12-retrieved",
)
p_ft4.add_argument("--from-lists", type=str, default=None)
p_ft4.add_argument(
"--lists-source",
dest="lists_source",
default="",
help="Label for --from-lists (hybrid-k50 or retrieved-bm25)",
)
p_ft4.add_argument("--max-pairs", type=int, default=80000)
p_ft4.add_argument("--epochs", type=int, default=1)
p_ft4.add_argument("--batch-size", type=int, default=32)
p_ft4.add_argument("--max-length", type=int, default=192)
p_ft4.add_argument("--ckpt-every", type=int, default=200)
p_ft4.add_argument("--class-weights", action="store_true")
p_ft4.add_argument(
"--base",
default="cross-encoder/ms-marco-MiniLM-L-12-v2",
)
p_ft4.add_argument("--seed", type=int, default=11)
p_ft4.set_defaults(func=cmd_finetune_4class)
p_cb = sub.add_parser(
"colbert-local",
help="ColBERT MaxSim sidecar over the JSONL corpus (not BrainAPI)",
)
p_cb.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_cb.add_argument("--run", type=str, default="search-esci-74-colbert-k50")
p_cb.add_argument("--k", type=int, default=50)
p_cb.add_argument("--ks", type=str, default="5,10,20,50")
p_cb.set_defaults(func=cmd_colbert_local)
p_rc = sub.add_parser(
"rank-corpus",
help="Score every JSONL doc with 4-class CE (exhaustive-catalog, not production)",
)
p_rc.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_rc.add_argument("--run", type=str, default="search-esci-74-exhaustive-ce")
p_rc.add_argument(
"--model",
type=str,
default="data/models/esci-minilm-l12-4class-nowt-e2",
)
p_rc.add_argument("--k", type=int, default=50)
p_rc.add_argument("--ks", type=str, default="5,10,20,50")
p_rc.add_argument("--max-length", dest="max_length", type=int, default=192)
p_rc.set_defaults(func=cmd_rank_corpus)
p_ov = sub.add_parser(
"list-overlap",
help="Count gold ASINs in sidecar top-k missing from passages",
)
p_ov.add_argument(
"--passages-run",
dest="passages_run",
default="search-esci-74-passages-k50",
)
p_ov.add_argument(
"--against-runs",
dest="against_runs",
default="search-esci-74-bge-base-k50,search-esci-74-colbert-k50",
)
p_ov.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_ov.add_argument("--run", type=str, default="search-esci-74-list-overlap")
p_ov.add_argument("--k", type=int, default=50)
p_ov.set_defaults(func=cmd_list_overlap)
p_un = sub.add_parser(
"union-lists",
help="RRF-union stored first-stage lists (harness only, not live graph)",
)
p_un.add_argument(
"--from-runs",
dest="from_runs",
default="search-esci-74-passages-k50,search-esci-74-bge-base-k50",
)
p_un.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_un.add_argument("--run", type=str, default="search-esci-74-union-bge-k50")
p_un.add_argument("--k", type=int, default=50)
p_un.add_argument("--ks", type=str, default="5,10,20,50")
p_un.set_defaults(func=cmd_union_lists)
p_cs = sub.add_parser(
"cascade-lists",
help="Keep passages top-10; inject sidecar unique golds into ranks 11-50",
)
p_cs.add_argument(
"--passages-run",
dest="passages_run",
default="search-esci-74-passages-k50",
)
p_cs.add_argument(
"--from-runs",
dest="from_runs",
default="search-esci-74-bge-base-k50,search-esci-74-colbert-k50",
)
p_cs.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_cs.add_argument("--run", type=str, default="search-esci-74-cascade-tail-k50")
p_cs.add_argument("--k", type=int, default=50)
p_cs.add_argument("--head-k", dest="head_k", type=int, default=10)
p_cs.add_argument("--ks", type=str, default="5,10,20,50")
p_cs.set_defaults(func=cmd_cascade_lists)
p_ltr = sub.add_parser(
"ltr-head",
help="Query-grouped CV LTR on stored hybrid k=50 (BM25/dense rank + title overlap)",
)
p_ltr.add_argument(
"--from-run",
dest="from_run",
default="search-esci-74-passages-k50",
)
p_ltr.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_ltr.add_argument("--run", type=str, default="search-esci-74-ltr-head-k50")
p_ltr.add_argument("--k", type=int, default=50)
p_ltr.add_argument("--ks", type=str, default="5,10,20,50")
p_ltr.add_argument("--folds", type=int, default=5)
p_ltr.add_argument(
"--pair-policy",
dest="pair_policy",
default="unlabeled_zero",
choices=["unlabeled_zero", "other_query_neg"],
)
p_ltr.add_argument(
"--ce-model",
dest="ce_model",
default="",
help="Optional 4-class CE dir; ce_gain is a feature, not the ranker",
)
p_ltr.add_argument(
"--ltr-model",
dest="ltr_model",
default="ranknet",
choices=["ranknet", "lightgbm"],
help="Linear RankNet (default) or gated LightGBM lambdarank",
)
p_ltr.add_argument(
"--train-from-run",
dest="train_from_run",
default="",
help="Fit on this run's lists; apply to --from-run (no CV on eval qids)",
)
p_ltr.add_argument(
"--train-dataset",
dest="train_dataset",
default="",
help="JSONL for --train-from-run",
)
p_ltr.set_defaults(func=cmd_ltr_head)
p_fs = sub.add_parser(
"pool-first-stage",
help="Local fielded BM25 over the JSONL corpus (not Reddy 0.857)",
)
p_fs.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
p_fs.add_argument("--run", type=str, default="search-esci-74-fielded-bm25")
p_fs.add_argument(
"--variant",
default="all,title,title-boost,rm3,rm3-title-boost",
)
p_fs.add_argument(
"--expand-out",
type=str,
default="data/search_esci_74_rm3.jsonl",
)
p_fs.set_defaults(func=cmd_pool_first_stage)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
settings = Settings.load(args.env_file)
if args.brain:
settings.brain_id = validate_brain_id(args.brain)
return int(args.func(args, settings))
if __name__ == "__main__":
raise SystemExit(main())