eliezer avihail Claude commited on
Commit
7c37d31
·
unverified ·
1 Parent(s): a9fcad0

retrieval: remove the cross-encoder reranker (ablation showed no earned keep) (#100)

Browse files

The 2026-07-10 built-in ablation on real content measured the reranker at
recall@8 0.810 (ON) vs 0.790 (OFF) and MRR 0.615 vs 0.620 — +0.02 recall,
-0.005 MRR. That does not justify a ~90MB cross-encoder loaded on the query
path (its cold download was the 17.9s latency outlier) plus ~0.3-0.5s per
query. On real content the dense+keyword RRF fusion already ranks well; the
reranker was a crutch from the empty-stub era. So it is removed, not just
disabled.

- index/retrieve.py: drop the rerank path and the rerank_fn param; the
interleaved per-kind RRF top-k is the final order (the exact-symbol pin
still applies). index/rerank.py deleted.
- app/main.py: drop the reranker warmup (no 90MB model to pre-load).
- eval.yml: drop the input and TORCHDOCS_RERANK env; run_retrieval
reverts from the ON/OFF ablation to a single pass (nothing left to ablate).
- conftest.py: drop the test-suite TORCHDOCS_RERANK=0 default; test_rerank.py
deleted. Stale comments in route.py / diagnose_retrieval.py updated.

218 tests pass; ruff clean.


Claude-Session: https://claude.ai/code/session_01LX1gm8fuzK2ZeEWi3Zar1b

Co-authored-by: Claude <noreply@anthropic.com>

.github/workflows/eval.yml CHANGED
@@ -32,15 +32,9 @@ on:
32
  live index was built with, or the query vector width won't fit."
33
  type: string
34
  default: "BAAI/bge-small-en-v1.5"
35
- rerank:
36
- description: "cross-encoder rerank stage (the production default is ON;
37
- pick 0 for a before/after ablation against the raw fused order)"
38
- type: choice
39
- options: ["1", "0"]
40
- default: "1"
41
  k:
42
  description: "top-k sections retrieved per question (the app's default
43
- is 8; try 4 to measure precision-over-volume after reranking)"
44
  type: string
45
  default: "8"
46
  provider:
@@ -92,7 +86,6 @@ jobs:
92
  NEON_URL: ${{ secrets.NEON }}
93
  TORCHDOCS_EVAL_SET: ${{ inputs.set }}
94
  TORCHDOCS_EMBED_MODEL: ${{ inputs.model }}
95
- TORCHDOCS_RERANK: ${{ inputs.rerank }}
96
  TORCHDOCS_RETRIEVAL_K: ${{ inputs.k }}
97
  run: python -m eval.run_retrieval
98
  - name: Rank diagnostic (why do the known misses miss)
@@ -175,7 +168,6 @@ jobs:
175
  TORCHDOCS_OPENAI_COMPAT_MODEL: ${{ inputs.llm }}
176
  TORCHDOCS_EVAL_SET: ${{ inputs.set }}
177
  TORCHDOCS_JUDGE_LIMIT: ${{ inputs.judge_limit }}
178
- TORCHDOCS_RERANK: ${{ inputs.rerank }}
179
  TORCHDOCS_RETRIEVAL_K: ${{ inputs.k }}
180
  run: python -m eval.run_judge
181
  - name: Commit results back to the branch
 
32
  live index was built with, or the query vector width won't fit."
33
  type: string
34
  default: "BAAI/bge-small-en-v1.5"
 
 
 
 
 
 
35
  k:
36
  description: "top-k sections retrieved per question (the app's default
37
+ is 8; try 4 to measure precision-over-volume)"
38
  type: string
39
  default: "8"
40
  provider:
 
86
  NEON_URL: ${{ secrets.NEON }}
87
  TORCHDOCS_EVAL_SET: ${{ inputs.set }}
88
  TORCHDOCS_EMBED_MODEL: ${{ inputs.model }}
 
89
  TORCHDOCS_RETRIEVAL_K: ${{ inputs.k }}
90
  run: python -m eval.run_retrieval
91
  - name: Rank diagnostic (why do the known misses miss)
 
168
  TORCHDOCS_OPENAI_COMPAT_MODEL: ${{ inputs.llm }}
169
  TORCHDOCS_EVAL_SET: ${{ inputs.set }}
170
  TORCHDOCS_JUDGE_LIMIT: ${{ inputs.judge_limit }}
 
171
  TORCHDOCS_RETRIEVAL_K: ${{ inputs.k }}
172
  run: python -m eval.run_judge
173
  - name: Commit results back to the branch
agent/route.py CHANGED
@@ -6,8 +6,8 @@ wall-clock — unacceptable for the common case. But the loop's value is real
6
  only for MULTI-SOURCE questions (catalog / compare / recipe / internals),
7
  where it assembles pages one search can't: measured agentic coverage 0.567 vs
8
  0.133 single-shot. A usage question ("how do I use SGD with momentum?") is
9
- answered by ONE retrieval pass + ONE generation — now backed by the reranked
10
- retrieval (recall@8 0.84).
11
 
12
  So: a zero-LLM-call heuristic sends multi-source shapes to the loop and
13
  everything else to the grounded single-shot path. The heuristic is allowed to
 
6
  only for MULTI-SOURCE questions (catalog / compare / recipe / internals),
7
  where it assembles pages one search can't: measured agentic coverage 0.567 vs
8
  0.133 single-shot. A usage question ("how do I use SGD with momentum?") is
9
+ answered by ONE retrieval pass + ONE generation — backed by the hybrid
10
+ per-kind RRF retrieval (recall@8 ~0.81 on real content).
11
 
12
  So: a zero-LLM-call heuristic sends multi-source shapes to the loop and
13
  everything else to the grounded single-shot path. The heuristic is allowed to
app/main.py CHANGED
@@ -108,8 +108,7 @@ def _warm_up() -> None:
108
  """Load the embedding model once so the first question isn't slow.
109
 
110
  This also covers the guard: its topicality check embeds the (translated)
111
- question with the same model. The reranker (when enabled) is warmed for
112
- the same reason — its ~90MB cross-encoder must not download mid-question.
113
  """
114
  try:
115
  from index.embed import embed_query
@@ -117,13 +116,6 @@ def _warm_up() -> None:
117
  embed_query("warmup")
118
  except Exception as exc: # noqa: BLE001 — warmup is best-effort
119
  print(f"[app] warmup skipped: {exc}")
120
- try:
121
- from index import rerank
122
-
123
- if rerank.enabled():
124
- rerank.rerank("warmup", [{"url": ""}, {"url": ""}], k=1)
125
- except Exception as exc: # noqa: BLE001 — warmup is best-effort
126
- print(f"[app] rerank warmup skipped: {exc}")
127
 
128
 
129
  def render(answer: Answer) -> str:
 
108
  """Load the embedding model once so the first question isn't slow.
109
 
110
  This also covers the guard: its topicality check embeds the (translated)
111
+ question with the same model.
 
112
  """
113
  try:
114
  from index.embed import embed_query
 
116
  embed_query("warmup")
117
  except Exception as exc: # noqa: BLE001 — warmup is best-effort
118
  print(f"[app] warmup skipped: {exc}")
 
 
 
 
 
 
 
119
 
120
 
121
  def render(answer: Answer) -> str:
eval/diagnose_retrieval.py CHANGED
@@ -85,7 +85,7 @@ def main() -> int:
85
  # The decisive number: the expected page's OWN nearest chunk — its
86
  # absolute cosine distance, and its true dense rank among ALL api
87
  # chunks (count of api chunks strictly closer). rank ~25 = a crowding
88
- # problem (a deeper pool / rerank helps); a far distance / rank in the
89
  # hundreds = an embedding problem (only doc-side enrichment helps).
90
  pat = f"%{expected}%"
91
  best = conn.execute(
 
85
  # The decisive number: the expected page's OWN nearest chunk — its
86
  # absolute cosine distance, and its true dense rank among ALL api
87
  # chunks (count of api chunks strictly closer). rank ~25 = a crowding
88
+ # problem (a deeper pool helps); a far distance / rank in the
89
  # hundreds = an embedding problem (only doc-side enrichment helps).
90
  pat = f"%{expected}%"
91
  best = conn.execute(
eval/run_retrieval.py CHANGED
@@ -31,14 +31,9 @@ from dotenv import load_dotenv
31
 
32
  EVAL_DIR = Path(__file__).parent
33
  EVAL_SET = os.environ.get("TORCHDOCS_EVAL_SET", "v1")
 
34
  _K = os.environ.get("TORCHDOCS_RETRIEVAL_K", "8")
35
-
36
-
37
- def _results_path(rerank_on: bool) -> Path:
38
- """Standard file for the reranked run; a _norerank sibling for the ablation
39
- (a k≠8 sweep suffixes k too) — so neither masquerades as the other."""
40
- variant = ("" if _K == "8" else f"_k{_K}") + ("" if rerank_on else "_norerank")
41
- return EVAL_DIR / "results" / f"retrieval_{EVAL_SET}{variant}.jsonl"
42
 
43
 
44
  def load_questions() -> dict[str, dict]:
@@ -86,19 +81,16 @@ def question_metrics(expected: list[list[str]], pointers: list[dict]) -> dict:
86
  }
87
 
88
 
89
- def _measure(questions: dict[str, dict], k: int, *, rerank_on: bool) -> dict:
90
- """One retrieval pass over the set at a fixed rerank setting.
91
-
92
- The rerank on/off toggle is TORCHDOCS_RERANK, which retrieve() reads (via
93
- index.rerank.enabled()) fresh on every call — so flipping the env here runs
94
- the exact production path with the reranker in or out. Returns the aggregate
95
- plus per-question records (also written to the pass's own results file)."""
96
- os.environ["TORCHDOCS_RERANK"] = "1" if rerank_on else "0"
97
  from index.retrieve import retrieve
98
 
99
- label = "rerank ON " if rerank_on else "rerank OFF"
 
 
 
100
  records, recalls, mrrs = [], [], []
101
- print(f"\n── {label} ── eval set: {EVAL_SET} ({len(questions)} questions), k={k}")
102
  print(f"{'id':<6}{'recall@' + str(k):<12}{'MRR':<8}misses")
103
  for qid, q in questions.items():
104
  expected = q.get("expected", [])
@@ -117,41 +109,17 @@ def _measure(questions: dict[str, dict], k: int, *, rerank_on: bool) -> dict:
117
  {"id": qid, "question": q["question"], "k": k, **m,
118
  "retrieved": [p["url"] + "#" + p.get("anchor", "") for p in pointers]}
119
  )
120
- path = _results_path(rerank_on)
121
- path.parent.mkdir(exist_ok=True)
122
- with path.open("w") as out:
123
- for record in records:
124
- out.write(json.dumps(record, ensure_ascii=False) + "\n")
125
- n = len(recalls)
126
- return {
127
- "recall": sum(recalls) / n if n else 0.0,
128
- "mrr": sum(mrrs) / n if n else 0.0,
129
- "n": n,
130
- "path": path,
131
- }
132
-
133
-
134
- def main() -> int:
135
- load_dotenv()
136
- k = int(os.environ.get("TORCHDOCS_RETRIEVAL_K", "8"))
137
- questions = load_questions()
138
 
139
- # measure BOTH configurations every run — the reranker's contribution is the
140
- # ablation the team keeps asking for, so make it a permanent side-by-side
141
- # instead of a manual rerank=0 dispatch nobody can trigger from mobile.
142
- on = _measure(questions, k, rerank_on=True)
143
- off = _measure(questions, k, rerank_on=False)
144
- if not on["n"]:
145
  print("no questions with expectations — nothing measured")
146
  return 1
147
-
148
- d_recall = on["recall"] - off["recall"]
149
- d_mrr = on["mrr"] - off["mrr"]
150
- print(f"\n{'config':<14}{'recall@' + str(k):<12}{'MRR':<8}")
151
- print(f"{'rerank ON':<14}{on['recall']:<12.3f}{on['mrr']:<8.3f}")
152
- print(f"{'rerank OFF':<14}{off['recall']:<12.3f}{off['mrr']:<8.3f}")
153
- print(f"{'Δ (rerank)':<14}{d_recall:<+12.3f}{d_mrr:<+8.3f}")
154
- print(f"\nresults → {on['path']} | {off['path']}")
155
  return 0
156
 
157
 
 
31
 
32
  EVAL_DIR = Path(__file__).parent
33
  EVAL_SET = os.environ.get("TORCHDOCS_EVAL_SET", "v1")
34
+ # a k≠8 sweep suffixes the file so it can't masquerade as the standard k=8 run
35
  _K = os.environ.get("TORCHDOCS_RETRIEVAL_K", "8")
36
+ RESULTS = EVAL_DIR / "results" / f"retrieval_{EVAL_SET}{'' if _K == '8' else f'_k{_K}'}.jsonl"
 
 
 
 
 
 
37
 
38
 
39
  def load_questions() -> dict[str, dict]:
 
81
  }
82
 
83
 
84
+ def main() -> int:
85
+ load_dotenv()
 
 
 
 
 
 
86
  from index.retrieve import retrieve
87
 
88
+ k = int(os.environ.get("TORCHDOCS_RETRIEVAL_K", "8"))
89
+ questions = load_questions()
90
+
91
+ RESULTS.parent.mkdir(exist_ok=True)
92
  records, recalls, mrrs = [], [], []
93
+ print(f"eval set: {EVAL_SET} ({len(questions)} questions), k={k}")
94
  print(f"{'id':<6}{'recall@' + str(k):<12}{'MRR':<8}misses")
95
  for qid, q in questions.items():
96
  expected = q.get("expected", [])
 
109
  {"id": qid, "question": q["question"], "k": k, **m,
110
  "retrieved": [p["url"] + "#" + p.get("anchor", "") for p in pointers]}
111
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ if not recalls:
 
 
 
 
 
114
  print("no questions with expectations — nothing measured")
115
  return 1
116
+ print(f"\naggregate over {len(recalls)} questions: "
117
+ f"mean recall@{k}={sum(recalls) / len(recalls):.3f} "
118
+ f"mean MRR={sum(mrrs) / len(mrrs):.3f}")
119
+ with RESULTS.open("w") as out:
120
+ for record in records:
121
+ out.write(json.dumps(record, ensure_ascii=False) + "\n")
122
+ print(f"results {RESULTS}")
 
123
  return 0
124
 
125
 
index/rerank.py DELETED
@@ -1,104 +0,0 @@
1
- """Cross-encoder reranking: the precision stage after candidate generation.
2
-
3
- Why a reranker, and why now: the gloss re-embed (measured 2026-07-09) moved
4
- several expected pages INTO their kind-pool (LayerNorm: true dense rank
5
- 899 → 2; SGD: 7 → 3) but bi-encoder distance + RRF still decide the final
6
- order. A cross-encoder reads query and candidate TOGETHER — the interaction a
7
- bi-encoder cannot see — and fixes exactly that ordering. It cannot rescue a
8
- page that never became a candidate (Linear at dense rank 3,412 needs deeper
9
- index-side enrichment, not reranking); see docs/retrieval-gaps-and-improvements.md.
10
-
11
- What the cross-encoder scores: the index stores no chunk content (pointers
12
- only), and hydrating dozens of candidates per query is too slow for the live
13
- app — so each candidate is scored on its metadata line: symbol + page title +
14
- heading path + the page's committed gloss (index/glosses.jsonl). For api pages
15
- the gloss is exactly the plain-language "what is this page for" sentence, which
16
- is the signal a relevance judgment needs.
17
-
18
- Runs on CPU (~90MB model, a few hundred ms for a 24-candidate slate), behind
19
- the TORCHDOCS_RERANK kill switch so eval can measure before/after and a broken
20
- model can be turned off without a deploy. Fail-open: any model error returns
21
- the fused order unchanged — reranking must never take retrieval down.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import os
27
- import threading
28
- from typing import Any
29
-
30
- RERANK_MODEL = os.environ.get("TORCHDOCS_RERANK_MODEL", "cross-encoder/ms-marco-MiniLM-L-6-v2")
31
-
32
- # candidates fed to the cross-encoder: k=8 finals from a 24-candidate slate
33
- # keeps 3× headroom for the reorder while staying sub-second on CPU
34
- RERANK_SLATE = int(os.environ.get("TORCHDOCS_RERANK_SLATE", "24"))
35
-
36
-
37
- def enabled() -> bool:
38
- """Kill switch, read per call so tests/deploys can flip it late."""
39
- return os.environ.get("TORCHDOCS_RERANK", "1") not in ("0", "false", "no")
40
-
41
-
42
- _MODEL_LOCK = threading.Lock()
43
- _MODEL = None
44
-
45
-
46
- def _model():
47
- # same double-checked load as index/embed._model: exactly one instance,
48
- # concurrent first calls must not download/build the model twice
49
- global _MODEL
50
- if _MODEL is None:
51
- with _MODEL_LOCK:
52
- if _MODEL is None:
53
- from sentence_transformers import CrossEncoder
54
-
55
- print(f"[rerank] loading {RERANK_MODEL} (first run downloads ~90MB)")
56
- _MODEL = CrossEncoder(RERANK_MODEL, device="cpu")
57
- return _MODEL
58
-
59
-
60
- def _score_pairs(pairs: list[tuple[str, str]]) -> list[float]:
61
- return [float(s) for s in _model().predict(pairs, show_progress_bar=False)]
62
-
63
-
64
- def rerank_text(pointer: dict[str, Any], glosses: dict[str, str]) -> str:
65
- """The candidate line the cross-encoder judges the query against."""
66
- from index.embed import symbol_from_url
67
-
68
- parts = [
69
- p
70
- for p in (
71
- symbol_from_url(pointer.get("url", "")),
72
- pointer.get("page_title", ""),
73
- pointer.get("heading_path", ""),
74
- glosses.get(pointer.get("url", ""), ""),
75
- )
76
- if p
77
- ]
78
- return ". ".join(parts)
79
-
80
-
81
- def rerank(
82
- query: str,
83
- pointers: list[dict[str, Any]],
84
- k: int,
85
- scorer=None,
86
- ) -> list[dict[str, Any]]:
87
- """Reorder candidate pointers by cross-encoder relevance; return the top-k.
88
-
89
- `scorer` (tests) replaces the model: (list of (query, text) pairs) → scores.
90
- Fail-open: any scoring error keeps the incoming (RRF-fused) order.
91
- """
92
- if len(pointers) <= 1:
93
- return pointers[:k]
94
- from index.embed import load_glosses
95
-
96
- glosses = load_glosses()
97
- pairs = [(query, rerank_text(p, glosses)) for p in pointers]
98
- try:
99
- scores = (scorer or _score_pairs)(pairs)
100
- except Exception as exc: # noqa: BLE001 — reranking must never break retrieval
101
- print(f"[rerank] scoring failed ({type(exc).__name__}: {exc}); keeping fused order")
102
- return pointers[:k]
103
- order = sorted(range(len(pointers)), key=lambda i: scores[i], reverse=True)
104
- return [pointers[i] for i in order[:k]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index/retrieve.py CHANGED
@@ -8,12 +8,13 @@ interleaved strongest-first into the top-k, and the answering model judges
8
  what is actually relevant.
9
  This replaced a global-RRF design whose crowding fixes (a reference channel,
10
  reserved seats, distance gates) traded one benchmark regression for another.
11
- A cross-encoder rerank (index/rerank.py) then reorders a wide slate of the
12
- fused candidates into the final top-k precision after candidate generation.
 
13
 
14
- Returns POINTERS (url, anchor, heading path, ...) — never content. The
15
- caller hydrates content from the snapshot (index/hydrate.py). This is the
16
- engine behind the agent's `search_docs` tool.
17
  """
18
 
19
  from __future__ import annotations
@@ -157,7 +158,6 @@ def retrieve(
157
  pool: int = 20,
158
  debug: bool = False,
159
  kind: str | None = None,
160
- rerank_fn=None,
161
  ) -> list[dict[str, Any]]:
162
  """Top-k pointers for a query, drawn from per-kind pools.
163
 
@@ -167,16 +167,16 @@ def retrieve(
167
  (e.g. `scaled_dot_product_attention`) that dense similarity misses.
168
 
169
  Candidates farther than RELEVANCE_GAP from the best hit are dropped, then
170
- the pools are interleaved (strongest pool first) into a wide slate. A
171
- cross-encoder rerank (index/rerank.py the precision stage; kill switch
172
- TORCHDOCS_RERANK) reorders the slate into the top-k; when it is off the
173
- slate's first k are the top-k unchanged. An exact-symbol match is pinned
174
- first, docs-search style.
 
175
 
176
  `kind` restricts the search to one content space — the agent's planner
177
  sets it when it decides the question needs the reference catalog rather
178
- than tutorial prose. `rerank_fn` (tests) replaces the real reranker and
179
- forces the rerank path on.
180
  """
181
  from contextlib import nullcontext
182
 
@@ -236,21 +236,11 @@ def retrieve(
236
  for ck in ranked[:3]:
237
  print(f"[debug] {pointers[ck]['heading_path']} | {pointers[ck]['url']}")
238
 
239
- from index import rerank as rerank_mod
240
-
241
- # rerank reads a slate wider than k; round-robin order is prefix-stable, so
242
- # with reranking off (or failing open) the first k match today's behavior
243
- use_rerank = rerank_fn is not None or rerank_mod.enabled()
244
- slate = max(k, rerank_mod.RERANK_SLATE) if use_rerank else k
245
- ranked = _interleave_pools(pools, dists, slate)
246
-
247
- if use_rerank and len(ranked) > 1:
248
- rerank_fn = rerank_fn or rerank_mod.rerank
249
- ranked = [p["chunk_key"] for p in rerank_fn(query, [pointers[ck] for ck in ranked], k=k)]
250
 
251
  # exact API lookup: if the user typed a precise symbol and its reference
252
  # page was found, pin it first — the docs-search behavior users expect
253
- # (an explicit identifier outranks even the cross-encoder's judgment)
254
  if symbol:
255
  exact = next(
256
  (ck for kd, pl in pools for ck in pl if is_exact_api(pointers[ck], symbol)),
 
8
  what is actually relevant.
9
  This replaced a global-RRF design whose crowding fixes (a reference channel,
10
  reserved seats, distance gates) traded one benchmark regression for another.
11
+ (A cross-encoder rerank stage once reordered the fused slate; the 2026-07-10
12
+ ablation on real content measured it at +0.02 recall / −0.005 MRR and it was
13
+ removed — the fused RRF order is the final order.)
14
 
15
+ Returns POINTERS (url, anchor, heading path, content, ...). The caller reads
16
+ each pointer's stored content directly (index/hydrate.py). This is the engine
17
+ behind the agent's `search_docs` tool.
18
  """
19
 
20
  from __future__ import annotations
 
158
  pool: int = 20,
159
  debug: bool = False,
160
  kind: str | None = None,
 
161
  ) -> list[dict[str, Any]]:
162
  """Top-k pointers for a query, drawn from per-kind pools.
163
 
 
167
  (e.g. `scaled_dot_product_attention`) that dense similarity misses.
168
 
169
  Candidates farther than RELEVANCE_GAP from the best hit are dropped, then
170
+ the pools are interleaved (strongest pool first) and the first k are the
171
+ result. An exact-symbol match is pinned first, docs-search style. (A
172
+ cross-encoder rerank stage sat here until the 2026-07-10 ablation on real
173
+ content measured it at +0.02 recall / −0.005 MRR no earned keep for its
174
+ ~90MB model and per-query cost — so it was removed; the fused RRF order is
175
+ the final order.)
176
 
177
  `kind` restricts the search to one content space — the agent's planner
178
  sets it when it decides the question needs the reference catalog rather
179
+ than tutorial prose.
 
180
  """
181
  from contextlib import nullcontext
182
 
 
236
  for ck in ranked[:3]:
237
  print(f"[debug] {pointers[ck]['heading_path']} | {pointers[ck]['url']}")
238
 
239
+ ranked = _interleave_pools(pools, dists, k)
 
 
 
 
 
 
 
 
 
 
240
 
241
  # exact API lookup: if the user typed a precise symbol and its reference
242
  # page was found, pin it first — the docs-search behavior users expect
243
+ # (an explicit identifier outranks the fused ranking)
244
  if symbol:
245
  exact = next(
246
  (ck for kd, pl in pools for ck in pl if is_exact_api(pointers[ck], symbol)),
tests/conftest.py CHANGED
@@ -5,21 +5,13 @@ and agent/translate.py caches default-path translations. Both are deliberate
5
  in production — state must be shared across requests — but they would leak
6
  between tests: a model named "model-a" cooled down by one test would be
7
  silently skipped in the next.
8
-
9
- The cross-encoder reranker (on by default in production) is switched off for
10
- the unit suite — no test may download a 90MB model. Tests that exercise the
11
- rerank path inject a fake scorer via retrieve(rerank_fn=...) instead.
12
  """
13
 
14
- import os
15
-
16
  import pytest
17
 
18
  from agent.llm import reset_cooldowns
19
  from agent.translate import _translate_default
20
 
21
- os.environ.setdefault("TORCHDOCS_RERANK", "0")
22
-
23
 
24
  @pytest.fixture(autouse=True)
25
  def _reset_shared_llm_state():
 
5
  in production — state must be shared across requests — but they would leak
6
  between tests: a model named "model-a" cooled down by one test would be
7
  silently skipped in the next.
 
 
 
 
8
  """
9
 
 
 
10
  import pytest
11
 
12
  from agent.llm import reset_cooldowns
13
  from agent.translate import _translate_default
14
 
 
 
15
 
16
  @pytest.fixture(autouse=True)
17
  def _reset_shared_llm_state():
tests/index/test_rerank.py DELETED
@@ -1,106 +0,0 @@
1
- """The rerank stage — scoring text, ordering, fail-open, and retrieve() wiring."""
2
-
3
- from index.rerank import enabled, rerank, rerank_text
4
- from index.retrieve import retrieve
5
-
6
-
7
- def _ptr(key, url="https://docs.pytorch.org/docs/stable/generated/torch.nn.Linear.html",
8
- title="Linear", heading="Linear", kind="api"):
9
- return {
10
- "chunk_key": key, "url": url, "anchor": "", "page_title": title,
11
- "heading_path": heading, "library": "core", "kind": kind,
12
- "source_link": "", "part": 0,
13
- }
14
-
15
-
16
- def test_enabled_reads_env_per_call(monkeypatch):
17
- monkeypatch.setenv("TORCHDOCS_RERANK", "0")
18
- assert not enabled()
19
- monkeypatch.setenv("TORCHDOCS_RERANK", "1")
20
- assert enabled()
21
-
22
-
23
- def test_rerank_text_includes_symbol_title_heading_and_gloss(monkeypatch):
24
- url = "https://docs.pytorch.org/docs/stable/generated/torch.nn.Linear.html"
25
- glosses = {url: "The standard fully-connected layer applying a weight matrix and bias."}
26
- text = rerank_text(_ptr("a", url=url), glosses)
27
- assert "torch.nn.Linear" in text # symbol from the url
28
- assert "Linear" in text
29
- assert "fully-connected layer" in text # the gloss is the semantic payload
30
-
31
-
32
- def test_rerank_orders_by_score_and_cuts_to_k(monkeypatch):
33
- monkeypatch.setattr("index.embed.load_glosses", lambda: {})
34
- ptrs = [_ptr("low"), _ptr("high"), _ptr("mid")]
35
- scores = {"low": 0.1, "high": 0.9, "mid": 0.5}
36
-
37
- def scorer(pairs):
38
- # pairs align with ptrs order; look scores up by position
39
- return [scores[p["chunk_key"]] for p in ptrs]
40
-
41
- out = rerank("q", ptrs, k=2, scorer=scorer)
42
- assert [p["chunk_key"] for p in out] == ["high", "mid"]
43
-
44
-
45
- def test_rerank_fails_open_to_the_incoming_order(monkeypatch):
46
- monkeypatch.setattr("index.embed.load_glosses", lambda: {})
47
-
48
- def broken(pairs):
49
- raise RuntimeError("model exploded")
50
-
51
- ptrs = [_ptr("first"), _ptr("second"), _ptr("third")]
52
- out = rerank("q", ptrs, k=2, scorer=broken)
53
- assert [p["chunk_key"] for p in out] == ["first", "second"]
54
-
55
-
56
- def test_rerank_single_candidate_skips_scoring():
57
- # no glosses/model needed — must not even build pairs
58
- out = rerank("q", [_ptr("only")], k=8, scorer=None)
59
- assert [p["chunk_key"] for p in out] == ["only"]
60
-
61
-
62
- # --- retrieve() wiring -------------------------------------------------------
63
-
64
- from tests.index.test_retrieve import FakeConn, _row # noqa: E402 — shared fakes
65
-
66
-
67
- def test_retrieve_feeds_a_wide_slate_to_the_reranker_and_keeps_its_order():
68
- refs = [_row(f"ref{i}", kind="api", dist=0.2 + i / 100) for i in range(10)]
69
- conn = FakeConn([refs, [], [], [], [], []])
70
- seen = {}
71
-
72
- def fake_rerank(query, pointers, k):
73
- seen["slate"] = len(pointers)
74
- return list(reversed(pointers))[:k] # deliberately invert the fused order
75
-
76
- results = retrieve(
77
- "descriptive question", k=3, conn=conn,
78
- embed_fn=lambda q: [0.0] * 384, rerank_fn=fake_rerank,
79
- )
80
- assert seen["slate"] == 10 # the whole fused slate, not just top-k
81
- assert [r["chunk_key"] for r in results] == ["ref9", "ref8", "ref7"]
82
-
83
-
84
- def test_retrieve_skips_reranker_when_disabled(monkeypatch):
85
- monkeypatch.setenv("TORCHDOCS_RERANK", "0")
86
- refs = [_row(f"ref{i}", kind="api", dist=0.2) for i in range(4)]
87
- conn = FakeConn([refs, [], [], [], [], []])
88
- results = retrieve("q", k=2, conn=conn, embed_fn=lambda q: [0.0] * 384)
89
- # fused order preserved — no reranker touched it (none injected, env off)
90
- assert [r["chunk_key"] for r in results] == ["ref0", "ref1"]
91
-
92
-
93
- def test_exact_symbol_pin_outranks_the_reranker():
94
- exact = _row("exact", kind="api", url="https://docs.pytorch.org/docs/stable/generated/torch.optim.SGD.html")
95
- refs = [_row(f"ref{i}", kind="api", dist=0.2) for i in range(3)]
96
- # query pools: dense, keyword, then the symbol channel returns the exact page
97
- conn = FakeConn([refs, [], [], [], [], [], [exact]])
98
-
99
- def fake_rerank(query, pointers, k):
100
- return [p for p in pointers if p["chunk_key"] != "exact"][:k] # reranker demotes it
101
-
102
- results = retrieve(
103
- "how do I use torch.optim.SGD?", k=3, conn=conn,
104
- embed_fn=lambda q: [0.0] * 384, rerank_fn=fake_rerank,
105
- )
106
- assert results[0]["chunk_key"] == "exact" # the pin wins anyway