Retrieval¶
Vector stores need the retrieval extra. Stores hold vectors and metadata, not
documents: keep each chunk's text in the metadata under "text".
Vector stores¶
openbtk.retrieval.faiss.FAISSVectorStore ¶
FAISSVectorStore(*, dimension: int, metric: Metric = 'l2')
Bases: BaseVectorStore
Store and query vectors with a local FAISS index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dimension
|
int
|
Vector width -- fixed for the life of the index, like every FAISS index. |
required |
metric
|
Metric
|
|
'l2'
|
No I/O happens in __init__ (docs/09_CODING_STANDARDS.md rule 11):
the faiss import (and the index it builds) is deferred to first
real use, so constructing this class -- e.g. while validating a
pipeline config -- never requires faiss-cpu to be installed
unless a step actually runs.
Source code in src/openbtk/retrieval/faiss.py
66 67 68 69 70 71 72 73 | |
openbtk.retrieval.chroma.ChromaVectorStore ¶
ChromaVectorStore(*, path: str | None = None, collection_name: str = _DEFAULT_COLLECTION)
Bases: BaseVectorStore
Store and query vectors with an embedded Chroma collection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Directory for on-disk persistence via
|
None
|
collection_name
|
str
|
Chroma requires 3-512 characters from
|
_DEFAULT_COLLECTION
|
No I/O happens in __init__ (docs/09_CODING_STANDARDS.md rule 11):
the chromadb import and the client/collection it builds are
deferred to first real use.
Source code in src/openbtk/retrieval/chroma.py
64 65 66 67 68 69 | |
openbtk.retrieval.qdrant.QdrantVectorStore ¶
QdrantVectorStore(*, dimension: int, metric: Metric = 'cosine', path: str | None = None, collection_name: str = _DEFAULT_COLLECTION)
Bases: BaseVectorStore
Store and query vectors with an embedded Qdrant collection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dimension
|
int
|
Vector width, required up front by Qdrant's own
|
required |
metric
|
Metric
|
|
'cosine'
|
path
|
str | None
|
Directory for on-disk persistence. |
None
|
collection_name
|
str
|
The Qdrant collection to use. |
_DEFAULT_COLLECTION
|
No I/O happens in __init__ (docs/09_CODING_STANDARDS.md rule 11):
the qdrant_client import and the client/collection it builds are
deferred to first real use.
Source code in src/openbtk/retrieval/qdrant.py
92 93 94 95 96 97 98 99 100 101 102 103 104 | |
Reranking¶
openbtk.retrieval.reranker.ConceptOverlapReranker ¶
ConceptOverlapReranker(*, extract_concepts: Callable[[str], Iterable[str]], cuis_metadata_key: str = _DEFAULT_CUIS_KEY)
Bases: BaseReranker
Rerank results by the count of UMLS CUIs they share with the query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
extract_concepts
|
Callable[[str], Iterable[str]]
|
Maps a text string to the CUIs it mentions.
Called once, on the query text, per :meth: |
required |
cuis_metadata_key
|
str
|
The |
_DEFAULT_CUIS_KEY
|
Sorts by (overlap_count, original_score), both descending: shared
concepts are the primary criterion (the entire point of this
reranker), the original retrieval score the tiebreaker among results
that share the same number of concepts with the query -- including
"none at all", so a result set with no concept metadata populated
anywhere degrades gracefully to the original score order rather than
an arbitrary one.
No I/O happens in __init__ (docs/09_CODING_STANDARDS.md rule 11):
extract_concepts is only ever called from :meth:rerank.
Source code in src/openbtk/retrieval/reranker.py
72 73 74 75 76 77 78 79 | |
openbtk.retrieval.cross_encoder.CrossEncoderReranker ¶
CrossEncoderReranker(*, model: str = DEFAULT_MODEL, revision: str | None = None, text_metadata_key: str = _DEFAULT_TEXT_KEY, batch_size: int = 16, max_length: int = 512, device: str = 'cpu')
Bases: BaseReranker
Rerank search results with a local cross-encoder model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
A Hugging Face Hub model id (or local path) of a sequence-classification model with a single relevance logit. Defaults to MedCPT's cross-encoder. |
DEFAULT_MODEL
|
revision
|
str | None
|
The pinned commit of |
None
|
text_metadata_key
|
str
|
The |
_DEFAULT_TEXT_KEY
|
batch_size
|
int
|
Query-chunk pairs scored per model pass. |
16
|
max_length
|
int
|
Tokens kept per pair (longer pairs are truncated). |
512
|
device
|
str
|
A torch device string, |
'cpu'
|
Example
reranker = CrossEncoderReranker() # nothing is loaded yet reranker.provenance().model_identity.name 'ncbi/MedCPT-Cross-Encoder'
Source code in src/openbtk/retrieval/cross_encoder.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
Hybrid (dense + BM25)¶
See the retrieval guide for how the pieces fit and what BM25 will and will not find.
openbtk.retrieval.hybrid.BM25Index ¶
BM25Index(*, k1: float = 1.5, b: float = 0.75, tokenizer: Callable[[str], list[str]] = default_tokenizer, text_metadata_key: str = 'text')
An in-memory Okapi BM25 index. See the module docstring for its limits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k1
|
float
|
Term-frequency saturation. Higher rewards repeated terms for longer. |
1.5
|
b
|
float
|
Length normalisation, |
0.75
|
tokenizer
|
Callable[[str], list[str]]
|
Splits text into terms; used for documents and queries alike. |
default_tokenizer
|
text_metadata_key
|
str
|
Where :meth: |
'text'
|
Example
index = BM25Index() index.upsert( ... ["a", "b", "c"], ... ["metformin for diabetes", "aspirin for pain", "insulin for diabetes"], ... ) ids = [hit.id for hit in index.search("metformin")] ids ['a']
Source code in src/openbtk/retrieval/hybrid.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
delete ¶
delete(ids: Sequence[str]) -> None
Remove chunks by id. An id that is not indexed is ignored.
Source code in src/openbtk/retrieval/hybrid.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
search ¶
search(query: str, top_k: int = 5) -> list[SearchResult]
The top_k chunks that best match query, best first.
Only chunks sharing at least one term with the query are returned, so the result
can be shorter than top_k (or empty). Ties break on id, so the order is
deterministic.
Source code in src/openbtk/retrieval/hybrid.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
upsert ¶
upsert(ids: Sequence[str], texts: Sequence[str], metadata: Sequence[dict[str, JsonValue]] | None = None) -> None
Index texts under ids, replacing any chunk that already has an id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ids
|
Sequence[str]
|
One unique id per text (the ids you give the vector store). |
required |
texts
|
Sequence[str]
|
The chunk texts. |
required |
metadata
|
Sequence[dict[str, JsonValue]] | None
|
Optional metadata per chunk, returned with each hit. The text
itself is added under |
None
|
Raises:
| Type | Description |
|---|---|
RetrievalError
|
If the lists differ in length or |
Source code in src/openbtk/retrieval/hybrid.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
openbtk.retrieval.hybrid.reciprocal_rank_fusion ¶
reciprocal_rank_fusion(rankings: Sequence[Sequence[SearchResult]], *, k: int = 60, weights: Sequence[float] | None = None, top_k: int | None = None) -> list[SearchResult]
Merge several rankings into one by reciprocal rank (Cormack et al., 2009).
Each ranking gives an id weight / (k + rank) (rank 1 is the best), the scores
are summed per id, and ids are sorted by that sum. Only the order of each input
matters, so rankings on different scales (cosine similarity, BM25) combine
sensibly. A larger k flattens the advantage of a top rank.
The returned results carry the fused score and the metadata of the first ranking that contained the id, so put the ranking whose metadata you prefer first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rankings
|
Sequence[Sequence[SearchResult]]
|
Best-first lists of results. |
required |
k
|
int
|
The smoothing constant; |
60
|
weights
|
Sequence[float] | None
|
One weight per ranking (default all |
None
|
top_k
|
int | None
|
Keep at most this many. |
None
|
Example
dense = [SearchResult(id="a", score=0.9), SearchResult(id="b", score=0.8)] sparse = [SearchResult(id="b", score=12.0), SearchResult(id="c", score=3.0)] ids = [hit.id for hit in reciprocal_rank_fusion([dense, sparse])] ids ['b', 'a', 'c']
Source code in src/openbtk/retrieval/hybrid.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | |
openbtk.retrieval.hybrid.default_tokenizer ¶
default_tokenizer(text: str) -> list[str]
Lowercase alphanumeric runs ("58410-2" -> ["58410", "2"]).
Example
default_tokenizer("Metformin 500 mg, BID; LOINC 58410-2") ['metformin', '500', 'mg', 'bid', 'loinc', '58410', '2']
Source code in src/openbtk/retrieval/hybrid.py
54 55 56 57 58 59 60 61 | |
RAG¶
openbtk.pipelines.rag.RAGPipeline ¶
RAGPipeline(*, embedding: BaseEmbeddingProvider, vectorstore: BaseVectorStore, llm: BaseLLMProvider, reranker: BaseReranker | None = None, bm25: BM25Index | None = None, fusion_k: int = 60, top_k: int = 5, candidate_pool_size: int | None = None, text_metadata_key: str = _DEFAULT_TEXT_KEY, record_id_metadata_key: str = _DEFAULT_RECORD_ID_KEY, chunk_id_metadata_key: str = _DEFAULT_CHUNK_ID_KEY)
Answer a question by retrieving grounded context and generating against it, real chunk-level provenance attached throughout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding
|
BaseEmbeddingProvider
|
Embeds the question into the vector store's own space. |
required |
vectorstore
|
BaseVectorStore
|
Queried for the nearest indexed chunks. |
required |
llm
|
BaseLLMProvider
|
Generates the final answer from the retrieved context. |
required |
reranker
|
BaseReranker | None
|
Optional. When given, |
None
|
bm25
|
BM25Index | None
|
Optional. A :class: |
None
|
fusion_k
|
int
|
The reciprocal-rank-fusion smoothing constant (only used with
|
60
|
top_k
|
int
|
Number of chunks to ground the answer in. |
5
|
candidate_pool_size
|
int | None
|
How many results to fetch (from each retriever)
before fusing and reranking. Defaults to |
None
|
text_metadata_key
|
str
|
Metadata key holding each chunk's own text -- see this module's own docstring for the indexing convention this whole class depends on. |
_DEFAULT_TEXT_KEY
|
record_id_metadata_key
|
str
|
Metadata key holding the source record id. |
_DEFAULT_RECORD_ID_KEY
|
chunk_id_metadata_key
|
str
|
Metadata key holding the chunk id; falls back to the vector store's own result id when absent. |
_DEFAULT_CHUNK_ID_KEY
|
No I/O happens in __init__ (docs/09_CODING_STANDARDS.md rule 11):
every real call happens inside :meth:ask.
Source code in src/openbtk/pipelines/rag.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
ask ¶
ask(question: str, **generate_kwargs: Any) -> RAGAnswer
Retrieve context for question, then generate a grounded answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
question
|
str
|
The user's question. Embedded, used as the vector store query, and (if a reranker is set) as the reranking query too. |
required |
**generate_kwargs
|
Any
|
Forwarded to the LLM provider's |
{}
|
Returns:
| Type | Description |
|---|---|
RAGAnswer
|
The generated text plus the exact chunks it was grounded in, |
RAGAnswer
|
in the order given to the model. |
Raises:
| Type | Description |
|---|---|
RetrievalError
|
If embedding or the vector store query fails. |
ProviderError
|
If the LLM call fails. |
Source code in src/openbtk/pipelines/rag.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |