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.

78
Source files
2
Sub-directories
Retrieval & Data
Category
Python
Language

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

DecisionChoice madePlausible alternativesInferred rationale
Retriever interfaceBaseRetriever is a Runnable[str, list[Document]]Custom protocolLets retrievers compose with LCEL: `retriever \prompt \llm`
Strategy as wrapperEach strategy holds a base_retrieverSubclass per-storeKeeps retrievers store-agnostic (works with any BaseRetriever)
Parent-document mechanismTwo splitters (parent + child) + a ByteStore for parents + a vectorstore for childrenOne vectorstore with chunk hierarchyDecouples storage of the retrieved unit from the searched unit; parent text never enters the embedding model
MultiQuery generationLLM rewrites the user's query into N variantsEmbedding-side query augmentationLLM rewrites capture intent (synonyms, expansions); embedding tricks don't generalise
Ensemble fusion algorithmReciprocal Rank Fusion (RRF)Borda count; weighted mean of similarityRRF is robust to scale differences across retrievers (BM25 vs embeddings); doesn't need probability calibration
Self-query languageLLM emits a structured StructuredQuery(query=str, filter=Comparison/Operation)Free-form filter strings; SQLA typed AST is parsable into vendor-specific filter syntax (Pinecone, Chroma, Weaviate)
Contextual compression pipelineList of BaseDocumentCompressors applied in orderSingle compressorPipeline composition: redundancy filter → embedding filter → LLM extractor
Time-weighted scoringsalience(doc) = sim(doc) + decay_rate^hours_since_accessHard cutoff windowSmooth 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.

  1. Ingestion: for each parent doc:
  1. Retrieval: query → vectorstore.similarity_search returns 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.

  1. Run all child retrievers in parallel (async).
  2. For each retriever, rank its returned docs.
  3. For each (retriever, doc) pair, accumulate 1/(k+rank) into doc's score (de-duplicated by content hash or doc ID).
  4. 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.

  1. LLM is shown:
  1. LLM emits structured query. Filter is a tree of Comparison(comparator, attribute, value) and Operation(operator, arguments).
  2. The retriever's structured_query_translator (vendor-specific: ChromaTranslator, PineconeTranslator, …) lowers the AST to native filter syntax.
  3. 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:

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

Evolution clues

Step-by-Step Walkthrough

Follow a typical operation through the retrievers/ module:

  1. Caller invokes the public surface — usually .invoke(input) on a Runnable, or a legacy __call__(inputs_dict) on a Chain subclass.
  2. 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.
  3. CallbackManager.configure — a fresh manager is built per call, fans on_*_start events to all subscribers (LangSmith tracer, stdout handler, custom logging), and stamps a UUID4 run_id for tree reconstruction.
  4. 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.
  5. 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.
  6. Streaming fan-out — if the provider streams tokens, each chunk fires on_llm_new_token through the callback manager so downstream observers (UI, log, tracer) see partial output.
  7. OutputParser pass — if a parser is attached, the raw provider output is parsed into a structured Python object; OutputParserException may trigger a retry chain (RetryWithErrorOutputParser).
  8. Memory write-back — for chains/agents that have a BaseMemory attached, save_context(inputs, outputs) persists the turn before returning to the caller.
  9. on_*_end fan-out — the callback manager closes the run span; tracers serialise the Run tree and ship it to LangSmith asynchronously.
  10. 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

Resource Usage

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