LangChain VectorStoreRetriever — score threshold, MMR, and filter failures (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain Retriever failures
LangChain Retrieval · VectorStore Severity: Medium HTTP n/a

LangChain VectorStoreRetriever — empty results, wrong docs, filters ignored

<code>VectorStoreRetriever</code> wraps any vector store into a LangChain retriever. Most retrieval failures are configuration issues (bad thresholds, wrong search type, filters not pushed down to the store) rather than model or embedding problems.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: Retriever failures usually come from misconfigured search_kwargs: k too small, score_threshold too tight, filters written in a format the underlying store does not recognize, or MMR parameters that over-diversify. Fix by (a) starting with plain similarity + k=10 and measuring hit rate, (b) using store-native filter formats via the retriever's search_kwargs["filter"], and (c) tuning MMR only after similarity is proven working.

Real error messages you'll see

These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

Retriever returns empty list
docs = retriever.invoke("some query")
print(docs)
# []
# search_kwargs={"score_threshold": 0.9, "k": 5} — threshold too tight
Filter silently ignored
retriever = vs.as_retriever(search_kwargs={"filter": {"category": "policy"}})
docs = retriever.invoke("...")
# All docs returned regardless of category — filter format wrong for this store
MMR returns unrelated docs
retriever = vs.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 5, "fetch_k": 50, "lambda_mult": 0.1}
)
# lambda_mult=0.1 heavily favours diversity — returns off-topic docs

Reference

search_type options and when to use each

<code>search_type</code>When to use
"similarity"Default — cosine or L2 similarity, top-k
"similarity_score_threshold"When you want to filter out low-similarity hits
"mmr"When results are too similar to each other (diversity)

Filter format by vector store backend

BackendFilter format
PineconeMongoDB-style dict: {"category": {"$eq": "policy"}}
ChromaMongoDB-style: {"category": "policy"}
WeaviateGraphQL-style operator dict
QdrantNative Qdrant filter dict with must/should
Milvus / ZillizBoolean expression string: "category == 'policy'"
pgvectorSQL WHERE clause or dict depending on wrapper
FAISSNo native filter — filter post-retrieval in Python
ElasticSearchElasticsearch query DSL

Root causes, ranked by frequency

Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.

  • 26%
    score_threshold too tight. 0.9+ is aggressive; most legitimate hits score 0.5-0.8 on cosine.
  • 18%
    Filter format not native to the store. Passing a Chroma-style filter to Pinecone or vice versa.
  • 14%
    k too small for downstream reranker. k=3 sent to a reranker leaves nothing to work with.
  • 10%
    Embedding mismatch. Indexed with one model, queried with another; scores meaningless.
  • 8%
    MMR lambda too low. lambda_mult below 0.3 prioritises diversity over relevance.
  • 7%
    Filter field not indexed. Some stores need explicit metadata field indexing for filter push-down.
  • 7%
    Async retriever wrapping a sync store. Async invoke works but blocks the event loop.
  • 10%
    Wrong distance metric. Cosine similarity vs L2 distance behave differently; store default may not match the embedding model's space.

Fixes — copy-paste solutions

Fix #1

Start with plain similarity + generous k, then measure

Do not add thresholds or MMR until you have a baseline.

Configure the retriever with the simplest possible search_kwargs. Measure hit rate on a labeled test set. Only introduce advanced features once you know they help.

baseline_retriever.py
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vs = Chroma(
    collection_name="my_docs",
    embedding_function=embeddings,
    persist_directory="./chroma_db",
)

# Baseline: plain similarity, k=10
retriever = vs.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 10},
)

# Measure with a labeled test set
test_queries = [
    ("What is the refund policy?", ["doc_refund_policy_v3", "doc_terms_of_service"]),
    ("How do I reset my password?", ["doc_password_reset_kb", "doc_account_faq"]),
    # ...
]

hits = 0
misses = 0
for query, expected_ids in test_queries:
    docs = retriever.invoke(query)
    retrieved_ids = {d.metadata.get("id") for d in docs}
    if any(exp in retrieved_ids for exp in expected_ids):
        hits += 1
    else:
        misses += 1
        print(f"MISS: {query!r}")
        print(f"  expected any of: {expected_ids}")
        print(f"  retrieved: {list(retrieved_ids)[:5]}")

print(f"Hit rate: {hits}/{hits+misses} = {hits/(hits+misses):.1%}")

# Then experiment with variations and see which lifts hit rate
A labeled test set of 30-100 (query, expected_docs) pairs is the single highest-leverage RAG investment. Every retrieval config knob can be tuned against it.
Fix #2

Push filters down to the store in the store's native format

Filter format is the biggest cross-store gotcha.

The filter in search_kwargs is passed as-is to the underlying vector store. Use the store's native filter format, not a LangChain-generic one.

filter_by_store.py
# --- Pinecone: MongoDB-style operators ---
from langchain_pinecone import PineconeVectorStore
retriever = PineconeVectorStore(index_name="...").as_retriever(
    search_kwargs={
        "k": 10,
        "filter": {"category": {"$eq": "policy"}, "date": {"$gte": "2025-01-01"}},
    }
)

# --- Chroma: simple dict (LangChain wrapper translates) ---
from langchain_community.vectorstores import Chroma
retriever = Chroma(...).as_retriever(
    search_kwargs={
        "k": 10,
        "filter": {"category": "policy"},   # equality
    }
)

# --- Qdrant: native filter format ---
from langchain_qdrant import QdrantVectorStore
from qdrant_client.models import Filter, FieldCondition, MatchValue
retriever = QdrantVectorStore(...).as_retriever(
    search_kwargs={
        "k": 10,
        "filter": Filter(must=[
            FieldCondition(key="category", match=MatchValue(value="policy"))
        ]),
    }
)

# --- Milvus / Zilliz: boolean expression string ---
from langchain_milvus import Milvus
retriever = Milvus(...).as_retriever(
    search_kwargs={
        "k": 10,
        "expr": "category == 'policy' and date >= '2025-01-01'",
    }
)

# --- FAISS: no native filter — post-filter in Python ---
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document

def faiss_with_filter(retriever, query: str, filter_fn) -> list[Document]:
    docs = retriever.invoke(query)
    return [d for d in docs if filter_fn(d)]

filtered = faiss_with_filter(
    faiss_retriever,
    "policy question",
    lambda d: d.metadata.get("category") == "policy",
)
When migrating between vector stores, filters are usually the biggest rewrite. Abstract them behind a helper function per store so switching backends is one file change.
Fix #3

Tune MMR for diversity only after similarity is working

MMR helps when your corpus has clusters of near-duplicates.

Maximum Marginal Relevance re-ranks the top fetch_k candidates to balance relevance and diversity. lambda_mult = 1 gives pure similarity; 0 gives pure diversity. Sensible starting point: 0.5-0.7.

mmr_tuning.py
# When to reach for MMR:
#   - Corpus has many near-duplicate documents
#   - Same passage indexed multiple times (different pages, versions)
#   - Similar-worded but distinct concepts

retriever_mmr = vs.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 5,                # final number of results
        "fetch_k": 30,         # candidate pool BEFORE re-ranking (must be > k)
        "lambda_mult": 0.6,    # 0.5-0.7 is a sensible starting range
                              # closer to 1 = more relevance
                              # closer to 0 = more diversity
    }
)

# For A/B testing MMR vs plain similarity on your test set:
def compare_retrievers(similarity_retriever, mmr_retriever, test_queries):
    sim_hit_rate = evaluate(similarity_retriever, test_queries)
    mmr_hit_rate = evaluate(mmr_retriever, test_queries)
    print(f"similarity hit rate: {sim_hit_rate:.1%}")
    print(f"mmr hit rate:        {mmr_hit_rate:.1%}")

# Common finding: MMR HURTS hit rate but IMPROVES answer quality (diverse context
# reduces one-sided answers). Measure end-to-end answer quality, not just retrieval.

# For score-threshold retrieval, tune with real data — do not guess:
retriever_thresh = vs.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 10, "score_threshold": 0.4},
    # Start LOW (0.3-0.4) and raise only if you see too much irrelevant context.
)
MMR does not always improve retrieval — sometimes it hurts. Measure on your test set before making it the default. It is more valuable for corpora with lots of near-duplicates than for clean, deduplicated data.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Build a labeled test set (30-100 query-doc pairs) before tuning any retriever config.
  • Start with search_type="similarity" and k=10; add complexity only when measured to help.
  • Match filter format to the vector store — never assume a generic dict works.
  • Use the SAME embedding model for indexing and querying. Version pin.
  • For score-threshold retrieval, start with threshold 0.3-0.4 and raise carefully.
  • MMR fetch_k should be at least 3× k; lambda_mult in 0.5-0.7.
  • Log retriever hit rate as a production SLI — regressions surface fast.

Frequently asked questions

Your threshold is too tight. Cosine similarity scores for legitimate hits typically sit in the 0.5-0.8 range for text-embedding-3-small; 0.7-0.9 for larger models. Start at 0.3-0.4 and raise until you see false positives.
Yes for anything mission-critical. A cross-encoder reranker (Cohere Rerank, BGE Rerank) applied to top 20-50 retrieved docs and cut to top 5 typically lifts hit rate 10-30%. Cost is small compared to the LLM inference.
For domain-specific corpora with exact-match terms (product codes, names, jargon), yes — hybrid catches what vector misses. For general text where semantic similarity is enough, vector alone is fine. LangChain's EnsembleRetriever combines both.
Yes — pass a filter in search_kwargs["filter"] in the store's native format. Some stores need the metadata field indexed at ingest time for efficient filtering. Check your store's docs.
Depends on corpus and embedding model. Sensible starting point: 500-1000 tokens with 100-200 overlap. See our document splitter page for details on tuning.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.