Semantic & hybrid search
Opt into dense retrieval in Python: embed at register, search with search_async, and handle the typed embedding errors.
BM25 is the default and needs none of this page. When paraphrase queries keep missing, opt into the dense engines: fix the method at construction, await register(...) to embed, search with search_async. How the engines rank is on Retrieval and search; the model and its sources are on Embedding models.
Opt in and embed
The method is fixed at construction, and register is the only thing that embeds:
import asyncio
from ratel_ai import ToolCatalog
catalog = ToolCatalog(
method="hybrid", # every search defaults to hybrid
# embedding={"huggingface": "..."}, # optional non-default model
)
async def main():
# One batch, one embedding request. Metadata is stored synchronously, the
# instant register() is called; the awaitable drives only the embedding
# pass, on a worker thread that never blocks the event loop.
await catalog.register(tools)
asyncio.run(main())- Always await the result. A forgotten
awaitnever drops metadata, but it skips the embedding pass, and the next dense search raises with exactly that hint instead of ranking an empty cache. - Pass the whole batch at once: separate
registercalls embed separately. - Re-registering an id replaces it in place and costs one embedding, not N.
- A
"bm25"catalog never loads a model; its awaitable is a no-op. - A model or dimension change is not recovered in place: construct a new catalog and re-register. The cache lifecycle is on The embedding cache.
Configure another model
Omit embedding and the default BAAI/bge-small-en-v1.5 runs in-process, keyless. To use another model, name the source by its key — one of four:
# HuggingFace repo — in-process via Candle, like the default.
ToolCatalog(method="hybrid", embedding={"huggingface": "BAAI/bge-base-en-v1.5"})
# Local checkpoint — same in-process engine, air-gapped. A bare path also works.
ToolCatalog(method="hybrid", embedding={"local": "/opt/models/bge"})
# Ollama — the local server at http://localhost:11434.
ToolCatalog(method="hybrid", embedding={"ollama": "nomic-embed-text"})
# OpenAI-compatible endpoint — the route for non-BERT or multilingual models.
ToolCatalog(
method="hybrid",
embedding={
"url": "https://api.openai.com/v1/embeddings",
"model": "text-embedding-3-small",
"api_key_env": "OPENAI_API_KEY", # named env var, read at call time
},
)SkillCatalog takes the same option. Which model each source loads, downloads, and how it fails is on Embedding models.
Search any method
search_async(query, top_k, origin="direct", method=None) runs any method on a worker thread; method= overrides the catalog default for that call:
async def main():
hits = await catalog.search_async("rotate the api key", 5)
bm25 = await catalog.search_async("rotate the api key", 5, method="bm25")Synchronous search stays BM25-only: a "semantic"/"hybrid" method, passed or inherited from the catalog default, raises immediately with guidance to use search_async; an unknown method string raises ValueError. A search never embeds the corpus; on an unembedded corpus a dense search fails fast and the guard loads no model. search_capabilities always ranks with the construction-time default (Use the discovery tools).
Handle embedding failures
Every dense failure from await register(...) or await search_async(...) is a typed EmbedderError. Branch on the class:
from ratel_ai import DimensionMismatchError, EmbedderError
async def main():
try:
await catalog.register(tools)
except DimensionMismatchError:
# the model changed under an existing corpus: new catalog + re-register
raise
except EmbedderError as err:
print(err) # load / download / endpoint / auth detail is in the messageEmbedderErrorsubclassesRuntimeError, so existingexcept RuntimeErrorhandlers keep working;DimensionMismatchErrorsubclassesEmbedderError.- Unlike TypeScript there is no
.codeattribute: branch on the class and read the detail from the message. - Invalid embedding config is not an
EmbedderError: it raisesValueErrorat construction, before any model loads.
Skills rank the same way
SkillCatalog mirrors all of it: method= and embedding= at construction, await skills.register(...) embeds, skills.search_async(query, top_k, origin="direct", method=None) ranks. The tool and skill caches are independent (Register skills).
Next steps
Use the discovery tools
Hand the agent search_capabilities, invoke_tool, and get_skill_content, then assemble each turn's toolset with a top-K pre-filter.
Framework integrations
Wire Ratel into Pydantic AI: one adapter to the framework's tool type, one loop that assembles the capability tools plus a top-K pre-filter.