Ratel Docs
Python SDK

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 await never 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 register calls 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 message
  • EmbedderError subclasses RuntimeError, so existing except RuntimeError handlers keep working; DimensionMismatchError subclasses EmbedderError.
  • Unlike TypeScript there is no .code attribute: branch on the class and read the detail from the message.
  • Invalid embedding config is not an EmbedderError: it raises ValueError at 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

On this page