windlass.providers.retrievers.bm25¶
bm25
¶
BM25 lexical retrieval.
Okapi BM25, implemented from scratch over an inverted index — no rank_bm25,
no dependencies. It earns its place next to vector search because the two fail
in opposite ways: embeddings miss exact identifiers (error codes, product SKUs,
function names) that BM25 nails, and BM25 misses paraphrases that embeddings
handle. Hybrid retrieval combines both.
The scoring function is the standard one::
score(q, d) = Σ IDF(t) · f(t,d)·(k1+1) / (f(t,d) + k1·(1 - b + b·|d|/avgdl))
t∈q
Example
from windlass.core.types import Chunk r = BM25Retriever() r.index([ ... Chunk(content="error E1042 occurs when the socket closes"), ... Chunk(content="the network layer handles reconnection"), ... ]) 2 r.retrieve("E1042").hits[0].chunk.content.startswith("error E1042") True
BM25Retriever
¶
BM25Retriever(
*,
top_k: int = 5,
k1: float = 1.5,
b: float = 0.75,
remove_stopwords: bool = True,
min_token_length: int = 1,
vectorstore: VectorStore | None = None,
**config: Any
)
Bases: Retriever
Sparse lexical retriever.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
top_k
|
int
|
Default number of results. |
5
|
k1
|
float
|
Term-frequency saturation. Higher values let repeated terms keep
adding score; |
1.5
|
b
|
float
|
Length normalisation, in |
0.75
|
remove_stopwords
|
bool
|
Drop :data: |
True
|
min_token_length
|
int
|
Ignore tokens shorter than this. |
1
|
vectorstore
|
VectorStore | None
|
Optional store to seed the index from at construction, so a hybrid retriever built on an existing collection works immediately. |
None
|
**config
|
Any
|
Forwarded to :class: |
{}
|
Attributes:
| Name | Type | Description |
|---|---|---|
chunks |
dict[str, Chunk]
|
Indexed chunks, keyed by id. |
Performance
Indexing is O(total tokens). Query cost is proportional to the
number of documents containing the query terms, not the corpus size,
which makes it fast even on large collections. Memory is roughly
proportional to the vocabulary.
Thread safety
Indexing and search are guarded by a re-entrant lock.
Source code in src\windlass\providers\retrievers\bm25.py
aindex
async
¶
Add chunks to the inverted index.
Re-indexing a chunk id replaces its previous entry, so ingestion is idempotent.
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\bm25.py
remove
¶
Remove chunks from the index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chunk_ids
|
Sequence[str]
|
Ids to drop. |
required |
Returns:
| Type | Description |
|---|---|
int
|
How many were actually removed. |
Source code in src\windlass\providers\retrievers\bm25.py
clear
¶
aretrieve_chunks
async
¶
aretrieve_chunks(
query: str, k: int, *, filters: MetadataFilter | None = None, **kwargs: Any
) -> list[ScoredChunk]
Score the corpus against query using BM25.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The search query. |
required |
k
|
int
|
How many results to return. |
required |
filters
|
MetadataFilter | None
|
Metadata constraints, applied to candidates before ranking. |
None
|
**kwargs
|
Any
|
Ignored. |
{}
|
Returns:
| Type | Description |
|---|---|
list[ScoredChunk]
|
Scored chunks, best first. Chunks matching no query term are omitted |
list[ScoredChunk]
|
entirely rather than returned with a zero score. |
Source code in src\windlass\providers\retrievers\bm25.py
stats
¶
Return index statistics.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A dict with |
dict[str, Any]
|
|