1. Get the architecture right: two-stage retrieval
Reranking is not a standalone retrieval method. It is the second stage of a pipeline.1
Recall
Use vector search or BM25 to pull a candidate set out of the full corpus. This stage must
be fast and should over-fetch: the goal is “the answer is somewhere in here,” not
“the answer is first.”
2
Rerank
Send the candidate set plus the query to
bge-reranker-v2-m3 to score and re-sort.
This stage must be accurate: the goal is to push the right answer to the top.3
Truncate and feed the LLM
Take the reranked top 3–5 into your prompt. The more precise this is, the less the model
hallucinates and the less context you pay for.
Why you can’t skip recall and rerank everything: a cross-encoder needs the query before it can
compute anything, so nothing can be precomputed offline. Scoring millions of documents one at a
time is unaffordable in both cost and latency. Recall takes you from millions to hundreds;
reranking sorts those hundreds correctly.
A complete working example
This is a minimal but complete two-stage retriever —text-embedding-3-small for recall, bge-reranker-v2-m3 for reranking, both through the same APIYI token:
2. How many candidates should you recall?
Measured latency (short documents, P50 of 5 runs per rung):
The important part is the ~2 second fixed overhead: even one document costs 2 seconds. Going from 1 to 100 candidates adds only 1.8 seconds, and the retrieval-quality gain is worth far more than that.
What actually drives latency is total tokens, not document count. Same 50 candidates:
Same count, 4.7× the latency. This is also why the chunking in the next section doesn’t slow retrieval down — chunking raises document count without raising total tokens.
If you genuinely need to rank many documents (offline batch work, say), split into several ≤100-document requests with concurrency capped at 4. Note that splitting doesn’t reduce total tokens — TPM 20,000 remains the overall gate, so batch jobs need per-minute throttling. That quota is being expanded; for large batch workloads, check the currently available headroom with APIYI support first.
3. Chunk long documents
This is the highest-leverage preprocessing step. Two measurements explain why. First, unrelated content dilutes relevance. The same matching sentence with varying amounts of filler appended:
Second, length inflates noise scores. The same irrelevant document scored 0.029 short and 0.121 after 450 characters of filler — 4× higher.
Put together: a long document with the answer at the end loses to a long document that’s entirely off-topic.
4. Setting thresholds (don’t just pick 0.5)
relevance_score is sigmoid-mapped into the 0–1 range, which makes it look like a confidence value. It isn’t.
Measured distribution across all quality test cases:
A fixed 0.5 threshold would drop every correct cross-lingual result and most second-place hits in non-Chinese languages, while admitting a heavy-keyword-overlap distractor (for “Apple Inc. FY2024 revenue,” a document about apple farming prices scored 0.945 and ranked third).
Note the last row: the highest-scoring irrelevant document (0.945) outscores the lowest-scoring relevant one (0.289). The two distributions overlap — which is precisely why no “safe” absolute threshold exists.
5. Negation needs a safety net
This is the model’s clear weakness. For “Which attractions are good to visit in winter?”, two documents that explicitly say they are not ranked 2nd and 3rd, pushing genuinely relevant results down to 4th and 5th. nDCG@3 came out at 0.47. The model matched the topic “winter + attractions + travel” and never processed the negation. The fix: add a lightweight LLM verification pass after reranking. A cheap small model is enough:6. Multilingual and cross-lingual use
Chinese, English, Japanese, Korean, Russian, French and Arabic — plus ZH↔EN cross-lingual retrieval — all ordered correctly in testing. One model covers a multilingual knowledge base; you don’t need per-language deployments. But watch the score magnitudes:
Cross-lingual correct answers score 1–2 orders of magnitude lower — but the ordering still holds.
7. Concurrency, caching and idempotency
Budget the quota first, then talk about concurrency
The upstream (Huawei Cloud MaaS) allows TPM 20,000 / RPM 120 for this model. This is the constraint people most often underestimate — the BGE-M3 embedding model on the same platform gets 1,200,000 TPM, 60× more. Testing (with a 70-second silence before each case to let the quota window reset) separated two independent mechanisms:
Finding 1: roughly 4–5 concurrent in-flight slots. R1/R2 consume only 3–4% of TPM yet fail widely, and raising concurrency from 10 to 20 still caps successes at 4–5 — while R5’s 20 serial requests all pass. Requests beyond the slot limit are rejected outright, not queued.
Finding 2: TPM 20,000 applies independently of concurrency. R4 is fully serial, and the 8th request 429s at 16,093 cumulative tokens, with 9 consecutive failures before the window slides:
How many searches per minute
Using the measured≈26.9 tokens/document:
Candidate count is a capacity decision as much as a quality one: doubling candidates buys limited quality but halves throughput.
It also explains why a single request can’t carry too many candidates — 1000 candidates is 26,867 tokens, 134% of the entire minute’s budget in one call, so it is guaranteed to 429. At 2000 it’s 269%.
Quota expansion is in progress. The TPM 20,000 above is the upstream’s initial allocation, and
APIYI has already applied to have it raised. If your workload exceeds what the table allows,
contact APIYI support to have the upstream quota reviewed rather than scaling your design down
to fit the current number.
Caching
Repeating an identical request 10 times is bit-identical (zero drift). Drift of ~3.7e-4 appears only when the candidate order is shuffled (bf16 batching jitter), and ordering is unaffected. So results are safe to cache:- Key on
normalised query + ordered hash of document contents - Do not use
relevance_scoreitself as an idempotency or dedupe key — change the candidate order and its last digits move - Repeated queries (FAQs, popular searches) usually cache well, saving both latency and upstream pressure
8. Integrating with existing frameworks
Parameter names match Cohere Rerank v1 (query / documents / top_n / return_documents), but documents only accepts a string array — [{"text": "..."}] returns 400 — and the response has no id / meta fields.
9. Pre-launch checklist
Correctness
Correctness
- Mapping back via
results[].index, not by matching text - Long documents chunked (200–500 chars) and aggregated by best chunk score
- No single query-document pair exceeds 8192 tokens
- Queries with negation or exclusion have an LLM verification pass
Reliability
Reliability
- Candidate sets capped at 100
- Concurrency capped at 4 (measured: upstream allows ~4–5 in-flight requests, excess is rejected outright rather than queued)
- Peak throughput checked against TPM 20,000 (~15 searches/min at 100 candidates) and confirmed sufficient
- Backoff-retry implemented for 429 (upstream congestion is transient)
- Timeout set to 60 s or more (100 short documents is ~4.3 s at P95, but long documents or large sets reach tens of seconds)
- Request path confirmed as
/v1/rerank— a wrong path returns 200 + HTML, not 404 - Model name spelled correctly and lowercase — a typo returns 503, easily mistaken for an outage
Quality
Quality
- Filtering uses Top-N or a relative threshold, not a guessed fixed value
- Multilingual and cross-lingual score magnitudes verified so low scores don’t get filtered out
- A small labelled query set A/B tested (rerank on vs off) to confirm the metrics actually moved
Cost and caching
Cost and caching
- Aware that
usage.input_tokens/output_tokensare always 0 — readprompt_tokens/total_tokens - Aware that neither
top_nnorreturn_documentsreduces usage — every candidate is scored - Cost accounting based on the console invoice (
prompt_tokensandtotal_tokensdiffer by ~45% at 100 candidates, and this round could not confirm which one is charged) - Result caching in place for high-frequency queries