Leon4gr45 commited on
Commit
faab021
·
verified ·
1 Parent(s): 65e35a0

Delete benchmarks/search/cli.py

Browse files
Files changed (1) hide show
  1. benchmarks/search/cli.py +0 -1563
benchmarks/search/cli.py DELETED
@@ -1,1563 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- from pathlib import Path
6
-
7
- from rich.console import Console
8
- from rich.table import Table
9
-
10
- from search.catalog import (
11
- DOWNLOAD_ALL_CATALOGS,
12
- SUPPORTED_CATALOGS,
13
- catalog_jsonl_path,
14
- catalog_overwrite_blocked,
15
- prepare_catalog,
16
- )
17
- from search.client import BrainAPIClient, SearchDisabledError
18
- from search.config import BENCHMARKS_ROOT, DATA_DIR, DEFAULT_BRAIN_ID, Settings, validate_brain_id
19
- from search.dataset import dataset_stats, load_records, split_corpus
20
- from search.evaluate import assert_wandsgraph_node_join, ensure_run_dir, evaluate_search
21
- from search.mapping import load_interaction_rows
22
- from search.report import print_report_table, write_report
23
-
24
- console = Console()
25
-
26
-
27
- def _parse_channels(args: argparse.Namespace) -> list[str] | None:
28
- raw = getattr(args, "channels", None)
29
- if not raw:
30
- return None
31
- items = [item.strip() for item in str(raw).split(",") if item.strip()]
32
- return items or None
33
-
34
-
35
- def _parse_label_list(raw: str | None) -> list[str] | None:
36
- if not raw:
37
- return None
38
- items = [item.strip() for item in str(raw).split(",") if item.strip()]
39
- return items or None
40
-
41
-
42
- def _parse_extras(raw: str | None) -> dict[str, str] | None:
43
- if not raw:
44
- return None
45
- try:
46
- data = json.loads(raw)
47
- except json.JSONDecodeError as exc:
48
- raise SystemExit(f"--extras must be a JSON object: {exc}") from exc
49
- if not isinstance(data, dict):
50
- raise SystemExit("--extras must be a JSON object")
51
- return {str(key): str(value) for key, value in data.items() if value is not None} or None
52
-
53
-
54
- def _resolve_dataset_path(args: argparse.Namespace, settings: Settings) -> Path:
55
- if getattr(args, "dataset", None):
56
- return Path(args.dataset)
57
- return settings.dataset_path
58
-
59
-
60
- def _skip_enrichment(args: argparse.Namespace) -> bool:
61
- return not bool(getattr(args, "enrich", False))
62
-
63
-
64
- def _eval_kwargs(args: argparse.Namespace) -> dict:
65
- interactions = None
66
- path = getattr(args, "interactions", None)
67
- if path:
68
- interactions = load_interaction_rows(Path(path))
69
- return {
70
- "fusion": args.fusion,
71
- "fusion_alpha": getattr(args, "fusion_alpha", None),
72
- "rerank": args.rerank,
73
- "mode": getattr(args, "mode", None) or "default",
74
- "channels": _parse_channels(args),
75
- "node_labels": _parse_label_list(getattr(args, "node_labels", None)),
76
- "community_labels": _parse_label_list(getattr(args, "community_labels", None)),
77
- "expand": getattr(args, "expand", None) or "none",
78
- "skip_enrichment": _skip_enrichment(args),
79
- "ingest_graph": bool(getattr(args, "ingest_graph", False)),
80
- "skip_ingest": bool(getattr(args, "skip_ingest", False)),
81
- "rank_pool": bool(getattr(args, "rank_pool", False)),
82
- "personalize": bool(getattr(args, "personalize", False)),
83
- "interactions": interactions,
84
- "extras": _parse_extras(getattr(args, "extras", None)),
85
- }
86
-
87
-
88
- def cmd_download(args: argparse.Namespace, settings: Settings) -> int:
89
- from search.config import DATA_DIR
90
- from search.finetune_esci_4class import held_out_query_ids
91
-
92
- names = [item.strip().lower() for item in str(args.name).split(",") if item.strip()]
93
- if len(names) == 1 and names[0] in {"all", "*"}:
94
- names = list(DOWNLOAD_ALL_CATALOGS)
95
- unknown = [name for name in names if name not in SUPPORTED_CATALOGS]
96
- if unknown:
97
- console.print(
98
- f"[red]Unknown dataset[/red] {unknown!r}. "
99
- f"Supported: {', '.join(SUPPORTED_CATALOGS)}"
100
- )
101
- return 1
102
- holdout: set[str] = set()
103
- holdout_raw = str(getattr(args, "holdout_dataset", None) or "").strip()
104
- if holdout_raw:
105
- holdout_path = Path(holdout_raw)
106
- if not holdout_path.is_absolute():
107
- holdout_path = (BENCHMARKS_ROOT / holdout_path).resolve()
108
- if not holdout_path.exists():
109
- console.print(f"[red]Missing holdout dataset[/red] {holdout_path}")
110
- return 1
111
- holdout = held_out_query_ids(holdout_path)
112
- console.print(f"[cyan]Holdout[/cyan] {holdout_path.name} n={len(holdout)}")
113
- protected = {
114
- (DATA_DIR / "search_esci.jsonl").resolve(),
115
- (DATA_DIR / "search_esci_74.jsonl").resolve(),
116
- }
117
- timeout = 0
118
- for name in names:
119
- try:
120
- default_out = catalog_jsonl_path(name, locale=args.locale)
121
- except ValueError as exc:
122
- console.print(f"[red]{exc}[/red]")
123
- return 1
124
- out = Path(args.out) if args.out and len(names) == 1 else default_out
125
- if not out.is_absolute():
126
- out = (BENCHMARKS_ROOT / out).resolve()
127
- split = str(args.split or "test")
128
- if getattr(args, "dry_stats", False):
129
- if name != "jdsearch":
130
- console.print("[red]--dry-stats is only implemented for --name jdsearch[/red]")
131
- return 1
132
- from search.jdsearch import collect_jdsearch_stats, print_jdsearch_stats
133
-
134
- console.print(f"[cyan]JDsearch stats[/cyan] (no JSONL write)")
135
- stats = collect_jdsearch_stats()
136
- print_jdsearch_stats(stats)
137
- timeout += 1
138
- continue
139
- if catalog_overwrite_blocked(out):
140
- console.print(f"[red]Refusing to overwrite[/red] {out}")
141
- return 1
142
- if split == "train" and out.resolve() in protected:
143
- console.print(
144
- f"[red]Refusing to overwrite[/red] {out} with split=train. "
145
- "Pass --out data/search_esci_ltr200.jsonl"
146
- )
147
- return 1
148
- console.print(f"[cyan]Downloading[/cyan] {name} split={split} → {out}")
149
- path = prepare_catalog(
150
- name,
151
- out_path=out,
152
- force=args.force,
153
- max_queries=args.max_queries,
154
- max_docs=args.max_docs,
155
- candidates_per_query=args.candidates_per_query,
156
- locale=args.locale,
157
- split=split,
158
- holdout_qids=holdout or None,
159
- )
160
- rows = load_records(path)
161
- stats = dataset_stats(rows)
162
- console.print(f"[green]Ready[/green] {path}")
163
- console.print({key: value for key, value in stats.items() if key != "doc_ids"})
164
- timeout += 1
165
- return 0 if timeout else 1
166
-
167
-
168
- def cmd_dataset_stats(args: argparse.Namespace, settings: Settings) -> int:
169
- path = _resolve_dataset_path(args, settings)
170
- if not path.exists():
171
- console.print(f"[red]Missing[/red] {path}")
172
- return 1
173
- rows = load_records(path)
174
- stats = dataset_stats(rows)
175
- table = Table(title=f"Search dataset — {path.name}")
176
- table.add_column("field")
177
- table.add_column("value")
178
- table.add_row("n_docs", str(stats["n_docs"]))
179
- table.add_row("n_queries", str(stats["n_queries"]))
180
- table.add_row("slices", json.dumps(stats["slices"]))
181
- table.add_row("graded", str(stats.get("graded")))
182
- console.print(table)
183
- return 0
184
-
185
-
186
- def cmd_smoke(args: argparse.Namespace, settings: Settings) -> int:
187
- path = _resolve_dataset_path(args, settings)
188
- if not path.exists():
189
- console.print(f"[red]Missing[/red] {path}")
190
- return 1
191
- rows = load_records(path)
192
- docs, queries = split_corpus(rows)
193
- skip_ingest = bool(getattr(args, "skip_ingest", False))
194
- if skip_ingest and settings.brain_id == "searchbenchwandsgraph":
195
- queries = queries[: max(1, min(args.limit_queries, len(queries)))]
196
- else:
197
- docs = docs[: args.limit]
198
- queries = queries[: max(1, min(args.limit_queries, len(queries)))]
199
- limited = [{"type": "doc", **doc} for doc in docs] + [
200
- {"type": "query", **query} for query in queries
201
- ]
202
- console.print(
203
- f"[cyan]Smoke[/cyan] brain={settings.brain_id} "
204
- f"docs={len(docs)} queries={len(queries)}"
205
- )
206
- with BrainAPIClient(settings) as client:
207
- try:
208
- result = evaluate_search(
209
- client,
210
- limited,
211
- ks=(5, 10, 20),
212
- timeout_s=args.timeout,
213
- k=args.k,
214
- dataset_name=path.name,
215
- **_eval_kwargs(args),
216
- )
217
- except SearchDisabledError as exc:
218
- console.print(f"[red]{exc}[/red]")
219
- return 1
220
- if result.get("search_error"):
221
- console.print(f"[red]{result['search_error']}[/red]")
222
- return 1
223
- if result.get("ingest", {}).get("status") not in {"completed", "partial_failed"}:
224
- console.print("[red]Smoke ingest failed[/red]")
225
- console.print(result.get("ingest"))
226
- return 1
227
- if int(result.get("n_docs_mapped") or 0) < 1:
228
- console.print("[red]Smoke could not map DOCID markers to chunks[/red]")
229
- return 1
230
- if int(result.get("n_queries") or 0) < 1:
231
- console.print("[red]Smoke search returned no scored queries[/red]")
232
- return 1
233
- try:
234
- join = assert_wandsgraph_node_join(client, result)
235
- except SystemExit as exc:
236
- console.print(f"[red]{exc}[/red]")
237
- return 1
238
- if not join.get("skipped"):
239
- console.print(
240
- f"node_id={join.get('node_id')} neighbors={join.get('status_code')} "
241
- f"count={join.get('neighbor_count')}"
242
- )
243
- first = (result.get("queries") or [{}])[0]
244
- console.print(
245
- f"retrieve/search({first.get('qid')}) -> {first.get('n_hits')} hits "
246
- f"recall@10={((first.get('metrics') or {}).get('recall@10'))}"
247
- )
248
- console.print("[green]Smoke passed[/green]")
249
- return 0
250
-
251
-
252
- def cmd_backfill_entity_text(args: argparse.Namespace, settings: Settings) -> int:
253
- from search.backfill_entities import apply_entity_text_backfill, refuse_entity_backfill
254
- from search.mapping import catalog_entity_backfill_rows
255
-
256
- try:
257
- brain_id = refuse_entity_backfill(settings.brain_id)
258
- except SystemExit as exc:
259
- console.print(f"[red]{exc}[/red]")
260
- return 1
261
- path = _resolve_dataset_path(args, settings)
262
- if not path.exists():
263
- console.print(f"[red]Missing[/red] {path}")
264
- return 1
265
- docs, _ = split_corpus(load_records(path))
266
- limit = getattr(args, "limit", None)
267
- if limit:
268
- docs = docs[: int(limit)]
269
- rows = catalog_entity_backfill_rows(docs)
270
- console.print(
271
- f"[cyan]ENTITY backfill[/cyan] brain={brain_id} docs={len(rows)}"
272
- )
273
- import os
274
- from dotenv import load_dotenv
275
-
276
- for env_path in (
277
- Path.home() / ".brainapi" / "source" / ".env",
278
- Path(__file__).resolve().parents[2] / ".env",
279
- ):
280
- if env_path.exists():
281
- load_dotenv(env_path, override=False)
282
- os.environ.setdefault("DATA_DB", "postgresql")
283
- os.environ.setdefault("VECTOR_DB", "postgresql")
284
- os.environ.setdefault("GRAPH_DB", "networkx")
285
- os.environ.setdefault("SEARCH_ENABLED", "true")
286
- os.environ.setdefault("ENV", "development")
287
- from src.core.instances import (
288
- embeddings_adapter,
289
- graph_adapter,
290
- vector_store_adapter,
291
- )
292
-
293
- summary = apply_entity_text_backfill(
294
- brain_id=brain_id,
295
- rows=rows,
296
- graph=graph_adapter,
297
- embeddings=embeddings_adapter,
298
- vector_store=vector_store_adapter,
299
- )
300
- console.print(summary)
301
- if int(summary.get("updated") or 0) < 1:
302
- console.print("[red]No ENTITY nodes updated[/red]")
303
- return 1
304
- console.print("[green]ENTITY backfill done[/green]")
305
- return 0
306
-
307
-
308
- def cmd_evaluate(args: argparse.Namespace, settings: Settings) -> int:
309
- path = _resolve_dataset_path(args, settings)
310
- if not path.exists():
311
- console.print(f"[red]Missing[/red] {path}")
312
- return 1
313
- rows = load_records(path)
314
- run_id, run_dir = ensure_run_dir(settings, args.run)
315
- ks = tuple(int(x) for x in args.ks.split(",") if x.strip()) or (5, 10, 20)
316
- console.print(
317
- f"[cyan]Evaluating[/cyan] brain={settings.brain_id} "
318
- f"dataset={path.name} run={run_id} fusion={args.fusion or 'rrf'}"
319
- )
320
- with BrainAPIClient(settings) as client:
321
- try:
322
- result = evaluate_search(
323
- client,
324
- rows,
325
- ks=ks,
326
- timeout_s=args.timeout,
327
- k=args.k,
328
- dataset_name=path.name,
329
- limit_docs=getattr(args, "limit_docs", None),
330
- limit_queries=getattr(args, "limit_queries", None),
331
- **_eval_kwargs(args),
332
- )
333
- except SearchDisabledError as exc:
334
- console.print(f"[red]{exc}[/red]")
335
- return 1
336
- report = write_report(run_dir, result)
337
- print_report_table(report)
338
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
339
- if result.get("search_error"):
340
- console.print(f"[red]{result['search_error']}[/red]")
341
- return 0 if report.get("status") == "ok" else 1
342
-
343
-
344
- def cmd_report(args: argparse.Namespace, settings: Settings) -> int:
345
- run_dir = settings.runs_dir / args.run
346
- eval_path = run_dir / "eval.json"
347
- if not eval_path.exists():
348
- console.print(f"[red]Missing[/red] {eval_path}")
349
- return 1
350
- eval_result = json.loads(eval_path.read_text(encoding="utf-8"))
351
- report = write_report(run_dir, eval_result)
352
- print_report_table(report)
353
- return 0 if report.get("status") == "ok" else 1
354
-
355
-
356
- def cmd_finetune_ce(args: argparse.Namespace, settings: Settings) -> int:
357
- from search.finetune_esci_ce import finetune
358
-
359
- path = _resolve_dataset_path(args, settings)
360
- if not path.exists():
361
- console.print(f"[red]Missing[/red] {path}")
362
- return 1
363
- out = Path(args.out)
364
- console.print(
365
- f"[cyan]Fine-tune CE[/cyan] dataset={path.name} out={out} "
366
- f"label_mode={args.label_mode} fields={args.fields}"
367
- )
368
- meta = finetune(
369
- jsonl_path=path,
370
- out_dir=out,
371
- max_pairs=int(args.max_pairs),
372
- epochs=int(args.epochs),
373
- batch_size=int(args.batch_size),
374
- label_mode=str(args.label_mode),
375
- fields=str(args.fields),
376
- base=str(args.base),
377
- lr=float(args.lr),
378
- max_length=int(getattr(args, "max_length", 256)),
379
- )
380
- console.print(json.dumps(meta, indent=2))
381
- return 0
382
-
383
-
384
- def cmd_miss_strata(args: argparse.Namespace, settings: Settings) -> int:
385
- from search.miss_strata import main_classify
386
-
387
- path = _resolve_dataset_path(args, settings)
388
- eval_path = settings.runs_dir / args.from_run / "eval.json"
389
- if not eval_path.exists():
390
- console.print(f"[red]Missing[/red] {eval_path}")
391
- return 1
392
- out = Path(args.out) if args.out else settings.runs_dir / args.from_run / "miss_strata.json"
393
- taxonomy = main_classify(eval_path=eval_path, dataset_path=path, out_path=out)
394
- table = Table(title=f"Miss strata {args.from_run}")
395
- table.add_column("field")
396
- table.add_column("value")
397
- for key in (
398
- "n_queries",
399
- "n_gold",
400
- "n_in_top10",
401
- "n_in_11_to_k",
402
- "n_missed",
403
- "stratum_counts",
404
- "n_total_miss",
405
- "n_rewritable_total_miss",
406
- "run_query_side",
407
- "rewritable_qids",
408
- "total_miss_qids",
409
- ):
410
- table.add_row(key, str(taxonomy.get(key)))
411
- console.print(table)
412
- console.print(f"[green]Wrote[/green] {out}")
413
- return 0
414
-
415
-
416
- def cmd_query_rewrite(args: argparse.Namespace, settings: Settings) -> int:
417
- from search.miss_strata import main_classify, write_rewritten_jsonl
418
-
419
- path = _resolve_dataset_path(args, settings)
420
- eval_path = settings.runs_dir / args.from_run / "eval.json"
421
- if not eval_path.exists():
422
- console.print(f"[red]Missing[/red] {eval_path}")
423
- return 1
424
- taxonomy = main_classify(eval_path=eval_path, dataset_path=path)
425
- if not taxonomy.get("run_query_side"):
426
- console.print(
427
- "[yellow]No rewritable total-miss qids (skip Task 2).[/yellow] "
428
- f"total_miss={taxonomy.get('total_miss_qids')}"
429
- )
430
- return 0
431
- dest = Path(args.out) if args.out else path.with_name("search_esci_74_qrewrite.jsonl")
432
- rows = load_records(path)
433
- write_rewritten_jsonl(rows, taxonomy, dest)
434
- console.print(
435
- f"[green]Wrote[/green] {dest} qids={taxonomy.get('rewritable_qids')}"
436
- )
437
- return 0
438
-
439
-
440
- def cmd_spell_normalize(args: argparse.Namespace, settings: Settings) -> int:
441
- from search.miss_strata import write_spell_jsonl
442
-
443
- path = _resolve_dataset_path(args, settings)
444
- if not path.exists():
445
- console.print(f"[red]Missing[/red] {path}")
446
- return 1
447
- dest = Path(args.out) if args.out else path.with_name("search_esci_es_spell.jsonl")
448
- rows = load_records(path)
449
- summary = write_spell_jsonl(rows, dest)
450
- console.print(
451
- f"[green]Wrote[/green] {dest} changed_queries={summary.get('n_queries_changed')}"
452
- )
453
- return 0
454
-
455
-
456
- def cmd_finetune_dense(args: argparse.Namespace, settings: Settings) -> int:
457
- from search.finetune_esci_dense import finetune_dense
458
-
459
- path = _resolve_dataset_path(args, settings)
460
- if not path.exists():
461
- console.print(f"[red]Missing[/red] {path}")
462
- return 1
463
- eval_path = Path(args.from_eval) if args.from_eval else None
464
- out = Path(args.out)
465
- console.print(
466
- f"[cyan]Fine-tune dense ANCE[/cyan] dataset={path.name} out={out} "
467
- f"(not Reddy 0.857)"
468
- )
469
- meta = finetune_dense(
470
- jsonl_path=path,
471
- out_dir=out,
472
- eval_path=eval_path,
473
- max_pairs=int(args.max_pairs),
474
- epochs=int(args.epochs),
475
- batch_size=int(args.batch_size),
476
- fields=str(args.fields),
477
- base=str(args.base),
478
- lr=float(args.lr),
479
- max_length=int(args.max_length),
480
- )
481
- console.print(json.dumps(meta, indent=2))
482
- return 0
483
-
484
-
485
- def cmd_local_dense(args: argparse.Namespace, settings: Settings) -> int:
486
- from search.local_dense import run_local_dense
487
-
488
- path = _resolve_dataset_path(args, settings)
489
- if not path.exists():
490
- console.print(f"[red]Missing[/red] {path}")
491
- return 1
492
- rows = load_records(path)
493
- run_id, run_dir = ensure_run_dir(settings, args.run)
494
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
495
- model_name = str(args.model)
496
- protocol = None
497
- if "bge" in model_name.lower():
498
- protocol = (
499
- "local BGE-base dual-encoder over search_esci_74.jsonl; "
500
- "not MiniLM ANCE; no query: prefix; not Reddy 0.857"
501
- )
502
- if "paraphrase-multilingual-minilm" in model_name.lower() or "italian_smoke" in path.name:
503
- protocol = (
504
- "local paraphrase-multilingual-MiniLM-L12-v2 over "
505
- "search_italian_smoke.jsonl; pipeline only (hits returned); "
506
- "not text-embedding-3-large quality; not Reddy; not ES n=62"
507
- )
508
- console.print(
509
- f"[cyan]Local dense retrieve[/cyan] dataset={path.name} run={run_id} "
510
- f"model={model_name} brain={settings.brain_id}"
511
- )
512
- result = run_local_dense(
513
- rows,
514
- model_name=str(args.model),
515
- dataset_name=path.name,
516
- k=int(args.k),
517
- ks=ks,
518
- brain_id=settings.brain_id,
519
- protocol=protocol,
520
- )
521
- report = write_report(run_dir, result)
522
- print_report_table(report)
523
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
524
- return 0 if report.get("status") == "ok" else 1
525
-
526
-
527
- def cmd_mine_retrieved_lists(args: argparse.Namespace, settings: Settings) -> int:
528
- from search.mine_retrieved_lists import mine
529
-
530
- path = _resolve_dataset_path(args, settings)
531
- if not path.exists():
532
- console.print(f"[red]Missing[/red] {path}")
533
- return 1
534
- out = Path(args.out)
535
- console.print(
536
- f"[cyan]Mine retrieved lists[/cyan] dataset={path.name} out={out} "
537
- f"(unlabeled=I; hold out test qids)"
538
- )
539
- meta = mine(
540
- jsonl_path=path,
541
- out_path=out,
542
- max_queries=int(args.max_queries),
543
- k=int(args.k),
544
- seed=int(args.seed),
545
- )
546
- console.print(json.dumps(meta, indent=2))
547
- return 0
548
-
549
-
550
- def cmd_export_hybrid_lists(args: argparse.Namespace, settings: Settings) -> int:
551
- from search.export_hybrid_lists import PROTECTED_OUT_NAMES, export_hybrid_lists
552
- from search.list_overlap import load_eval_run
553
-
554
- path = _resolve_dataset_path(args, settings)
555
- if not path.exists():
556
- console.print(f"[red]Missing[/red] {path}")
557
- return 1
558
- holdout_raw = str(getattr(args, "holdout_dataset", None) or "").strip()
559
- holdout = Path(holdout_raw) if holdout_raw else DATA_DIR / "search_esci_74.jsonl"
560
- if not holdout.is_absolute():
561
- holdout = (BENCHMARKS_ROOT / holdout).resolve()
562
- if not holdout.exists():
563
- console.print(f"[red]Missing holdout[/red] {holdout}")
564
- return 1
565
- out = Path(args.out)
566
- if not out.is_absolute():
567
- out = (BENCHMARKS_ROOT / out).resolve()
568
- if out.name in PROTECTED_OUT_NAMES:
569
- console.print(f"[red]Refusing to overwrite[/red] {out}")
570
- return 1
571
- run_dir = settings.runs_dir / str(args.from_run)
572
- if not (run_dir / "eval.json").exists():
573
- console.print(f"[red]Missing eval[/red] {run_dir / 'eval.json'}")
574
- return 1
575
- eval_result = load_eval_run(run_dir)
576
- console.print(
577
- f"[cyan]Export hybrid lists[/cyan] from={args.from_run} "
578
- f"dataset={path.name} holdout={holdout.name} out={out} "
579
- f"(unlabeled=I; source=hybrid-k50)"
580
- )
581
- try:
582
- meta = export_hybrid_lists(
583
- eval_result=eval_result,
584
- dataset_path=path,
585
- holdout_path=holdout,
586
- out_path=out,
587
- k=int(args.k),
588
- )
589
- except ValueError as exc:
590
- console.print(f"[red]{exc}[/red]")
591
- return 1
592
- console.print(json.dumps(meta, indent=2))
593
- return 0
594
-
595
-
596
- def cmd_finetune_4class(args: argparse.Namespace, settings: Settings) -> int:
597
- from search.finetune_esci_4class import finetune
598
-
599
- path = _resolve_dataset_path(args, settings)
600
- if not path.exists():
601
- console.print(f"[red]Missing[/red] {path}")
602
- return 1
603
- lists_path = Path(args.from_lists) if args.from_lists else None
604
- if lists_path is not None and not lists_path.exists():
605
- console.print(f"[red]Missing[/red] {lists_path}")
606
- return 1
607
- out = Path(args.out)
608
- if not out.is_absolute():
609
- out = (BENCHMARKS_ROOT / out).resolve()
610
- base_model = str(args.base)
611
- base_path = Path(base_model)
612
- if not base_path.is_absolute():
613
- maybe_base = (BENCHMARKS_ROOT / base_path).resolve()
614
- if maybe_base.exists():
615
- base_path = maybe_base
616
- base_model = str(base_path)
617
- if out.exists() and base_path.exists() and out.resolve() == base_path.resolve():
618
- console.print(f"[red]Refusing to overwrite base checkpoint[/red] {out}")
619
- return 1
620
- lists_source = str(getattr(args, "lists_source", None) or "").strip()
621
- if lists_path is not None:
622
- source = lists_source or "retrieved-bm25"
623
- else:
624
- source = "pool"
625
- console.print(
626
- f"[cyan]Fine-tune 4-class CE[/cyan] dataset={path.name} out={out} "
627
- f"source={source}"
628
- )
629
- meta = finetune(
630
- jsonl_path=path,
631
- out_dir=out,
632
- max_pairs=int(args.max_pairs),
633
- epochs=int(args.epochs),
634
- batch_size=int(args.batch_size),
635
- max_length=int(args.max_length),
636
- base_model=base_model,
637
- seed=int(args.seed),
638
- ckpt_every=int(args.ckpt_every),
639
- use_class_weights=bool(args.class_weights),
640
- lists_path=lists_path,
641
- lists_source=source if lists_path is not None else None,
642
- )
643
- console.print(json.dumps(meta, indent=2))
644
- return 0
645
-
646
-
647
- def cmd_colbert_local(args: argparse.Namespace, settings: Settings) -> int:
648
- from search.local_colbert import run_local_colbert
649
-
650
- path = _resolve_dataset_path(args, settings)
651
- if not path.exists():
652
- console.print(f"[red]Missing[/red] {path}")
653
- return 1
654
- rows = load_records(path)
655
- run_id, run_dir = ensure_run_dir(settings, args.run)
656
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
657
- console.print(
658
- f"[cyan]Local ColBERT MaxSim[/cyan] dataset={path.name} run={run_id} "
659
- f"(not fused with passages; not Reddy 0.857)"
660
- )
661
- result = run_local_colbert(
662
- rows,
663
- dataset_name=path.name,
664
- k=int(args.k),
665
- ks=ks,
666
- brain_id="harness-local-colbert",
667
- )
668
- report = write_report(run_dir, result)
669
- print_report_table(report)
670
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
671
- return 0 if report.get("status") == "ok" else 1
672
-
673
-
674
- def _existing_path(path: Path) -> Path:
675
- if path.exists():
676
- return path
677
- from search.config import BENCHMARKS_ROOT
678
-
679
- alt = BENCHMARKS_ROOT / path
680
- return alt if alt.exists() else path
681
-
682
-
683
- def cmd_rank_corpus(args: argparse.Namespace, settings: Settings) -> int:
684
- from search.rank_corpus import DEFAULT_MODEL, load_predict, run_exhaustive_ce
685
-
686
- path = _resolve_dataset_path(args, settings)
687
- if not path.exists():
688
- console.print(f"[red]Missing[/red] {path}")
689
- return 1
690
- model_dir = _existing_path(Path(args.model) if args.model else DEFAULT_MODEL)
691
- if not model_dir.exists():
692
- console.print(f"[red]Missing model[/red] {model_dir}")
693
- return 1
694
- rows = load_records(path)
695
- run_id, run_dir = ensure_run_dir(settings, args.run)
696
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
697
- console.print(
698
- f"[cyan]Exhaustive 4-class CE[/cyan] dataset={path.name} run={run_id} "
699
- f"model={model_dir} (protocol exhaustive-catalog; not Reddy 0.857)"
700
- )
701
- predict, model_name = load_predict(model_dir, max_length=int(args.max_length))
702
- result = run_exhaustive_ce(
703
- rows,
704
- predict=predict,
705
- model_name=model_name,
706
- dataset_name=path.name,
707
- k=int(args.k),
708
- ks=ks,
709
- brain_id="harness-local-exhaustive",
710
- )
711
- report = write_report(run_dir, result)
712
- print_report_table(report)
713
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
714
- return 0 if report.get("status") == "ok" else 1
715
-
716
-
717
- def cmd_list_overlap(args: argparse.Namespace, settings: Settings) -> int:
718
- from search.list_overlap import load_eval_run, summarize_overlap
719
-
720
- names = [item.strip() for item in str(args.against_runs).split(",") if item.strip()]
721
- passages = load_eval_run(settings.runs_dir / args.passages_run)
722
- passages["run_id"] = args.passages_run
723
- sidecars: dict = {}
724
- for name in names:
725
- eval_result = load_eval_run(settings.runs_dir / name)
726
- eval_result["run_id"] = name
727
- sidecars[name] = eval_result
728
- queries = None
729
- dataset = getattr(args, "dataset", None)
730
- if dataset:
731
- path = Path(dataset)
732
- if not path.exists():
733
- path = _existing_path(path)
734
- if path.exists():
735
- _, queries = split_corpus(load_records(path))
736
- summary = summarize_overlap(
737
- passages,
738
- sidecars,
739
- k=int(args.k),
740
- queries=queries,
741
- )
742
- run_id, run_dir = ensure_run_dir(settings, args.run)
743
- out = run_dir / "overlap.json"
744
- out.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
745
- headline = {
746
- name: {
747
- "unique_gold_hits": row["unique_gold_hits"],
748
- "queries_with_unique": row["queries_with_unique"],
749
- "n_queries": row["n_queries"],
750
- }
751
- for name, row in summary["runs"].items()
752
- }
753
- console.print(json.dumps(headline, indent=2))
754
- console.print(f"[green]Wrote[/green] {out}")
755
- return 0
756
-
757
-
758
- def cmd_union_lists(args: argparse.Namespace, settings: Settings) -> int:
759
- from search.list_overlap import load_eval_run, run_union
760
-
761
- path = _resolve_dataset_path(args, settings)
762
- if not path.exists():
763
- console.print(f"[red]Missing[/red] {path}")
764
- return 1
765
- names = [item.strip() for item in str(args.from_runs).split(",") if item.strip()]
766
- evals = []
767
- for name in names:
768
- eval_result = load_eval_run(settings.runs_dir / name)
769
- eval_result["run_id"] = name
770
- evals.append(eval_result)
771
- rows = load_records(path)
772
- run_id, run_dir = ensure_run_dir(settings, args.run)
773
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
774
- console.print(
775
- f"[cyan]Harness RRF union[/cyan] from={names} dataset={path.name} "
776
- f"run={run_id} (not live graph; not Reddy 0.857)"
777
- )
778
- result = run_union(
779
- evals,
780
- rows,
781
- dataset_name=path.name,
782
- k=int(args.k),
783
- ks=ks,
784
- brain_id="harness-local-union",
785
- run_names=names,
786
- )
787
- report = write_report(run_dir, result)
788
- print_report_table(report)
789
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
790
- return 0 if report.get("status") == "ok" else 1
791
-
792
-
793
- def cmd_cascade_lists(args: argparse.Namespace, settings: Settings) -> int:
794
- from search.list_overlap import load_eval_run, run_cascade
795
-
796
- path = _resolve_dataset_path(args, settings)
797
- if not path.exists():
798
- console.print(f"[red]Missing[/red] {path}")
799
- return 1
800
- passages = load_eval_run(settings.runs_dir / args.passages_run)
801
- passages["run_id"] = args.passages_run
802
- names = [item.strip() for item in str(args.from_runs).split(",") if item.strip()]
803
- sidecars = []
804
- for name in names:
805
- eval_result = load_eval_run(settings.runs_dir / name)
806
- eval_result["run_id"] = name
807
- sidecars.append(eval_result)
808
- rows = load_records(path)
809
- run_id, run_dir = ensure_run_dir(settings, args.run)
810
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
811
- console.print(
812
- f"[cyan]Frozen-head cascade[/cyan] passages={args.passages_run} "
813
- f"sidecars={names} head_k={args.head_k} dataset={path.name} "
814
- f"run={run_id} (not live graph; not Reddy 0.857)"
815
- )
816
- result = run_cascade(
817
- passages,
818
- sidecars,
819
- rows,
820
- dataset_name=path.name,
821
- k=int(args.k),
822
- head_k=int(args.head_k),
823
- ks=ks,
824
- brain_id="harness-local-cascade",
825
- run_names=[args.passages_run, *names],
826
- )
827
- report = write_report(run_dir, result)
828
- print_report_table(report)
829
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
830
- return 0 if report.get("status") == "ok" else 1
831
-
832
-
833
- def cmd_ltr_head(args: argparse.Namespace, settings: Settings) -> int:
834
- from search.list_overlap import load_eval_run
835
- from search.ltr_head import HEAD_RANKNET, PAIR_UNLABELED_ZERO, ce_cache_name, run_ltr_head
836
-
837
- path = _resolve_dataset_path(args, settings)
838
- if not path.exists():
839
- if not path.is_absolute():
840
- alt = (BENCHMARKS_ROOT / path).resolve()
841
- if alt.exists():
842
- path = alt
843
- if not path.exists():
844
- console.print(f"[red]Missing[/red] {path}")
845
- return 1
846
- passages = load_eval_run(settings.runs_dir / args.from_run)
847
- passages["run_id"] = args.from_run
848
- rows = load_records(path)
849
- run_id, run_dir = ensure_run_dir(settings, args.run)
850
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip()) or (5, 10, 20, 50)
851
- policy = str(getattr(args, "pair_policy", None) or PAIR_UNLABELED_ZERO)
852
- head = str(getattr(args, "ltr_model", None) or HEAD_RANKNET)
853
- ce_raw = str(getattr(args, "ce_model", None) or "").strip()
854
- ce_model = Path(ce_raw) if ce_raw else None
855
- if ce_model is not None and not ce_model.is_absolute():
856
- ce_model = (BENCHMARKS_ROOT / ce_model).resolve()
857
- if ce_model is not None and not ce_model.exists():
858
- console.print(f"[red]Missing CE model[/red] {ce_model}")
859
- return 1
860
- ce_cache = None
861
- if ce_model is not None:
862
- ce_cache = settings.runs_dir / args.from_run / ce_cache_name(ce_model)
863
- train_run = str(getattr(args, "train_from_run", None) or "").strip()
864
- train_eval = None
865
- train_rows = None
866
- train_ce_cache = None
867
- train_dataset_name = None
868
- if train_run:
869
- train_ds = str(getattr(args, "train_dataset", None) or "").strip()
870
- if not train_ds:
871
- console.print("[red]--train-dataset is required with --train-from-run[/red]")
872
- return 1
873
- train_path = Path(train_ds)
874
- if not train_path.is_absolute():
875
- train_path = (BENCHMARKS_ROOT / train_path).resolve()
876
- if not train_path.exists():
877
- console.print(f"[red]Missing train dataset[/red] {train_path}")
878
- return 1
879
- train_eval = load_eval_run(settings.runs_dir / train_run)
880
- train_eval["run_id"] = train_run
881
- train_rows = load_records(train_path)
882
- train_dataset_name = train_path.name
883
- if ce_model is not None:
884
- train_ce_cache = settings.runs_dir / train_run / ce_cache_name(ce_model)
885
- mode = "apply" if train_run else "CV"
886
- console.print(
887
- f"[cyan]LTR head {mode}[/cyan] from={args.from_run} dataset={path.name} "
888
- f"train={train_run or 'folds'} {train_dataset_name or ''} "
889
- f"run={run_id} folds={args.folds} pair={policy} head={head} "
890
- f"ce={ce_model or 'none'} (not live; not Reddy 0.857)"
891
- )
892
- result = run_ltr_head(
893
- passages,
894
- rows,
895
- dataset_name=path.name,
896
- k=int(args.k),
897
- ks=ks,
898
- n_folds=int(args.folds),
899
- brain_id="harness-local-ltr",
900
- source_run=args.from_run,
901
- pair_policy=policy,
902
- ce_model=ce_model,
903
- ce_cache_path=ce_cache,
904
- ltr_head=head,
905
- train_eval_result=train_eval,
906
- train_rows=train_rows,
907
- train_source_run=train_run or None,
908
- train_ce_cache_path=train_ce_cache,
909
- )
910
- report = write_report(run_dir, result)
911
- print_report_table(report)
912
- overlap = result.get("overlap_only_metrics") or {}
913
- console.print(
914
- f"overlap-only ndcg@10={overlap.get('ndcg@10')} "
915
- f"recall@10={overlap.get('recall@10')}"
916
- )
917
- weights = result.get("ltr_mean_weights") or {}
918
- if weights:
919
- kind = "gain importances" if head == "lightgbm" else "weights"
920
- console.print(f"mean CV {kind} {weights}")
921
- if result.get("ltr_train_run"):
922
- console.print(
923
- f"trained on {result.get('ltr_n_train_queries')} queries "
924
- f"from {result.get('ltr_train_run')}"
925
- )
926
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
927
- return 0 if report.get("status") == "ok" else 1
928
-
929
-
930
- def cmd_pool_first_stage(args: argparse.Namespace, settings: Settings) -> int:
931
- from search.pool_first_stage import main as pool_main
932
-
933
- argv = ["--dataset", str(_resolve_dataset_path(args, settings))]
934
- if args.run:
935
- argv.extend(["--run", str(args.run)])
936
- if args.variant:
937
- argv.extend(["--variant", str(args.variant)])
938
- if args.expand_out:
939
- argv.extend(["--expand-out", str(args.expand_out)])
940
- return int(pool_main(argv))
941
-
942
-
943
- def cmd_rerank_retrieved(args: argparse.Namespace, settings: Settings) -> int:
944
- from search.rerank_retrieved import run_ce_on_retrieved
945
-
946
- path = _resolve_dataset_path(args, settings)
947
- if not path.exists():
948
- console.print(f"[red]Missing[/red] {path}")
949
- return 1
950
- source_dir = settings.runs_dir / args.from_run
951
- eval_path = source_dir / "eval.json"
952
- if not eval_path.exists():
953
- console.print(f"[red]Missing[/red] {eval_path}")
954
- return 1
955
- eval_result = json.loads(eval_path.read_text(encoding="utf-8"))
956
- eval_result["run_id"] = args.from_run
957
- rows = load_records(path)
958
- run_id, run_dir = ensure_run_dir(settings, args.run)
959
- ks = None
960
- if getattr(args, "ks", None):
961
- ks = tuple(int(x) for x in str(args.ks).split(",") if x.strip())
962
- console.print(
963
- f"[cyan]Harness CE on retrieved hits[/cyan] from={args.from_run} "
964
- f"dataset={path.name} run={run_id} (not Reddy 0.857)"
965
- )
966
- result = run_ce_on_retrieved(
967
- eval_result,
968
- rows,
969
- dataset_name=path.name,
970
- ks=ks,
971
- brain_id=settings.brain_id,
972
- )
973
- report = write_report(run_dir, result)
974
- print_report_table(report)
975
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
976
- return 0 if report.get("status") == "ok" else 1
977
-
978
-
979
- def cmd_rank_pool_ce(args: argparse.Namespace, settings: Settings) -> int:
980
- from search.rank_pool import run_ce_on_pool
981
-
982
- path = _resolve_dataset_path(args, settings)
983
- if not path.exists():
984
- console.print(f"[red]Missing[/red] {path}")
985
- return 1
986
- rows = load_records(path)
987
- run_id, run_dir = ensure_run_dir(settings, args.run)
988
- console.print(
989
- f"[cyan]Ranking-in-pool CE[/cyan] dataset={path.name} run={run_id} "
990
- f"(cite Reddy 0.857 only if protocol is CE-on-pool; n is not ~4477)"
991
- )
992
- result = run_ce_on_pool(
993
- rows,
994
- dataset_name=path.name,
995
- brain_id=settings.brain_id,
996
- )
997
- report = write_report(run_dir, result)
998
- print_report_table(report)
999
- console.print(f"[green]Wrote[/green] {run_dir / 'report.json'}")
1000
- if result.get("ce_model"):
1001
- console.print(
1002
- f"ce_model={result.get('ce_model')} "
1003
- f"missing_text={result.get('ce_missing_text')}"
1004
- )
1005
- return 0 if report.get("status") == "ok" else 1
1006
-
1007
-
1008
- def build_parser() -> argparse.ArgumentParser:
1009
- parser = argparse.ArgumentParser(
1010
- prog="search",
1011
- description=(
1012
- "Labeled search eval on searchbench*: ingest docs → "
1013
- "POST /retrieve/search (Recall/nDCG/MRR + retrieve latency)"
1014
- ),
1015
- )
1016
- parser.add_argument(
1017
- "--env-file",
1018
- type=Path,
1019
- default=None,
1020
- help="Optional dotenv path (default benchmarks/.env)",
1021
- )
1022
- parser.add_argument(
1023
- "--brain",
1024
- default=None,
1025
- help=f"Brain id (default {DEFAULT_BRAIN_ID}; must start with searchbench)",
1026
- )
1027
- sub = parser.add_subparsers(dest="command", required=True)
1028
-
1029
- p_download = sub.add_parser(
1030
- "download",
1031
- help="Download Amazon ESCI and/or WANDS into search JSONL",
1032
- )
1033
- p_download.add_argument(
1034
- "--name",
1035
- default="all",
1036
- help="esci, wands, jdsearch, or all (comma-separated; all = esci,wands)",
1037
- )
1038
- p_download.add_argument("--out", type=str, default=None)
1039
- p_download.add_argument("--force", action="store_true")
1040
- p_download.add_argument(
1041
- "--dry-stats",
1042
- action="store_true",
1043
- dest="dry_stats",
1044
- help="JDsearch only: print label histogram from the tar/cache without writing JSONL",
1045
- )
1046
- p_download.add_argument("--max-queries", type=int, default=80)
1047
- p_download.add_argument("--max-docs", type=int, default=2000)
1048
- p_download.add_argument("--candidates-per-query", type=int, default=40)
1049
- p_download.add_argument(
1050
- "--locale",
1051
- default="us",
1052
- help="ESCI locale: us, es, or jp (default us). Not Italian.",
1053
- )
1054
- p_download.add_argument(
1055
- "--split",
1056
- default="test",
1057
- help="ESCI split (default test; small_version / Task 1 only)",
1058
- )
1059
- p_download.add_argument(
1060
- "--holdout-dataset",
1061
- dest="holdout_dataset",
1062
- default="",
1063
- help="JSONL whose qids are skipped (e.g. data/search_esci_74.jsonl)",
1064
- )
1065
- p_download.set_defaults(func=cmd_download)
1066
-
1067
- p_stats = sub.add_parser("dataset-stats", help="Summarize search JSONL")
1068
- p_stats.add_argument("--dataset", type=str, default=None)
1069
- p_stats.set_defaults(func=cmd_dataset_stats)
1070
-
1071
- p_smoke = sub.add_parser(
1072
- "smoke", help="Ingest a few docs then POST /retrieve/search"
1073
- )
1074
- p_smoke.add_argument("--dataset", type=str, default=None)
1075
- p_smoke.add_argument("--limit", type=int, default=2)
1076
- p_smoke.add_argument("--limit-queries", type=int, default=2)
1077
- p_smoke.add_argument("--k", type=int, default=10)
1078
- p_smoke.add_argument("--fusion", choices=["rrf", "cc"], default=None)
1079
- p_smoke.add_argument("--fusion-alpha", dest="fusion_alpha", type=float, default=None)
1080
- p_smoke.add_argument(
1081
- "--rerank",
1082
- default=None,
1083
- help="none or plugin:<name> (e.g. plugin:cross-encoder). Unknown name is 400.",
1084
- )
1085
- p_smoke.add_argument(
1086
- "--channels",
1087
- default=None,
1088
- help=(
1089
- "Comma-separated channels (default passages): "
1090
- "passages,entities,events,communities and/or plugin:<name>."
1091
- ),
1092
- )
1093
- p_smoke.add_argument("--node-labels", dest="node_labels", default=None)
1094
- p_smoke.add_argument("--community-labels", dest="community_labels", default=None)
1095
- p_smoke.add_argument(
1096
- "--expand",
1097
- choices=["none", "neighbors"],
1098
- default="none",
1099
- )
1100
- p_smoke.add_argument(
1101
- "--ingest-graph",
1102
- action="store_true",
1103
- help="Also write deterministic catalog triples (entity uuid = doc_id).",
1104
- )
1105
- p_smoke.add_argument(
1106
- "--interactions",
1107
- type=str,
1108
- default=None,
1109
- help="Optional interaction JSONL (EVENT+happened_at) on this searchbench brain.",
1110
- )
1111
- p_smoke.add_argument("--timeout", type=float, default=600.0)
1112
- p_smoke.add_argument(
1113
- "--enrich",
1114
- action="store_true",
1115
- help="Run Scout/Architect LLM ingest. Default skips enrichment (chunk+embed only).",
1116
- )
1117
- p_smoke.add_argument(
1118
- "--skip-ingest",
1119
- action="store_true",
1120
- help="Reuse chunks already on this searchbench brain (no POST /ingest/).",
1121
- )
1122
- p_smoke.add_argument(
1123
- "--personalize",
1124
- action="store_true",
1125
- dest="personalize",
1126
- help="Send each query's target field on POST /retrieve/search. Default omit.",
1127
- )
1128
- p_smoke.add_argument(
1129
- "--target-from-query",
1130
- action="store_true",
1131
- dest="personalize",
1132
- help="Alias for --personalize.",
1133
- )
1134
- p_smoke.set_defaults(func=cmd_smoke)
1135
-
1136
- p_bf = sub.add_parser(
1137
- "backfill-entity-text",
1138
- help="ENTITY-only search_text + nodes vector refresh (not architect, not frozen brains)",
1139
- )
1140
- p_bf.add_argument("--dataset", type=str, default="data/search_wands.jsonl")
1141
- p_bf.add_argument("--limit", type=int, default=None)
1142
- p_bf.set_defaults(func=cmd_backfill_entity_text)
1143
-
1144
- p_eval = sub.add_parser(
1145
- "evaluate",
1146
- help="Ingest corpus, search all queries, write report + ledger",
1147
- )
1148
- p_eval.add_argument("--dataset", type=str, default=None)
1149
- p_eval.add_argument("--run", type=str, default=None)
1150
- p_eval.add_argument("--ks", type=str, default="5,10,20")
1151
- p_eval.add_argument("--k", type=int, default=20)
1152
- p_eval.add_argument("--fusion", choices=["rrf", "cc"], default=None)
1153
- p_eval.add_argument("--fusion-alpha", dest="fusion_alpha", type=float, default=None)
1154
- p_eval.add_argument(
1155
- "--rerank",
1156
- default=None,
1157
- help="none or plugin:<name> (e.g. plugin:cross-encoder). Unknown name is 400.",
1158
- )
1159
- p_eval.add_argument(
1160
- "--mode",
1161
- choices=["default", "catalog"],
1162
- default="default",
1163
- help="default keeps RERANK_MAX_K=10. catalog retrieves deeper and reranks up to 50.",
1164
- )
1165
- p_eval.add_argument(
1166
- "--channels",
1167
- default=None,
1168
- help=(
1169
- "Comma-separated channels (default passages): "
1170
- "passages,entities,events,communities and/or plugin:<name>."
1171
- ),
1172
- )
1173
- p_eval.add_argument("--node-labels", dest="node_labels", default=None)
1174
- p_eval.add_argument("--community-labels", dest="community_labels", default=None)
1175
- p_eval.add_argument(
1176
- "--expand",
1177
- choices=["none", "neighbors"],
1178
- default="none",
1179
- )
1180
- p_eval.add_argument(
1181
- "--ingest-graph",
1182
- action="store_true",
1183
- help="Also write deterministic catalog triples (entity uuid = doc_id).",
1184
- )
1185
- p_eval.add_argument(
1186
- "--interactions",
1187
- type=str,
1188
- default=None,
1189
- help="Optional interaction JSONL (EVENT+happened_at) on this searchbench brain.",
1190
- )
1191
- p_eval.add_argument("--timeout", type=float, default=600.0)
1192
- p_eval.add_argument(
1193
- "--enrich",
1194
- action="store_true",
1195
- help="Run Scout/Architect LLM ingest. Default skips enrichment (chunk+embed only).",
1196
- )
1197
- p_eval.add_argument("--limit-docs", dest="limit_docs", type=int, default=None)
1198
- p_eval.add_argument("--limit-queries", dest="limit_queries", type=int, default=None)
1199
- p_eval.add_argument(
1200
- "--skip-ingest",
1201
- action="store_true",
1202
- help="Reuse chunks already on this searchbench brain (no POST /ingest/).",
1203
- )
1204
- p_eval.add_argument(
1205
- "--rank-pool",
1206
- action="store_true",
1207
- dest="rank_pool",
1208
- help=(
1209
- "Restrict hits to each query's candidate_doc_ids (including I) "
1210
- "and score pool nDCG@20. Do not average with shared-corpus nDCG@10."
1211
- ),
1212
- )
1213
- p_eval.add_argument(
1214
- "--personalize",
1215
- action="store_true",
1216
- dest="personalize",
1217
- help="Send each query's target field on POST /retrieve/search. Default omit.",
1218
- )
1219
- p_eval.add_argument(
1220
- "--target-from-query",
1221
- action="store_true",
1222
- dest="personalize",
1223
- help="Alias for --personalize.",
1224
- )
1225
- p_eval.add_argument(
1226
- "--extras",
1227
- default=None,
1228
- help='Optional equality filter JSON object, e.g. {"locale":"it"}',
1229
- )
1230
- p_eval.set_defaults(func=cmd_evaluate)
1231
-
1232
- p_report = sub.add_parser("report", help="Rebuild report from eval.json")
1233
- p_report.add_argument("--run", type=str, required=True)
1234
- p_report.set_defaults(func=cmd_report)
1235
-
1236
- p_pool = sub.add_parser(
1237
- "rank-pool-ce",
1238
- help="Score labeled candidate pools with MiniLM CE (no search API)",
1239
- )
1240
- p_pool.add_argument("--dataset", type=str, default=None)
1241
- p_pool.add_argument("--run", type=str, default=None)
1242
- p_pool.set_defaults(func=cmd_rank_pool_ce)
1243
-
1244
- p_rr = sub.add_parser(
1245
- "rerank-retrieved",
1246
- help="Harness CE over stored first-stage hits (not production RERANK_MAX_K)",
1247
- )
1248
- p_rr.add_argument("--from-run", dest="from_run", type=str, required=True)
1249
- p_rr.add_argument("--dataset", type=str, default=None)
1250
- p_rr.add_argument("--run", type=str, default=None)
1251
- p_rr.add_argument("--ks", type=str, default=None)
1252
- p_rr.set_defaults(func=cmd_rerank_retrieved)
1253
-
1254
- p_ft = sub.add_parser(
1255
- "finetune-ce",
1256
- help="Fine-tune a pool CE on ESCI US train (test qids held out)",
1257
- )
1258
- p_ft.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1259
- p_ft.add_argument(
1260
- "--out",
1261
- type=str,
1262
- default="data/models/esci-minilm-graded",
1263
- )
1264
- p_ft.add_argument("--max-pairs", type=int, default=80000)
1265
- p_ft.add_argument("--epochs", type=int, default=1)
1266
- p_ft.add_argument("--batch-size", type=int, default=32)
1267
- p_ft.add_argument(
1268
- "--label-mode",
1269
- choices=["binary", "graded", "multiclass"],
1270
- default="graded",
1271
- )
1272
- p_ft.add_argument("--fields", choices=["title", "catalog"], default="catalog")
1273
- p_ft.add_argument(
1274
- "--base",
1275
- default="cross-encoder/ms-marco-MiniLM-L-6-v2",
1276
- )
1277
- p_ft.add_argument("--lr", type=float, default=7e-6)
1278
- p_ft.add_argument("--max-length", type=int, default=256)
1279
- p_ft.set_defaults(func=cmd_finetune_ce)
1280
-
1281
- p_ms = sub.add_parser(
1282
- "miss-strata",
1283
- help="Classify k=50 hits: head-ok / rank-too-low / total-miss",
1284
- )
1285
- p_ms.add_argument("--from-run", dest="from_run", type=str, required=True)
1286
- p_ms.add_argument("--dataset", type=str, default=None)
1287
- p_ms.add_argument("--out", type=str, default=None)
1288
- p_ms.set_defaults(func=cmd_miss_strata)
1289
-
1290
- p_qw = sub.add_parser(
1291
- "query-rewrite",
1292
- help="Rewrite pathological total-miss queries only (skip esci-72)",
1293
- )
1294
- p_qw.add_argument("--from-run", dest="from_run", type=str, required=True)
1295
- p_qw.add_argument("--dataset", type=str, default=None)
1296
- p_qw.add_argument("--out", type=str, default=None)
1297
- p_qw.set_defaults(func=cmd_query_rewrite)
1298
-
1299
- p_sp = sub.add_parser(
1300
- "spell-normalize",
1301
- help="Harness-only NFKC/punctuation/accent normalize of query text",
1302
- )
1303
- p_sp.add_argument("--dataset", type=str, default="data/search_esci_es.jsonl")
1304
- p_sp.add_argument("--out", type=str, default="data/search_esci_es_spell.jsonl")
1305
- p_sp.set_defaults(func=cmd_spell_normalize)
1306
-
1307
- p_ftd = sub.add_parser(
1308
- "finetune-dense",
1309
- help="Fine-tune a dual encoder with k=50 hard-negative bank (test qids held out)",
1310
- )
1311
- p_ftd.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1312
- p_ftd.add_argument(
1313
- "--out",
1314
- type=str,
1315
- default="data/models/esci-minilm-dense-ance",
1316
- )
1317
- p_ftd.add_argument(
1318
- "--from-eval",
1319
- type=str,
1320
- default="runs/search-esci-74-passages-k50/eval.json",
1321
- )
1322
- p_ftd.add_argument("--max-pairs", type=int, default=20000)
1323
- p_ftd.add_argument("--epochs", type=int, default=1)
1324
- p_ftd.add_argument("--batch-size", type=int, default=32)
1325
- p_ftd.add_argument("--fields", choices=["title", "catalog"], default="catalog")
1326
- p_ftd.add_argument(
1327
- "--base",
1328
- default="sentence-transformers/all-MiniLM-L6-v2",
1329
- )
1330
- p_ftd.add_argument("--lr", type=float, default=2e-5)
1331
- p_ftd.add_argument("--max-length", type=int, default=256)
1332
- p_ftd.set_defaults(func=cmd_finetune_dense)
1333
-
1334
- p_ld = sub.add_parser(
1335
- "local-dense",
1336
- help="Retrieve the JSONL corpus with a local dual encoder (not BrainAPI)",
1337
- )
1338
- p_ld.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1339
- p_ld.add_argument("--run", type=str, default="search-esci-74-dense-ance-k50")
1340
- p_ld.add_argument("--model", type=str, required=True)
1341
- p_ld.add_argument("--k", type=int, default=50)
1342
- p_ld.add_argument("--ks", type=str, default="5,10,20,50")
1343
- p_ld.set_defaults(func=cmd_local_dense)
1344
-
1345
- p_mine = sub.add_parser(
1346
- "mine-retrieved-lists",
1347
- help="BM25 top-k train lists with ESCI labels; unlabeled=I",
1348
- )
1349
- p_mine.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1350
- p_mine.add_argument("--out", type=str, default="data/esci_retrieved_lists.jsonl")
1351
- p_mine.add_argument("--max-queries", type=int, default=6000)
1352
- p_mine.add_argument("--k", type=int, default=50)
1353
- p_mine.add_argument("--seed", type=int, default=11)
1354
- p_mine.set_defaults(func=cmd_mine_retrieved_lists)
1355
-
1356
- p_exh = sub.add_parser(
1357
- "export-hybrid-lists",
1358
- help="Export stored hybrid k=50 hits as 4-class rows; unlabeled=I",
1359
- )
1360
- p_exh.add_argument(
1361
- "--from-run",
1362
- dest="from_run",
1363
- default="search-esci-ltr200-passages-k50",
1364
- )
1365
- p_exh.add_argument("--dataset", type=str, default="data/search_esci_ltr200.jsonl")
1366
- p_exh.add_argument(
1367
- "--holdout-dataset",
1368
- dest="holdout_dataset",
1369
- default="data/search_esci_74.jsonl",
1370
- )
1371
- p_exh.add_argument("--out", type=str, default="data/esci_hybrid_lists_ltr200.jsonl")
1372
- p_exh.add_argument("--k", type=int, default=50)
1373
- p_exh.set_defaults(func=cmd_export_hybrid_lists)
1374
-
1375
- p_ft4 = sub.add_parser(
1376
- "finetune-4class",
1377
- help="Fine-tune 4-class L-12 CE (pool or retrieved-bm25 lists)",
1378
- )
1379
- p_ft4.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1380
- p_ft4.add_argument(
1381
- "--out",
1382
- type=str,
1383
- default="data/models/esci-minilm-l12-retrieved",
1384
- )
1385
- p_ft4.add_argument("--from-lists", type=str, default=None)
1386
- p_ft4.add_argument(
1387
- "--lists-source",
1388
- dest="lists_source",
1389
- default="",
1390
- help="Label for --from-lists (hybrid-k50 or retrieved-bm25)",
1391
- )
1392
- p_ft4.add_argument("--max-pairs", type=int, default=80000)
1393
- p_ft4.add_argument("--epochs", type=int, default=1)
1394
- p_ft4.add_argument("--batch-size", type=int, default=32)
1395
- p_ft4.add_argument("--max-length", type=int, default=192)
1396
- p_ft4.add_argument("--ckpt-every", type=int, default=200)
1397
- p_ft4.add_argument("--class-weights", action="store_true")
1398
- p_ft4.add_argument(
1399
- "--base",
1400
- default="cross-encoder/ms-marco-MiniLM-L-12-v2",
1401
- )
1402
- p_ft4.add_argument("--seed", type=int, default=11)
1403
- p_ft4.set_defaults(func=cmd_finetune_4class)
1404
-
1405
- p_cb = sub.add_parser(
1406
- "colbert-local",
1407
- help="ColBERT MaxSim sidecar over the JSONL corpus (not BrainAPI)",
1408
- )
1409
- p_cb.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1410
- p_cb.add_argument("--run", type=str, default="search-esci-74-colbert-k50")
1411
- p_cb.add_argument("--k", type=int, default=50)
1412
- p_cb.add_argument("--ks", type=str, default="5,10,20,50")
1413
- p_cb.set_defaults(func=cmd_colbert_local)
1414
-
1415
- p_rc = sub.add_parser(
1416
- "rank-corpus",
1417
- help="Score every JSONL doc with 4-class CE (exhaustive-catalog, not production)",
1418
- )
1419
- p_rc.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1420
- p_rc.add_argument("--run", type=str, default="search-esci-74-exhaustive-ce")
1421
- p_rc.add_argument(
1422
- "--model",
1423
- type=str,
1424
- default="data/models/esci-minilm-l12-4class-nowt-e2",
1425
- )
1426
- p_rc.add_argument("--k", type=int, default=50)
1427
- p_rc.add_argument("--ks", type=str, default="5,10,20,50")
1428
- p_rc.add_argument("--max-length", dest="max_length", type=int, default=192)
1429
- p_rc.set_defaults(func=cmd_rank_corpus)
1430
-
1431
- p_ov = sub.add_parser(
1432
- "list-overlap",
1433
- help="Count gold ASINs in sidecar top-k missing from passages",
1434
- )
1435
- p_ov.add_argument(
1436
- "--passages-run",
1437
- dest="passages_run",
1438
- default="search-esci-74-passages-k50",
1439
- )
1440
- p_ov.add_argument(
1441
- "--against-runs",
1442
- dest="against_runs",
1443
- default="search-esci-74-bge-base-k50,search-esci-74-colbert-k50",
1444
- )
1445
- p_ov.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1446
- p_ov.add_argument("--run", type=str, default="search-esci-74-list-overlap")
1447
- p_ov.add_argument("--k", type=int, default=50)
1448
- p_ov.set_defaults(func=cmd_list_overlap)
1449
-
1450
- p_un = sub.add_parser(
1451
- "union-lists",
1452
- help="RRF-union stored first-stage lists (harness only, not live graph)",
1453
- )
1454
- p_un.add_argument(
1455
- "--from-runs",
1456
- dest="from_runs",
1457
- default="search-esci-74-passages-k50,search-esci-74-bge-base-k50",
1458
- )
1459
- p_un.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1460
- p_un.add_argument("--run", type=str, default="search-esci-74-union-bge-k50")
1461
- p_un.add_argument("--k", type=int, default=50)
1462
- p_un.add_argument("--ks", type=str, default="5,10,20,50")
1463
- p_un.set_defaults(func=cmd_union_lists)
1464
-
1465
- p_cs = sub.add_parser(
1466
- "cascade-lists",
1467
- help="Keep passages top-10; inject sidecar unique golds into ranks 11-50",
1468
- )
1469
- p_cs.add_argument(
1470
- "--passages-run",
1471
- dest="passages_run",
1472
- default="search-esci-74-passages-k50",
1473
- )
1474
- p_cs.add_argument(
1475
- "--from-runs",
1476
- dest="from_runs",
1477
- default="search-esci-74-bge-base-k50,search-esci-74-colbert-k50",
1478
- )
1479
- p_cs.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1480
- p_cs.add_argument("--run", type=str, default="search-esci-74-cascade-tail-k50")
1481
- p_cs.add_argument("--k", type=int, default=50)
1482
- p_cs.add_argument("--head-k", dest="head_k", type=int, default=10)
1483
- p_cs.add_argument("--ks", type=str, default="5,10,20,50")
1484
- p_cs.set_defaults(func=cmd_cascade_lists)
1485
-
1486
- p_ltr = sub.add_parser(
1487
- "ltr-head",
1488
- help="Query-grouped CV LTR on stored hybrid k=50 (BM25/dense rank + title overlap)",
1489
- )
1490
- p_ltr.add_argument(
1491
- "--from-run",
1492
- dest="from_run",
1493
- default="search-esci-74-passages-k50",
1494
- )
1495
- p_ltr.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1496
- p_ltr.add_argument("--run", type=str, default="search-esci-74-ltr-head-k50")
1497
- p_ltr.add_argument("--k", type=int, default=50)
1498
- p_ltr.add_argument("--ks", type=str, default="5,10,20,50")
1499
- p_ltr.add_argument("--folds", type=int, default=5)
1500
- p_ltr.add_argument(
1501
- "--pair-policy",
1502
- dest="pair_policy",
1503
- default="unlabeled_zero",
1504
- choices=["unlabeled_zero", "other_query_neg"],
1505
- )
1506
- p_ltr.add_argument(
1507
- "--ce-model",
1508
- dest="ce_model",
1509
- default="",
1510
- help="Optional 4-class CE dir; ce_gain is a feature, not the ranker",
1511
- )
1512
- p_ltr.add_argument(
1513
- "--ltr-model",
1514
- dest="ltr_model",
1515
- default="ranknet",
1516
- choices=["ranknet", "lightgbm"],
1517
- help="Linear RankNet (default) or gated LightGBM lambdarank",
1518
- )
1519
- p_ltr.add_argument(
1520
- "--train-from-run",
1521
- dest="train_from_run",
1522
- default="",
1523
- help="Fit on this run's lists; apply to --from-run (no CV on eval qids)",
1524
- )
1525
- p_ltr.add_argument(
1526
- "--train-dataset",
1527
- dest="train_dataset",
1528
- default="",
1529
- help="JSONL for --train-from-run",
1530
- )
1531
- p_ltr.set_defaults(func=cmd_ltr_head)
1532
-
1533
- p_fs = sub.add_parser(
1534
- "pool-first-stage",
1535
- help="Local fielded BM25 over the JSONL corpus (not Reddy 0.857)",
1536
- )
1537
- p_fs.add_argument("--dataset", type=str, default="data/search_esci_74.jsonl")
1538
- p_fs.add_argument("--run", type=str, default="search-esci-74-fielded-bm25")
1539
- p_fs.add_argument(
1540
- "--variant",
1541
- default="all,title,title-boost,rm3,rm3-title-boost",
1542
- )
1543
- p_fs.add_argument(
1544
- "--expand-out",
1545
- type=str,
1546
- default="data/search_esci_74_rm3.jsonl",
1547
- )
1548
- p_fs.set_defaults(func=cmd_pool_first_stage)
1549
-
1550
- return parser
1551
-
1552
-
1553
- def main(argv: list[str] | None = None) -> int:
1554
- parser = build_parser()
1555
- args = parser.parse_args(argv)
1556
- settings = Settings.load(args.env_file)
1557
- if args.brain:
1558
- settings.brain_id = validate_brain_id(args.brain)
1559
- return int(args.func(args, settings))
1560
-
1561
-
1562
- if __name__ == "__main__":
1563
- raise SystemExit(main())