Improving retrieval quality
This content is not available in your language yet.
Retrieval can fail two different ways, and they need opposite fixes. A document can be missing from what comes back at all, or it can be present but ranked wrong — buried under weaker passages, crowded out by five chunks of the same page, or simply not the one an agent should have led with. This page is about the second kind: the knobs that change order, once the right chunks are already being retrieved.
Start by looking, not tuning
Section titled “Start by looking, not tuning”Before changing anything, look at what a query actually returns. Search
preview, on a collection’s own screen, runs the exact pipeline an
agent’s search tool uses — same hybrid fusion, same reranking, same
settings — against a query you type by hand.
Turn on the candidate view while you’re there. It shows what was retrieved before reranking, alongside what was returned after. That split answers the one diagnostic that matters here: a chunk that never appears in the candidate list was never retrieved at all, and no amount of ranking tuning will produce it. Adjusting recency, boost, or diversity changes the order of chunks that are already candidates — it has nothing to offer a chunk that isn’t. If your document is missing from the candidates, the problem is upstream: chunking, embeddings, or the hybrid weight, covered by Reviewing chunks and Collections & knowledge.
Only once you can see the right chunk sitting in the candidate list, in the wrong position, are the settings on this page the ones to reach for.
The knobs, in the order worth trying
Section titled “The knobs, in the order worth trying”Retrieval quality has more than one dial, and they aren’t all equally worth your time. Roughly in the order they tend to pay off:
- Chunking — how a document is cut into passages. Wrong boundaries mean the right sentence sits in the wrong chunk, which nothing downstream can fix. See Reviewing chunks.
- Hybrid weight — how much of the fused score comes from meaning versus keywords. A terminology-heavy collection (contract clauses, part numbers) usually wants more keyword weight than a narrative one.
- Score threshold — how confident the reranker must be before an answer is accepted rather than the query being broadened and retried.
- Model-written context and questions — an opt-in enrichment that has a model write, per chunk, a short note placing it in its document and a few questions it answers, both indexed beside the chunk. Like chunking and hybrid weight it changes what is retrieved rather than how a candidate set is ordered — but unlike them it costs a model call per chunk, which is why it sits here rather than at the top. See the section below.
- Recency, collection boost, diversity and ordering — the ranking knobs this page covers. They reorder a candidate set that chunking and hybrid weight already got right; they cannot rescue one that didn’t.
If a collection retrieves badly, work down this list in order. Reaching for recency or diversity before checking chunking is the most common way to tune a knob that was never going to fix the actual problem.
Recency
Section titled “Recency”Text similarity alone cannot tell a 2019 policy from its 2025 revision — to a reranker, an old document saying the same thing in similar words scores just as well as the current one. Recency is a boost that corrects for that, applied on the collection’s Settings tab.
How it ranks. Turning recency on picks a date field and applies a half-life decay: at the configured half-life, a document has lost half of the configured weight; beyond that it keeps decaying, bounded so the weight is a hard ceiling on how far recency can ever move a result. It is a boost, never a filter — an old document still competes, just with a handicap, so a genuinely better old passage can still win.
The three kinds of date you can rank on. A collection can rank on any
field it has declared with type date (see Metadata &
filters), on the two reserved keys created
and modified when a document’s own metadata carries them, or on
upload time — literally when the file reached the platform, not any
date the document itself claims. Upload time is the honest fallback: most
parsed formats don’t extract an authored date at all today, so for many
collections it’s the only date that’s always there. Pick it only when you
mean “newest upload wins,” which is not the same question as “newest
document wins.”
A document with no usable date is treated as the oldest one, not as a neutral one. If you turn recency on and a document has nothing in the field you named, that document gets the same penalty as the oldest possible age — it is never allowed to jump ahead of every dated document by default. This is deliberate: the alternative (treating “unknown” as “now”) would let an undated document silently outrank ones that actually have a date, which is the opposite of what turning recency on asks for. The practical consequence: if nothing in your collection has the date field you picked, recency changes nothing at all, because every candidate takes the identical penalty. That’s a safe failure mode to notice, not a bug — it means the field name doesn’t match what’s actually in your documents’ metadata.
Letting an agent ask for “the latest.” search’s recency_bias
parameter (0 to 1) lets an agent scale up a collection’s configured
recency weight for one call, for questions that are explicitly about the
current state of something. It can only scale a weight that’s already
configured — it can’t invent recency ranking on a collection that has
none, and it can’t push the weight past its ceiling.
Collection boost
Section titled “Collection boost”When an agent searches several collections at once, results are merged and ordered together. Collection boost multiplies a collection’s scores relative to the others in that merge — set it above 1 to make a collection’s results generally outrank the rest, or below 1 to hold it back.
Use it sparingly. Boost is a blunt instrument: set it high enough to matter and it can float this collection’s weakest chunk above another collection’s best one, purely by arithmetic, with nothing to do with which passage actually answers the question. Prefer attaching fewer, better-scoped collections to an agent over leaning on boost to fix a crowded one — see Attach knowledge to an agent.
Diversity and the per-document cap
Section titled “Diversity and the per-document cap”Five near-identical chunks of the same page can fill an entire answer, crowding out a passage from a different document that would have served the question better. Two settings address this, and they work together rather than as alternatives.
The per-document cap limits how many chunks from a single document
can appear in the final result list. The platform default is 5. It
never reduces how many results a search returns: when there aren’t enough
other candidates to fill the remaining slots, the cap backfills with
whatever is left, including chunks from the document it just capped. So
turning the cap on costs you ordering, never count — a search that
would have returned 8 results still returns 8, just with a preference for
variety where variety was available. Set it to 0 to turn the cap off
explicitly.
Diversity (MMR λ) goes further: it actively penalizes a candidate for being too similar to one already chosen, rather than just counting chunks per document. λ close to 1 favors relevance; close to 0 favors spread. It’s off by default, and turning it on costs one extra query per search (the candidates are compared to each other in the database rather than being shipped out of it). Try the per-document cap first — it’s free and it’s on by default — and reach for MMR only if a collection still reads as repetitive with the cap already in place.
When an agent searches multiple collections, the strictest setting among the ones actually searched wins for both of these: the smallest cap and the smallest λ, so one collection’s preference for variety isn’t overruled by another’s silence on the subject.
Lost-in-the-middle ordering
Section titled “Lost-in-the-middle ordering”Once the final results are chosen, this setting reorders how they’re presented — nothing about which chunks are returned changes, only the sequence a model reads them in: strongest first, then alternating outward so the strongest passages sit at both the start and the end of the block, with weaker ones buried in the middle.
This exists because models tend to attend more reliably to the beginning and end of a long context than to what’s between them. It’s a presentation change applied after everything else — after selection, after grading — so it never affects which passages your agent had available, only the order it saw them in.
Model-written context and questions
Section titled “Model-written context and questions”Under Settings → Enrichment a collection can ask a model to write, for every chunk, a 50–100-word note saying where the chunk sits in the document, and from one to five questions a reader would ask that the chunk answers. Both are added to what is embedded and to what keyword search sees, beside the chunk’s own text, which never changes. Either half can be turned on without the other, and both are off until you turn them on.
Turn it on when questions arrive phrased differently from the text that answers them, or when a chunk read on its own does not say which document, product or period it is about. Leave it off for a collection of short, self-describing records, where the chunk already carries its own context and the extra text is only cost.
Where the questions are indexed. Keyword search sees them whichever option you pick; Index questions as only decides what the vector side does with them. Part of the chunk’s embedding (the default) averages them into the one vector the chunk already had. Their own vectors stores each question separately, so a question can match a query on its own instead of being diluted by the rest of the chunk — at the price of one row per question and a shallower per-chunk search depth, since a collection asking for five questions has five rows competing for the same candidate budget. Both does each.
What it costs. One model call per chunk. The document goes in front of every one of those calls — or, for a document too long to fit, its outline plus the chunk’s own section — so a provider that caches prompt prefixes bills most of it once per document rather than once per chunk. The Enrichment panel prices the settings you are about to save against the collection as it stands, before you save them; it assumes no caching discount, so treat it as a ceiling rather than a forecast. It reports that it cannot price the work at all if the chosen model has no rates configured on the Models screen.
Each document’s History tab then shows what its last ingestion actually spent, and any warnings that ingestion raised — including a per-document token budget being exhausted. That budget is a stop, not a failure: chunks past it keep the deterministic header they would have had anyway, and the document still indexes. Re-chunking or re-indexing text that has not changed re-uses what was already written and spends nothing; re-parsing a document discards it, because the chunks are no longer the same text.
Measured comparison: context and questions
Section titled “Measured comparison: context and questions”Measured on 2026-09-28 against a real collection: nine documents (regulations, standards and national strategy papers in English and Macedonian), 4,166 chunks, and 129 golden questions, 111 answerable and 18 with no answer. Each configuration was re-indexed and scored through the same hybrid search and Cohere rerank-v3.5 reranker agents use. The enrichment model was GPT-6 Luna. Reranked is what an agent actually receives. Paraphrase is the 52 questions worded differently from the text that answers them, which is the case enrichment exists for.
| Configuration | Reranked hit@5 | MRR | NDCG | Recall | Paraphrase NDCG | Paraphrase recall |
|---|---|---|---|---|---|---|
| Off (baseline) | 99.1% | 0.977 | 0.899 | 0.895 | 0.879 | 0.876 |
| Context only | 99.1% | 0.966 | 0.896 | 0.898 | 0.875 | 0.875 |
| 3 questions, part of the embedding | 99.1% | 0.977 | 0.900 | 0.897 | 0.881 | 0.879 |
| 3 questions, their own vectors | 100.0% | 0.979 | 0.910 | 0.910 | 0.909 | 0.910 |
| Context + 3 questions, both | 99.1% | 0.973 | 0.904 | 0.905 | 0.894 | 0.896 |
What it cost. One model call per chunk writes the context and the questions together, so the price barely depends on which half you turn on. Here that came to $6.37 for all 4,166 chunks, against an estimate of $8.37 on the Enrichment panel. The run capped each call’s document prefix at 20,000 tokens: smaller documents went in whole, larger ones as their outline plus the chunk’s section. Re-indexing into the other four configurations re-used those results and cost nothing.
What the table says:
- Questions as their own vectors is the only clear gain: recall +1.5 points, and paraphrased questions +3 points of NDCG and recall. A question that shares no words with the text still finds the chunk, through a question written in the user’s terms.
- Context alone did not help here. The reranked result is unchanged. Without the reranker it made the vector search worse (NDCG 0.701 → 0.581), because the note dilutes the chunk’s own vector. These documents already have descriptive headings, which the chunk header carries for free. A collection whose chunks do not say what they are about may still benefit.
- The reranker absorbs most of the difference. Every configuration lands within about a point of the others after reranking. Without a reranker the gaps are larger in both directions, so measure on the pipeline your agents actually use.
- The golden questions were generated from the chunks’ own text, so they lean towards that wording. That favours an un-enriched index, and a set of real user questions may show a larger effect.
Recommendation. If you enable enrichment, start with 3 questions per chunk, indexed as their own vectors, with context off. Turn context on only if a golden set of your own shows it helps.
Whichever way you turn it on, measure it the way the next section describes — the effect depends on the collection, and enrichment is the one knob on this page with a per-chunk bill attached to guessing.
Parent-child chunking
Section titled “Parent-child chunking”Under Settings → Chunking, the hierarchical strategy matches on small
passages and returns their section (see Collections → Match small, return
the section). It is a trade: a section carries the context a passage
lacks, and costs the tokens a passage saved. The Return setting is
where that trade is made per collection, and the search preview can try
each policy on one query without saving anything — set Return
(hierarchical) and Merge threshold, and a matched result that merged
carries a Section badge, with the merges themselves listed in a
Merged sections panel below the results.
Measure it before leaving it on. kovac eval-retrieval-metrics scores the
three policies from one index — no re-index, no rebind:
kovac eval-retrieval-metrics handbook --top-k 5 --parent-return childkovac eval-retrieval-metrics handbook --top-k 5 --parent-return parentkovac eval-retrieval-metrics handbook --top-k 5 --parent-return autoBeside hit rate, MRR and NDCG each run now reports the mean tokens
returned per question, the mean number of results, and — for parent and
auto — how often a merge happened. A policy that lifts hit rate by
returning four times the text has not necessarily won.
What we measured. Nine documents and 111 golden questions, embedded with bge-m3 and reranked, top 5 results per question:
| Configuration | Hit rate | MRR | NDCG | Recall | Tokens returned |
|---|---|---|---|---|---|
| structure, 256 tokens | 94.6% | 0.904 | 0.840 | 0.858 | 1,649 |
hierarchical, child |
94.6% | 0.904 | 0.843 | 0.864 | 1,651 |
hierarchical, auto (merged 56% of the time) |
94.6% | 0.904 | 0.812 | 0.875 | 1,916 |
hierarchical, parent (merged every time) |
94.6% | 0.906 | 0.807 | 0.885 | 5,487 |
child behaves like structure, so switching strategy costs nothing on
its own. auto finds a little more of each answer for about 16% more
text, and parent finds the most but returns over three times the text
and ranks slightly worse. Start with auto when answers need the
surrounding section; stay on child when they don’t.
Changing a chunk strategy and your golden set. A strategy change moves
chunk boundaries, so a golden set’s ground truth has to be re-bound. Run
kovac golden-rebind once before the change, so every positive carries
its text; after the re-index, run it again and it binds each positive to
the chunk that now holds that text — by verbatim containment, no judge
involved. Only a positive whose text no longer appears anywhere in the
document goes to the judge.
Rerankers
Section titled “Rerankers”Every search’s final order comes from a reranker: a cross-encoder
that reads the query together with each candidate passage and returns a
relevance score. Hybrid fusion is what finds the candidates; the
reranker is what decides which of them actually answers the question,
what the score threshold measures, and what badges a citation weak
rather than supported.
A reranker is a row on the Models screen, not a fixed setting.
bge-reranker-v2-m3, served by TEI — the local reranker that ships with
the platform — is the seeded default, and needs nothing configured to
start scoring searches. An operator can register others (an OpenRouter
hosted model, or Cohere’s own API), make one of them the platform
default, or bind a specific collection to one; every search picks up the
change within a minute, with no restart.
One reranker scores a search. When an agent searches several collections at once, their candidates are merged into a single list and reranked in one call, so the whole search has one score scale — which is what keeps the grade and the confidence badges meaningful. When every collection an agent searches agrees on which reranker to use, that one scores the call. When they don’t — one collection is bound to the local TEI reranker, another follows a hosted default — the search falls back to scoring through the default reranker instead of splitting the work across two incomparable scales. The one thing worth knowing before binding different rerankers to collections an agent might search together: a collection’s own chosen reranker is not guaranteed to score every search it takes part in.
Adding one. From Models → Rerankers → Add reranker, pick a source:
- TEI — a self-managed endpoint; no key needed, just its URL.
- OpenRouter — discover its hosted rerank catalogue and add a model with an API key.
- Cohere — its own API; there’s no discovery for this source yet, so the model id is typed by hand.
Test connection before saving, on either a draft or a saved row: it scores three passages with an obvious order — clearly relevant, a near-miss, clearly irrelevant — and reports the latency and the score range, so you can see roughly where this model’s numbers live before you set anything against them.
Why a threshold does not transfer between models. Two rerankers
scoring the same passage rarely land on the same number — one model’s
0.6 can be another’s 0.3 for an identical passage. A threshold is
therefore only meaningful against the model that produced it, whether
that’s a reranker’s own calibrated grade threshold or a collection’s
Confidence bar (retry & citations) override. That’s why rebinding a
collection to a different reranker clears its own confidence-bar
override rather than carrying a number tuned for the old scale onto the
new one, and why a collection whose override was tuned for a reranker it
no longer uses shows a plain warning naming both, instead of quietly
applying a number that means nothing on the new scale.
Calibrating one. From a reranker’s row menu, Calibrate threshold,
pick a collection with an accepted golden set. It
scores every one of that collection’s accepted questions through this
reranker and proposes a band and a value — maximising the share of
unanswerable questions correctly declined, minus the share of answerable
questions that already work today and would be pushed below the line.
Review the band and the proposed value, then Accept to save it —
that’s the one action that marks the row calibrated. Editing the number
directly, or changing the model it points at, marks it uncalibrated
again even if you type the exact value Accept would have saved, so the
badge on the Rerankers tab always says whether a real run stands behind
today’s number. The same pipeline runs from the CLI for a scripted or
repeated pass: kovac eval-rerank-scores <collection> --reranker <alias> --out <file> collects the scores, and kovac eval-rerank-threshold <file> turns them into the same band and value.
How a collection picks one. Under a collection’s Settings, the Reranker field defaults to “Default (<alias>)” — whichever row currently carries the platform default — or names a specific one. Rebinding it clears the collection’s own confidence-bar override in the same save, unless that save also sets a new one.
Where the spend shows. A successful rerank call is metered like every other paid model call: Monitoring → Usage lists it under its own Reranking line. It’s priced from whatever the reranker itself reports back for that call when it reports one, else the row’s own price if you’ve set one, else left unpriced rather than shown as free. A failed rerank call — the reranker timing out or refusing the batch — is never billed, because nothing was billed.
Measured comparison: rerankers
Section titled “Measured comparison: rerankers”Measured on 2026-09-28 against a real collection — 129 golden questions, 111 answerable and 18 with no answer in the collection — on the platform’s own host, through the same search pipeline agents use (hybrid retrieval, then the reranker, then parent-child merging). Each reranker was scored at the depth it would actually be given:
| Reranker | Depth | hit@5 | MRR | NDCG | Recall | Rerank p50 / p95 | Per 1,000 searches | Calibrated threshold |
|---|---|---|---|---|---|---|---|---|
| Cohere rerank-v3.5 (OpenRouter) | 50 | 96.4% | 0.947 | 0.839 | 0.908 | 0.4 s / 1.2 s | $1.00 | 0.65 (band 0.64–0.67) |
| Cohere rerank-4-fast (OpenRouter) | 50 | 96.4% | 0.931 | 0.820 | 0.898 | 0.4 s / 0.5 s | $2.00 | 0.68 (band 0.68–0.69) |
| Cohere rerank-4-pro (OpenRouter) | 50 | 96.4% | 0.935 | 0.829 | 0.906 | 0.6 s / 0.7 s | $2.50 | 0.91 (band 0.91 only) |
| Qwen3 reranker 8B (OpenRouter) | 50 | 96.4% | 0.937 | 0.828 | 0.909 | 0.8 s / 1.6 s | $3.02 | not measurable² |
| bge-reranker-v2-m3 (local TEI, CPU) | 8 | 94.6% | 0.908 | 0.813 | 0.872 | 5.6 s / 7.3 s | $0 | 0.73 (band 0.72–0.74)¹ |
| No reranker (fusion order only) | — | 94.6% | 0.862 | 0.773 | 0.865 | — | — | — |
¹ TEI on CPU cannot go deeper than 8: at depth 16 its median rerank took 11.8 s, and 28 of 129 calls ran past the 15-second timeout and fell back to fusion order. The seeded TEI row keeps its original 0.6 threshold, which was calibrated on a different golden set; this measurement suggests 0.73 for this corpus, and Calibrate threshold is how to apply it. ² OpenRouter answered two of the 129 calls with 503, so the run carries two unscored questions, and calibration refuses a run with gaps. Its depth-8 run separates poorly: unanswerable questions score a median 0.88 on it, so a threshold cannot tell them apart from answered ones.
What the table says:
- Every hosted reranker beats the local one on quality, mostly because it can afford depth 50: hit@5 rises from 94.6% to 96.4%, and recall by 3–4 points. Cohere bills a flat price per call whatever the depth, so the deeper rerank costs nothing extra there.
- Ranking is close among the hosted models; the difference lies in the price and in how cleanly a model separates answerable questions from unanswerable ones. Rerank-4-fast separates them perfectly (every unanswerable question below its threshold, every answerable one above). Rerank-v3.5 would retry 4 answerable questions of 111 and let 1 unanswerable one through, at half the price.
- The local reranker is free and about 14× slower at the median. It is the right choice where documents must not leave the deployment, or where cost matters more than a few seconds per search.
Recommendation. Use Cohere rerank-v3.5 as the default: it ranks best, costs the least of the hosted options, and reranks in under half a second. Keep TEI registered for collections whose documents must stay local. Move to rerank-4-fast only if the confidence badges and retries need to be exact and the doubled price is acceptable.
Proving a change helped
Section titled “Proving a change helped”Changing a knob and asking an agent one question is a vibe check, not a measurement. The reliable way to tell whether a ranking change actually helped is a golden set: a set of real questions with known-correct chunks for the collection, scored automatically.
kovac eval-retrieval-metrics handbook --top-k 5reports hit rate, MRR, NDCG, precision and recall before you change a setting and after, so “this feels better” becomes a number you can compare. Evaluations → Retrieval quality shows the same history in the UI, with each run’s delta against the previous one.
One honest limit: recency has no signal on a collection whose documents carry no dates at all. If your golden set’s questions were generated from a corpus without a date field, running the metrics with recency on will show no change — that’s the measurement correctly reporting that there’s nothing here for it to measure, not evidence that recency doesn’t work. Judge recency changes against a collection that actually has the dates it needs, or by reading real answers rather than the golden-set numbers.
