Semantic & hybrid search
Opt into dense retrieval in TypeScript: embed at register, search with searchAsync, 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 searchAsync. 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 { ToolCatalog } from "@ratel-ai/sdk";
const catalog = new ToolCatalog({
method: "hybrid", // every search defaults to hybrid
// embedding: { huggingface: "..." } // optional non-default model
});
// One batch, one embedding request. Metadata is indexed synchronously;
// the await drives the embedding pass on a libuv worker, so the event
// loop is never blocked.
await catalog.register(tools);- 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 resolves as soon as metadata is indexed and never loads a model. - 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.
new ToolCatalog({ method: "hybrid", embedding: { huggingface: "BAAI/bge-base-en-v1.5" } });
// Local checkpoint — same in-process engine, air-gapped. A bare path also works.
new ToolCatalog({ method: "hybrid", embedding: { local: "/opt/models/bge" } });
// Ollama — the local server at http://localhost:11434.
new ToolCatalog({ method: "hybrid", embedding: { ollama: "nomic-embed-text" } });
// OpenAI-compatible endpoint — the route for non-BERT or multilingual models.
new ToolCatalog({
method: "hybrid",
embedding: {
url: "https://api.openai.com/v1/embeddings",
model: "text-embedding-3-small",
apiKeyEnv: "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
searchAsync(query, topK, origin?, method?) runs any method off the event loop; method overrides the catalog default for that call:
const hits = await catalog.searchAsync("rotate the api key", 5);
const bm25 = await catalog.searchAsync("rotate the api key", 5, "direct", "bm25");Synchronous search stays BM25-only: a "semantic"/"hybrid" method, passed or inherited from the catalog default, throws immediately with guidance to use searchAsync. 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 searchAsync(...) is a typed EmbedderError. Branch on instanceof or its stable code, never on message text:
import { DimensionMismatchError, EmbedderError } from "@ratel-ai/sdk";
try {
await catalog.register(tools);
} catch (err) {
if (err instanceof DimensionMismatchError) {
// the model changed under an existing corpus: new catalog + re-register
} else if (err instanceof EmbedderError) {
console.error(err.code, err.message); // e.g. "NotCached", "Download"
} else {
throw err;
}
}- The codes:
"Load","Download","NotCached","ModelMismatch","DimensionMismatch","EmbeddingsNotBuilt","Inference","CacheUnwritable". "EmbeddingsNotBuilt"on a corpus you did register is the signature of a forgottenawait: the message itself says toawait catalog.register(...)before a dense search.- Invalid embedding config is not an
EmbedderError: it throws a plainErrorat 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.searchAsync(query, topK, origin?, method?) ranks. The tool and skill caches are independent (Register skills).
Next steps
Use the discovery tools
Wrap the catalog in search_capabilities and invoke_tool, assemble each turn's toolset with a top-K pre-filter, and pick a retrieval method per call.
Framework integrations
Wire Ratel into the Vercel AI SDK: one adapter to the framework's tool type, one loop that assembles the capability tools plus a top-K pre-filter.