Home / Reranking / Maximal Marginal Relevance (MMR): Diversifying Search Results
ExplainerMaximal Marginal Relevance (MMR): Diversifying Search Results
Maximal marginal relevance (MMR) is a reranking method that picks search results one at a time, rewarding relevance to the query and penalizing similarity to results already picked. Plain vector search ranks every result by its closeness to the query alone, so near-duplicates crowd the top. MMR fixes that by asking, at each step, which remaining candidate adds the most new information. A weight called lambda, or balance, sets the trade-off: 1 means pure relevance and 0 means pure diversity. Jaime Carbonell and Jade Goldstein introduced MMR at SIGIR in 1998 for retrieval and summarization. It is now a standard step in RAG pipelines, recommendations and product search. Weaviate supports it natively as diversity selection, and LangChain offers it as a search option. MMR works on a candidate list from a first search, so it adds little cost.
Maximal marginal relevance in four facts
- MMR scores each candidate as lambda times its relevance minus one minus lambda times its highest similarity to picked results.
- Carbonell and Goldstein introduced maximal marginal relevance at SIGIR in 1998.
- Weaviate added MMR diversity selection for vector search in version 1.37.3 and for hybrid search in 1.38.6.
- LangChain’s max_marginal_relevance_search defaults to k of 4, fetch_k of 20 and lambda_mult of 0.5.
What is maximal marginal relevance in search?
Maximal marginal relevance in search is a selection rule that builds a result list greedily, choosing each next result for its relevance and for how much it differs from the results already chosen. The “marginal” part means the value a result adds on top of what the list already has.
A search engine that ranks by relevance alone treats each result in isolation. If five chunks of the same manual say nearly the same thing, all five get nearly the same score and fill the top five places. A reader, or a language model in a RAG pipeline, then sees one idea five times and misses others.
MMR changes the question asked at each position. The first result is simply the most relevant one. For the second, the method asks which candidate is relevant and also unlike the first. For the third, it compares against both earlier picks, and so on. The result list covers more of the ground the query touches.
Weaviate’s documentation gives a concrete case: a query for “Italian food” in a product catalogue can return five pictures of pizza instead of a spread of Italian dishes. MMR is one kind of reranker, alongside the scoring models covered in what is reranking. Its behaviour comes from one short formula.
How does the MMR formula work?
The MMR formula works by giving each candidate a score of lambda times its similarity to the query, minus one minus lambda times its highest similarity to any result already selected. The candidate with the top score is added, and the scores are recomputed.
Written out, the next pick is the candidate D that maximizes λ × sim(D, Q) − (1 − λ) × max sim(D, S), where Q is the query and S is the set already selected. Carbonell and Goldstein defined it this way in their 1998 SIGIR paper, “The use of MMR, diversity-based reranking for reordering documents and producing summaries”. The two similarity functions can differ, but most systems use cosine similarity for both.
The first term rewards relevance. The second term punishes redundancy, and it uses the maximum similarity to any picked result, not the average. A candidate that nearly duplicates even one earlier pick is pushed down. With λ at 1 the penalty vanishes and MMR returns the plain relevance order. With λ at 0 relevance vanishes after the first pick, and the method only seeks variety.
The process is greedy. It never revisits earlier choices, which keeps it fast and simple. It also means the first pick, always the most relevant result, anchors everything after it. A worked example shows the steps with real arithmetic.
How does MMR reorder results in a worked example?
MMR reorders results in a worked example by demoting a near-duplicate of the top result and promoting a less similar but still relevant one. The numbers in this example are illustrative, chosen to show the arithmetic, and every score follows from the formula.
Four candidates have these relevance scores: A 0.90, B 0.88, D 0.85 and C 0.80. B is a near-copy of A, with a similarity of 0.95 between them. C is far from A at 0.30, and D sits in between at 0.60. Plain ranking returns A, B, D, C. With λ = 0.5, MMR first picks A, the most relevant.
In step two each candidate is scored against A. B gets 0.5 × 0.88 − 0.5 × 0.95, which is −0.035. D gets 0.5 × 0.85 − 0.5 × 0.60, which is 0.125. C gets 0.5 × 0.80 − 0.5 × 0.30, which is 0.25, so C is picked.
In step three the penalty uses the higher of each candidate’s similarities to A and C. D’s similarity to C is 0.20, so its penalty stays at 0.60 and its score stays at 0.125. B’s similarity to C is 0.35, so its score stays at −0.035. D is picked, and the final order is A, C, D, B. The near-duplicate fell from second to last, while every result remains relevant. The size of that shift depends on λ.
How do you choose the lambda value for MMR?
You choose the lambda value for MMR by starting at 0.5 and moving it towards 1 when results drift off topic, or towards 0 when results still repeat. The right value depends on how redundant your collection is.
Weaviate and LangChain both treat 0.5 as the balanced setting. Weaviate calls the weight balance and documents three reference points: 0.0 for pure diversity, 0.5 for balanced and 1.0 for pure relevance, which is equivalent to standard search. LangChain calls the same weight lambda_mult and uses 0.5 as its default.
A collection with many near-duplicates, such as chunked manuals or product variants, tolerates a lower λ because there is plenty of redundancy to remove. A small, varied collection needs a higher λ, or MMR will pull in weakly related results to be different. Low values are risky in RAG. A diverse but irrelevant chunk wastes context space as surely as a duplicate.
Test λ on real queries. Compare result lists at 0.3, 0.5 and 0.7, and check two things: whether duplicates remain, and whether off-topic results appear. Measuring diversity alongside relevance is covered in information retrieval metrics. λ is half the setup; the other half is how many candidates MMR can choose from.
How many candidates should MMR rerank?
MMR should rerank a candidate list several times larger than the number of results you return, so it has varied options to choose from. Weaviate’s documentation suggests a candidate limit of 2 to 4 times the MMR limit as a starting point.
MMR can only choose among the candidates the first search returns. If that search fetches exactly five results and MMR must return five, nothing changes. LangChain’s defaults reflect this: fetch_k of 20 candidates for k of 4 results. Weaviate’s examples retrieve 20 candidates and return 5.
A larger pool gives more variety but costs more. Each step compares every remaining candidate with every picked result, so the work grows with pool size times result count. That is small next to a cross-encoder pass, because MMR only needs vector similarities, not model calls. A very large pool also lets in weaker results that MMR may promote for being different.
Pagination needs care. Weaviate’s documentation warns that with diversity selection, offset moves the candidate window, so it must advance by the candidate limit rather than the page size. Advancing by the page size makes windows overlap, repeating some results and skipping others. With the pool sized, MMR can run inside the database or in application code.
How do you use MMR in Weaviate and LangChain?
You use MMR in Weaviate by adding a diversity_selection parameter to a vector or hybrid query, and in LangChain by calling max_marginal_relevance_search on a vector store. Both rerank a larger candidate set on the fly, at query time.
from weaviate.classes.query import Diversity
collection = client.collections.use("Article")
response = collection.query.near_vector(
near_vector=query_vector,
limit=20, # candidate set
diversity_selection=Diversity.mmr(
limit=5, # results returned
balance=0.5, # 1.0 = pure relevance, 0.0 = pure diversity
),
)
for o in response.objects:
print(o.properties["title"])
Weaviate added diversity selection for vector search in version 1.37.3 and for hybrid search in 1.38.6, according to its search documentation. It works with near_text, near_vector, near_object, near_image and near_media, and needs no schema change or reindexing. Multi-vector collections are not supported. Results come back in MMR order, not by query similarity. The snippet follows the documented API and needs a server and client recent enough to support it.
In LangChain, any vector store that implements the method offers the same idea with one call.
docs = store.max_marginal_relevance_search(query, k=4, fetch_k=20, lambda_mult=0.5)
The values shown are LangChain’s defaults. The vector store integrations are covered in semantic search with LangChain and LlamaIndex. Where neither is available, MMR is short enough to write by hand.
How do you implement MMR in Python?
You implement MMR in Python with a loop that keeps a selected list and, at each step, picks the candidate with the highest MMR score. With normalized vectors, dot products are cosine similarities, so NumPy does all the work.
import numpy as np
def mmr(query_vec, doc_vecs, k=5, balance=0.5):
"""Pick k document indices by maximal marginal relevance.
Vectors must be normalized, so dot products are cosine similarities."""
rel = doc_vecs @ query_vec
sim = doc_vecs @ doc_vecs.T
selected = [int(np.argmax(rel))]
candidates = set(range(len(doc_vecs))) - set(selected)
while candidates and len(selected) < k:
best = max(candidates,
key=lambda i: balance * rel[i] - (1 - balance) * sim[i, selected].max())
selected.append(best)
candidates.remove(best)
return selected
The function takes the candidates’ vectors, usually the top 20 or so from a vector search, and returns indices in MMR order. The first pick is the most relevant candidate. Each later pick applies the formula with the maximum similarity to the picks so far. Run on the similarity values from the worked example, the same loop returns A, C, D, B.
The full similarity matrix is fine for a few dozen candidates. For hundreds, compute similarities only against the selected items as they are added. The vectors must come from the same model that produced the query embedding, as explained in cosine vs dot product vs Euclidean distance. The same function slots into several places in a pipeline.
Where does MMR fit with cross-encoders and RAG?
MMR fits after retrieval and usually after a relevance reranker, as the last step that decides which passages reach the user or the language model. A cross-encoder decides what is relevant; MMR decides what is redundant.
In RAG, context space is limited, and duplicate chunks waste it. Five near-identical passages about one refund rule leave no room for the passage about exceptions. MMR spreads the context across distinct points, which helps questions that need more than one fact. The broader setup is covered in reranking in RAG.
A cross-encoder reads the query and each passage together and scores relevance more accurately than vector similarity, as explained in cross-encoder rerankers. Running it first gives MMR better relevance scores to work with. LLM-based rerankers can also judge redundancy directly, at much higher cost, as described in LLM reranking.
MMR adds little delay because it needs no model calls, only similarities between vectors already retrieved. The cost of the model-based steps is covered in reranking latency and cost, and model choices in best reranking models. Recommendations use the same idea to avoid showing ten versions of one item, as in semantic recommendations.
Maximal marginal relevance builds a result list one pick at a time, scoring each candidate as λ times its relevance minus 1 − λ times its highest similarity to results already picked. That rule keeps the most relevant result first and pushes near-duplicates down, so the list covers more distinct points. Start with λ at 0.5 and a candidate pool 2 to 4 times the results you return, then tune on real queries. Use Weaviate’s diversity selection or LangChain’s MMR search, or a short NumPy loop, and place MMR after relevance reranking when passages feed a language model.