retrievers — MultiQuery, ContextualCompression, ParentDocument, EnsembleRetriever
MultiQuery, ContextualCompression, ParentDocument, EnsembleRetriever
Module langchain_classic/retrievers/ — MultiQuery, ContextualCompression, ParentDocument, EnsembleRetriever. Part of the Retrieval & Data group.
🎯 Why This Module Exists
This module is documented from the retrievers_deep.md Phase 1B research artifact. See sections below for design decisions, algorithms, error philosophy, performance, and evolution clues.
BaseRetriever Composable Multi-strategy
Architecture
flowchart LR
subgraph Core["langchain_classic/retrievers/"]
BASE["Base abstraction"]
IMPLS["Concrete implementations"]
BASE --> IMPLS
end
subgraph Up["Upstream callers"]
U1["chains/"]
U2["agents/"]
end
subgraph Down["Downstream"]
D1["langchain_core"]
D2["External providers"]
end
Up --> Core
Core --> Down
Data Flow
sequenceDiagram participant Caller as Caller participant Retrieve as retrievers participant Core as langchain_core participant Ext as External Caller->>Retrieve: invoke(input) Note over Retrieve: Validate & route Retrieve->>Core: delegate primitive Core->>Ext: provider call Ext-->>Core: response Core-->>Retrieve: typed output Retrieve-->>Caller: result
Existence rationale
retrievers/ is the home of strategies that wrap a vectorstore (or any document source) and produce relevant documents for a query. A bare vectorstore exposes only similarity_search; production RAG systems need re-ranking, query expansion, ensemble fusion, parent-context retrieval, time-decayed scoring, and structured-filter inference. Each of these is a strategy, not a vendor — they apply uniformly to Pinecone, Chroma, FAISS, etc. By separating the strategy layer from the storage layer, retrievers/ lets users compose techniques (multi-query → contextual compression → ensemble fusion) without touching the underlying store.
Design decisions visible in the code
| Decision | Choice made | Plausible alternatives | Inferred rationale | ||
|---|---|---|---|---|---|
| Retriever interface | BaseRetriever is a Runnable[str, list[Document]] | Custom protocol | Lets retrievers compose with LCEL: `retriever \ | prompt \ | llm` |
| Strategy as wrapper | Each strategy holds a base_retriever | Subclass per-store | Keeps retrievers store-agnostic (works with any BaseRetriever) | ||
| Parent-document mechanism | Two splitters (parent + child) + a ByteStore for parents + a vectorstore for children | One vectorstore with chunk hierarchy | Decouples storage of the retrieved unit from the searched unit; parent text never enters the embedding model | ||
| MultiQuery generation | LLM rewrites the user's query into N variants | Embedding-side query augmentation | LLM rewrites capture intent (synonyms, expansions); embedding tricks don't generalise | ||
| Ensemble fusion algorithm | Reciprocal Rank Fusion (RRF) | Borda count; weighted mean of similarity | RRF is robust to scale differences across retrievers (BM25 vs embeddings); doesn't need probability calibration | ||
| Self-query language | LLM emits a structured StructuredQuery(query=str, filter=Comparison/Operation) | Free-form filter strings; SQL | A typed AST is parsable into vendor-specific filter syntax (Pinecone, Chroma, Weaviate) | ||
| Contextual compression pipeline | List of BaseDocumentCompressors applied in order | Single compressor | Pipeline composition: redundancy filter → embedding filter → LLM extractor | ||
| Time-weighted scoring | salience(doc) = sim(doc) + decay_rate^hours_since_access | Hard cutoff window | Smooth recency boost preserves "old but very relevant" docs |
Algorithm deep-dives
1. ParentDocumentRetriever
Problem. Embedding a 5-page document loses local detail; embedding 200-token chunks loses surrounding context. We want to search small, return large.
Trace.
- Ingestion: for each parent doc:
- Optional
parent_splitter→ list of "parent chunks" (e.g., paragraph-sized). - Each parent chunk is split with
child_splitterinto "child chunks" (e.g., 200 tokens). - Child chunks get an embedding + parent ID metadata; stored in
vectorstore. - Parent chunks (text) stored in a
ByteStorekeyed by parent ID.
- Retrieval: query →
vectorstore.similarity_searchreturns matching child chunks → look up parent IDs → fetch parents from byte-store → de-dupe → return parents.
Complexity. Index: O(N_children) embeddings. Query: O(top_k) byte-store fetches. Top-k of children may yield <top_k unique parents.
Why this design. It separates what we search over (small, embedding-friendly chunks) from what we return (large, model-friendly chunks). The byte-store avoids re-storing the same parent text inside every child's metadata.
2. EnsembleRetriever / RRF
Problem. A vector retriever excels at semantic match; BM25 excels at exact lexical match. Neither is universally better. Combine them so the best of both ranks high.
RRF formula:
score(doc) = Σ_i 1 / (k + rank_i(doc))
where rank_i is doc's position in retriever i's ranked list (1-indexed) and k is a smoothing constant (default 60).
Trace.
- Run all child retrievers in parallel (async).
- For each retriever, rank its returned docs.
- For each (retriever, doc) pair, accumulate
1/(k+rank)into doc's score (de-duplicated by content hash or doc ID). - Sort by combined score descending; return top-k.
Why RRF. It avoids the calibration problem: vector cosine and BM25 scores aren't directly comparable. Rank-only fusion sidesteps this. The k=60 constant prevents the top-1 of each retriever from dominating.
3. SelfQueryRetriever
Problem. A user asks "comedies from the 90s with rating > 8". We have a vectorstore with genre, year, rating metadata. A pure embedding search won't filter on year > 1990 reliably.
Trace.
- LLM is shown:
metadata_field_info(each filterable field with name, type, description).document_contentsdescription.- The user query.
- Output schema:
{"query": str, "filter": Operation|Comparison|None}.
- LLM emits structured query. Filter is a tree of
Comparison(comparator, attribute, value)andOperation(operator, arguments). - The retriever's
structured_query_translator(vendor-specific:ChromaTranslator,PineconeTranslator, …) lowers the AST to native filter syntax. - Vectorstore is called with the rewritten query string + native filter.
Why an AST. Without it, LangChain would need an LLM-specific prompt for each vendor's filter dialect. The AST + per-vendor translator pattern keeps the LLM prompt vendor-agnostic.
4. ContextualCompressionRetriever
Problem. A retriever returns 10 docs with 800 tokens each — 8000 tokens of context. Half is irrelevant noise. Compress before passing to the answering LLM.
Pipeline. A list of BaseDocumentCompressors:
EmbeddingsRedundantFilter— drops docs whose embedding is >0.95 similar to a doc already kept (de-dup near-duplicates from chunking).EmbeddingsFilter— drops docs whose embedding similarity to query is below threshold (cheap cull).LLMChainExtractor— calls an LLM per doc to extract just the spans relevant to the query.LLMChainFilter— calls an LLM with yes/no per doc.CrossEncoderReranker— rescore with a cross-encoder model (slow but accurate).
Compressors are applied in order; a typical pipeline is EmbeddingsFilter → LLMChainExtractor (cheap cull then expensive extract).
Error philosophy
Best-effort, partial results. A failing child retriever in an ensemble doesn't kill the query — it's logged via callbacks and skipped. A failing self-query parse falls back to a non-filtered vector search. This makes retrievers robust enough to put behind a user-facing chat without circuit breakers.
Performance characteristics
- MultiQuery cost: 1 LLM call (rewrite) + N vectorstore queries (typically 3 variants) — query latency ≈ slowest variant.
- Ensemble cost: parallel; latency ≈ slowest child.
- Compression cost: can be substantial —
LLMChainExtractoris one LLM call per doc, so 10 docs = 10 calls. UseEmbeddingsFilterfirst to cull cheaply. - Time-weighted memory: maintains
last_accessed_atper doc; needs the underlying store to support metadata updates.
Evolution clues
BaseRetriever's API has evolved fromget_relevant_documents(query) -> list[Document]to the Runnable interface (invoke(query) -> list[Document]); both surfaces are kept for backward compat.MultiVectorRetrieverandParentDocumentRetrievershare the small-search-large-return idea; ParentDocument is the constrained-but-easier API.- Self-query translators live next to vectorstore vendor packages — the strategy/translator split is a canonical adapter pattern.
document_compressors/is a sibling sub-package because compressors compose withContextualCompressionRetriever; conceptually they're the same family.
Step-by-Step Walkthrough
Follow a typical operation through the retrievers/ module:
-
Caller invokes the public surface — usually
.invoke(input)on a Runnable, or a legacy__call__(inputs_dict)on aChainsubclass. - Pydantic validation — every entry point is a BaseModel; constructor args, input dicts, and tool args_schema are validated before any expensive work runs. Fail-fast on type errors.
-
CallbackManager.configure — a fresh manager is built per call, fans
on_*_startevents to all subscribers (LangSmith tracer, stdout handler, custom logging), and stamps a UUID4run_idfor tree reconstruction. - retrievers-specific logic — the module's core method runs: prompt rendering, retrieval, tool execution, memory load, document split, etc. See the deep-dive sections above.
- Provider call — typically a single HTTP request to OpenAI/Anthropic/etc., or a local SDK call. Retries are handled by tenacity with exponential backoff and a configurable max-attempts knob.
-
Streaming fan-out — if the provider streams tokens, each chunk fires
on_llm_new_tokenthrough the callback manager so downstream observers (UI, log, tracer) see partial output. -
OutputParser pass — if a parser is attached, the raw provider output is parsed into a structured Python object;
OutputParserExceptionmay trigger a retry chain (RetryWithErrorOutputParser). -
Memory write-back — for chains/agents that have a
BaseMemoryattached,save_context(inputs, outputs)persists the turn before returning to the caller. -
on_*_end fan-out — the callback manager closes the run span; tracers serialise the
Runtree and ship it to LangSmith asynchronously. - Result returned — caller receives a typed object (AIMessage, dict, BaseModel, list[Document], ...) matching the module's declared output schema.
What Could Go Wrong
| Failure Mode | Trigger | Detection | Recovery |
|---|---|---|---|
| Provider 5xx / rate-limit | OpenAI/Anthropic/etc. transient error | HTTP status from SDK | tenacity retries with exponential backoff up to max_retries; then surface to caller |
| Output parser failure | LLM returned malformed JSON / wrong schema |
OutputParserException in parser |
RetryWithErrorOutputParser re-prompts with the parser error included; or surface |
| Pydantic validation | Caller passes wrong-shaped dict |
ValidationError at __init__ / invoke
|
Surface immediately — no fallback is correct here |
| Callback handler exception | User-supplied tracer raises | Try/except around each fan-out | Logged and swallowed by default; raise_error=True to opt in to propagation |
| Provider auth missing |
OPENAI_API_KEY unset |
SDK AuthenticationError
|
Surface with a helpful message; no retry (would just fail again) |
Performance Profile
Hot Paths
- Provider HTTP round-trip dominates wall-clock time (100ms–30s depending on model and prompt length).
-
Pydantic validation is repeated on every
invoke; cached schema parsing keeps it under 1ms for typical inputs. - Callback fan-out is synchronous by default — a slow handler will slow every call. Async tracers (LangSmith) push work to a background thread to avoid blocking.
Resource Usage
-
Memory: token-history buffers (in
memory/) and document caches (in retrieval pipelines) are the largest steady-state allocations. - I/O: bounded by provider rate limits; vector store I/O for retrieval-flavored modules.
-
Concurrency: async support via
a*methods; agents and parallel chains useasyncio.gatherfor concurrent tool calls.
Directory Map
Source tree for retrievers
retrievers/
├── __init__.py
├── arcee.py
├── arxiv.py
├── azure_ai_search.py
├── bedrock.py
├── bm25.py
├── chaindesk.py
├── chatgpt_plugin_retriever.py
├── cohere_rag_retriever.py
├── contextual_compression.py
├── databerry.py
├── docarray.py
├── document_compressors/
├── elastic_search_bm25.py
├── embedchain.py
├── ensemble.py
├── google_cloud_documentai_warehouse.py
├── google_vertex_ai_search.py
├── kay.py
├── kendra.py
├── knn.py
├── llama_index.py
├── merger_retriever.py
├── metal.py
├── milvus.py
├── multi_query.py
├── multi_vector.py
├── outline.py
├── parent_document_retriever.py
├── pinecone_hybrid_search.py
├── pubmed.py
├── pupmed.py
├── re_phraser.py
├── remote_retriever.py
├── self_query/
├── svm.py
├── tavily_search_api.py
├── tfidf.py
├── time_weighted_retriever.py
├── vespa_retriever.py
├── weaviate_hybrid_search.py
├── web_research.py
├── wikipedia.py
├── you.py
├── zep.py
└── zilliz.py