windlass.providers.retrievers.contextual¶
contextual
¶
Contextual retrieval and query transformation.
Two techniques that attack retrieval failure from opposite ends:
Contextual chunk enrichment (index time). A chunk that reads "revenue grew 3%" is unfindable — grew from what, for whom, when? Before embedding, a small model writes one situating sentence using the surrounding document, and that sentence is prepended. Anthropic's published results put the retrieval-failure reduction at roughly 35-50%.
Query transformation (query time). HyDE has the model write a
hypothetical answer and embeds that instead of the question, because answers
live closer to answers in embedding space than questions do. Multi-query
expansion runs several rephrasings and fuses the results.
Both cost model calls. Enrichment pays once at ingestion; transformation pays on every query, so measure before enabling it in a latency-sensitive path.
Example
from windlass.providers.llm.fake import FakeLLM from windlass.providers.retrievers.bm25 import BM25Retriever from windlass.core.types import Chunk base = BM25Retriever() r = ContextualRetriever(retriever=base, llm=FakeLLM(responses=["Q3 revenue."])) r.index([Chunk(content="revenue grew 3%", metadata={"source": "10-K"})]) 1 "Q3 revenue." in base.chunks[next(iter(base.chunks))].content True
ContextualRetriever
¶
ContextualRetriever(
*,
retriever: Retriever | None = None,
llm: LLM | None = None,
enrich: bool = True,
transform: str = "none",
n_queries: int = 3,
context_window: int = 4000,
top_k: int = 5,
**config: Any
)
Bases: Retriever
Wraps another retriever with LLM-driven enrichment and query rewriting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
retriever
|
Retriever | None
|
The retriever doing the actual search. Required. |
None
|
llm
|
LLM | None
|
Model used for enrichment and rewriting. Required. Use a small, cheap model — this is a summarisation task, not reasoning. |
None
|
enrich
|
bool
|
Prepend a generated situating sentence to each chunk at index time. |
True
|
transform
|
str
|
Query-time strategy — |
'none'
|
n_queries
|
int
|
How many rewrites to generate for |
3
|
context_window
|
int
|
Characters of surrounding document shown to the model when enriching. Larger gives better context and costs more. |
4000
|
top_k
|
int
|
Default number of results. |
5
|
**config
|
Any
|
Forwarded to :class: |
{}
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
When |
ValueError
|
For an unknown |
Performance
Enrichment issues one model call per chunk, run with bounded concurrency. For a 10k-chunk corpus that is a real but one-off cost; the embedder's cache makes re-ingestion cheap.
Source code in src\windlass\providers\retrievers\contextual.py
native
¶
aindex
async
¶
Enrich chunks and hand them to the wrapped retriever.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chunks
|
Sequence[Chunk]
|
Chunks to index. |
required |
Returns:
| Type | Description |
|---|---|
int
|
How many chunks were indexed. |
Source code in src\windlass\providers\retrievers\contextual.py
aretrieve_chunks
async
¶
aretrieve_chunks(
query: str, k: int, *, filters: MetadataFilter | None = None, **kwargs: Any
) -> list[ScoredChunk]
Optionally rewrite the query, then delegate to the wrapped retriever.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The search query. |
required |
k
|
int
|
How many results to return. |
required |
filters
|
MetadataFilter | None
|
Metadata constraints. |
None
|
**kwargs
|
Any
|
Forwarded to the wrapped retriever. |
{}
|
Returns:
| Type | Description |
|---|---|
list[ScoredChunk]
|
Ranked hits. |