Maximal Marginal Relevance
The three most relevant chunks are sometimes three copies of the same idea. MMR picks the next chunk by how much it adds, not by how relevant it is in isolation.
Overview
Context first
Plain top-k retrieval picks the k highest-scoring documents independently. If a document collection has several near-identical passages about the same popular sub-topic, all of them can legitimately score near the top — and a naive top-k fills the context with repetition instead of coverage, wasting the retrieval budget on saying the same thing three times.
Selection
1.0 = pure relevance (same as MMR off). 0.0 = pure diversity, ignores relevance entirely.
query: "database indexing performance", top 3 of 6 selected
Selected, In Order
—Pairwise Similarity
Coverage
MMR: A Practical Guide
Trading a little relevance for a lot of coverage.
The formula
MMR = argmaxd ∈ remaining [ λ · relevance(d, query) − (1−λ) · maxs ∈ selected similarity(d, s) ]
Selection happens one document at a time, not all at once. The first pick is whatever is most relevant, exactly like ordinary retrieval. Every pick after that is penalised by how similar it is to whatever has already been chosen — so a document that would have ranked highly on relevance alone can lose to a less relevant document that covers new ground.
The problem with taking the top five
A vector search returns the five chunks closest to the query. On a corpus with any redundancy, those five are frequently near-duplicates of each other.
Ask "what is the refund policy?" of a corpus containing the policy in the handbook, in the FAQ, in a support macro and in two versions of the terms page, and the top five results are five statements of the same thing. Four slots wasted, and any nuance — the exceptions, the timescales, the process — is not retrieved at all.
Maximal marginal relevance fixes this by selecting results one at a time, each chosen to be relevant to the query and different from what has already been selected:
MMR = argmaxd [ λ · sim(d, q) − (1 − λ) · maxs ∈ selected sim(d, s) ]
The first term is relevance. The second is a penalty for similarity to anything already chosen. λ sets the balance.
The lambda dial
| λ | Behaviour |
|---|---|
| 1.0 | Pure relevance — identical to plain top-k |
| 0.7 | Mostly relevance, some diversity — the usual default |
| 0.5 | Balanced |
| 0.3 | Diversity-heavy; marginal results start appearing |
| 0.0 | Maximum diversity, relevance ignored |
0.5–0.7 is the range that works for most retrieval. Below about 0.3 the algorithm starts selecting documents chosen mainly for being unlike the others, which is not what you want.
The algorithm itself is greedy and short:
def mmr(query_vec, cand_vecs, lam=0.7, k=5):
sim_q = cand_vecs @ query_vec # relevance to the query
selected = [int(sim_q.argmax())]
while len(selected) < k:
best, best_score = None, -1e9
for i in range(len(cand_vecs)):
if i in selected:
continue
redundancy = max(cand_vecs[i] @ cand_vecs[j] for j in selected)
score = lam * sim_q[i] - (1 - lam) * redundancy
if score > best_score:
best, best_score = i, score
selected.append(best)
return selectedNote that it operates on a candidate set, not the whole corpus. Retrieve 30–50 by plain similarity first, then apply MMR to select the final 5. Running it over a million vectors would be quadratic and pointless.
A worked selection
Query: "refund policy". Suppose five candidates with these similarities to the query, and high mutual similarity among the first three:
| Doc | Relevance to query |
|---|---|
| A: Handbook refund section | 0.89 |
| B: FAQ refund entry | 0.87 |
| C: Terms page refunds | 0.86 |
| D: Refund processing timescales | 0.71 |
| E: Exceptions for digital goods | 0.68 |
Plain top-3 returns A, B, C — three statements of the same policy.
MMR with λ = 0.6 picks A first (highest relevance). For the second slot, B scores 0.6×0.87 − 0.4×0.93 = 0.15, while D scores 0.6×0.71 − 0.4×0.35 = 0.29. D wins despite being less relevant, because it adds information.
The result is A, D, E — the policy, the timescales and the exceptions. That is a far better context for answering the question, and it came from the same candidate set.
Running the selection, and watching lambda move it
The formula and the dial are described above. Here they are executed -- the greedy loop written out, every candidate's score at every step, and the point where turning lambda stops adding diversity and starts discarding relevance.
Guided experiments
- Read the pairwise similarity grid. Three of the six candidates are near-duplicates of each other — high similarity across that block — while the rest cover different ground.
- Leave MMR off. The top 3 by relevance alone are the query-planning chunk plus two of the three near-duplicate B-tree chunks — the hash-index sub-topic is crowded out entirely, covering only 2 of 3 sub-topics with two of three slots spent restating the same B-tree point.
- Turn MMR on at λ=0.5. The first pick does not change — it is still the most relevant document, since nothing has been selected yet to be similar to. The second and third picks do change: instead of a second B-tree chunk, MMR reaches for the hash-index one, and coverage goes from 2 of 3 sub-topics to all 3.
- Push λ to 1.0. The similarity penalty vanishes entirely and the result is identical to MMR being off — λ=1 is pure relevance by construction.
- Pull λ to 0.0. Relevance stops mattering at all; only spreading out matters, which can surface a barely-related document purely because nothing already picked resembles it.
What to remember
MMR selects documents one at a time, and after the first pick, every subsequent choice is weighed against how similar it is to what has already been selected — trading some relevance for coverage. λ controls the trade directly: 1.0 is ordinary top-k relevance ranking, 0.0 ignores relevance and maximises spread, and everything between balances the two. It costs one similarity comparison against the selected set per candidate per step, which is cheap next to embedding or reranking and is standard in any RAG system retrieving more than a couple of chunks per query.
Where diversity helps and where it hurts
It helps when the question has several facets. "What are the risks of this approach?" wants several distinct risks, not five phrasings of the biggest one.
It helps on redundant corpora. Documentation that repeats itself, support content duplicated across channels, versioned documents.
It helps summarisation and overview tasks, where coverage matters more than precision.
It hurts on narrow factual questions. "What is the notice period?" has one answer, and the best three chunks are probably all about it. Diversity here means introducing less relevant material.
It hurts when the corpus is already diverse. If there is no redundancy, the penalty term does nothing except distort the ordering slightly.
That split suggests treating λ as query-dependent rather than fixed: high for lookup questions, lower for exploratory ones. Some systems classify the query type and set it accordingly.
The alternatives
| Method | How it deduplicates |
|---|---|
| MMR | Penalises similarity to already-selected results |
| Exact deduplication | Hash the text, drop identical chunks |
| Near-duplicate filtering | Drop candidates above a similarity threshold to an earlier result |
| Clustering | Group candidates, take one per cluster |
| Parent deduplication | Collapse children of the same parent document |
Threshold filtering is the simpler cousin of MMR and often sufficient: go down the ranked list and skip anything with cosine similarity above 0.95 to something already kept. It handles the near-duplicate case without changing the ordering of genuinely distinct results.
Parent deduplication is worth doing regardless if you use parent-child retrieval, and it addresses a specific and common source of redundancy.
The pragmatic recommendation: start with threshold-based near-duplicate filtering, which is cheap and rarely harmful. Add MMR when the queries are genuinely multi-faceted.
Practical notes
It needs the candidate vectors, not just their ids — the redundancy term compares candidates with each other. Retrieval must return the embeddings, or they must be re-fetched.
Order matters after selection. MMR chooses a set; the order in which it is presented to the model still matters, because attention favours the ends of a long context. Keep the most relevant first.
It interacts with reranking. A cross-encoder gives more accurate relevance scores; running MMR on reranked candidates using cross-encoder scores for the relevance term and embedding similarity for the redundancy term works well.
Cost is negligible on 30–50 candidates — a few thousand dot products.
Questions people ask
What λ should I use? 0.5–0.7 as a default. Higher for factual lookup, lower for exploratory questions.
Is MMR better than simple deduplication? More principled, and more likely to change the result set. Threshold deduplication is simpler and adequate when the problem is exact repeats.
Does it improve answer quality? On multi-faceted questions and redundant corpora, measurably. On narrow factual questions it can reduce it slightly.
How many candidates should I pass it? 30–50. Too few and there is nothing diverse to choose; too many and the penalty term starts favouring outliers.
Can I use it with hybrid search? Yes — fuse the ranked lists first, then apply MMR to the fused candidates.
Does it help with the "lost in the middle" problem? Indirectly, by making every retrieved chunk carry distinct information, so fewer slots are wasted.
Recap in one screen
- Plain top-k on a redundant corpus returns several statements of the same thing.
- MMR selects greedily, scoring each candidate for relevance minus similarity to what is already selected.
λbalances the two; 0.5–0.7 suits most retrieval, higher for narrow factual questions.- Apply it to a candidate set of 30–50, not to the whole corpus.
- Simpler near-duplicate threshold filtering is often enough; MMR earns its place on multi-faceted queries.