Modules/Gen AI/ MMR Lab

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

0.5

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

Sub-topics covered1 of 3

 

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.0Pure relevance — identical to plain top-k
0.7Mostly relevance, some diversity — the usual default
0.5Balanced
0.3Diversity-heavy; marginal results start appearing
0.0Maximum 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 selected

Note 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:

DocRelevance to query
A: Handbook refund section0.89
B: FAQ refund entry0.87
C: Terms page refunds0.86
D: Refund processing timescales0.71
E: Exceptions for digital goods0.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.

example_01.pyNumPy
Output

Guided experiments

  1. 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.
  2. 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.
  3. 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.
  4. Push λ to 1.0. The similarity penalty vanishes entirely and the result is identical to MMR being off — λ=1 is pure relevance by construction.
  5. 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

MethodHow it deduplicates
MMRPenalises similarity to already-selected results
Exact deduplicationHash the text, drop identical chunks
Near-duplicate filteringDrop candidates above a similarity threshold to an earlier result
ClusteringGroup candidates, take one per cluster
Parent deduplicationCollapse 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.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. Without scrolling back — what is the one-line takeaway from this module?

  2. What does this module say about “Context first”?

  3. What does this module say about “Selection”?

Cheat sheet

Maximal Marginal Relevance (MMR)

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.

GEN AI · vizlearn.in/gen_ai/maximal_marginal_relevance.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.