Cut query latency ~6.8x: eval-gated reranker pool + doc-cap tuning
Browse filesProfiling (per-stage timers on the live index) showed the cross-encoder
rerank of the candidate pool is ~96% of query time -- ~4.3s of the ~4.5s
total; BM25, fusion and the semantic stage round to zero. The pool size
and the per-document text cap are therefore the only latency levers that
matter.
Both are now env-sweepable (CANLEX_RERANK_POOL, CANLEX_RERANK_DOC_CHARS)
and were swept with the 159-question eval as the gate:
POOL=50 CHARS=3000 (old): Hit@1 .81 / Hit@3 .94 / Hit@5 .97 / MRR .87, ~4470 ms/q
POOL=24 CHARS=1500 : identical metrics, ~1730 ms/q
POOL=16 CHARS=1000 (new): Hit@1 .81 / Hit@3 .94 / Hit@5 .97 / MRR .88, ~660 ms/q
The most aggressive config holds the eval exactly (MRR +0.01) at 6.8x
lower latency, so it ships as the default. Not pushed below 16: the
promote-only reranker needs a meaningful pool to lift low-fusion
candidates, and tuning further would fit the eval set rather than real
robustness. The doc cap is benign in practice -- the median section is
~520 chars and sub-chunked pieces cap at 1800.
Also cuts the eval harness from ~13 min to ~2.3 min, and proportionally
lightens every query on the 2-vCPU Space. 57 tests pass. No corpus or
embedding change (embeddings.npz untouched).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- canlex/index.py +8 -1
- canlex/rerank.py +9 -1
|
@@ -16,7 +16,14 @@ B = 0.75
|
|
| 16 |
RRF_K = 60 # reciprocal-rank-fusion damping constant
|
| 17 |
W_SEM = 2.0 # weight on the semantic retriever in the fusion (1.0 = equal; eval-tuned)
|
| 18 |
CANDIDATES = 80 # hits each retriever contributes to the fusion
|
| 19 |
-
RERANK_POOL =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
MN_WEIGHT = float(os.environ.get("CANLEX_MN_WEIGHT", "0.0024"))
|
| 21 |
# title-match boost per unit of idf-weighted overlap between
|
| 22 |
# the query and a candidate's marginal note (section title)
|
|
|
|
| 16 |
RRF_K = 60 # reciprocal-rank-fusion damping constant
|
| 17 |
W_SEM = 2.0 # weight on the semantic retriever in the fusion (1.0 = equal; eval-tuned)
|
| 18 |
CANDIDATES = 80 # hits each retriever contributes to the fusion
|
| 19 |
+
RERANK_POOL = int(os.environ.get("CANLEX_RERANK_POOL", "16"))
|
| 20 |
+
# top fused candidates the cross-encoder rescores. Profiled
|
| 21 |
+
# at ~96% of query latency (the pool is cross-encoded on
|
| 22 |
+
# CPU), so this is THE latency knob; env-sweepable, and any
|
| 23 |
+
# change must be eval-gated (a smaller pool limits which
|
| 24 |
+
# low-fusion candidates the promote-only rerank can lift).
|
| 25 |
+
# Swept 2026-06: 50 -> 16 (with doc cap 3000 -> 1000) held
|
| 26 |
+
# the 159-Q eval exactly (MRR +0.01) at ~5.5x lower latency.
|
| 27 |
MN_WEIGHT = float(os.environ.get("CANLEX_MN_WEIGHT", "0.0024"))
|
| 28 |
# title-match boost per unit of idf-weighted overlap between
|
| 29 |
# the query and a candidate's marginal note (section title)
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
"""Cross-encoder reranker over ONNX Runtime (local, key-free)."""
|
| 2 |
import json
|
|
|
|
| 3 |
|
| 4 |
import numpy as np
|
| 5 |
import onnxruntime as ort
|
|
@@ -9,7 +10,14 @@ from tokenizers import Tokenizer
|
|
| 9 |
RERANK_REPO = "Xenova/bge-reranker-base"
|
| 10 |
RERANK_ONNX = "onnx/model_quantized.onnx" # int8: ~3x faster on CPU, negligible quality loss
|
| 11 |
MAX_TOKENS = 512
|
| 12 |
-
_MAX_DOC_CHARS =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
class Reranker:
|
|
|
|
| 1 |
"""Cross-encoder reranker over ONNX Runtime (local, key-free)."""
|
| 2 |
import json
|
| 3 |
+
import os
|
| 4 |
|
| 5 |
import numpy as np
|
| 6 |
import onnxruntime as ort
|
|
|
|
| 10 |
RERANK_REPO = "Xenova/bge-reranker-base"
|
| 11 |
RERANK_ONNX = "onnx/model_quantized.onnx" # int8: ~3x faster on CPU, negligible quality loss
|
| 12 |
MAX_TOKENS = 512
|
| 13 |
+
_MAX_DOC_CHARS = int(os.environ.get("CANLEX_RERANK_DOC_CHARS", "1000"))
|
| 14 |
+
# cap doc text before tokenizing. Cross-encoder cost is
|
| 15 |
+
# ~linear in tokens per doc, so halving this ~halves the
|
| 16 |
+
# dominant query cost; env-sweepable, eval-gated (a
|
| 17 |
+
# tighter cap scores long sections on less of their text
|
| 18 |
+
# -- fine in practice: the median section is ~520 chars,
|
| 19 |
+
# and sub-chunked pieces cap at 1800). Swept 2026-06:
|
| 20 |
+
# 3000 -> 1000 (with pool 50 -> 16) held the eval exactly.
|
| 21 |
|
| 22 |
|
| 23 |
class Reranker:
|