Semantic & hybrid search
Opt into dense retrieval in TypeScript: fix the method on ratel(), embed at register, rank 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 on ratel(), await r.tools.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 { ratel } from "@ratel-ai/sdk";
const r = ratel({
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 r.tools.register(...tools);registeris variadic — spread the whole batch into one call: separateregistercalls embed separately.- Re-registering an id replaces it in place and costs one embedding, not N.
- A
"bm25"core 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 core and re-register. The cache lifecycle is on The embedding cache.
Assembling the layers yourself? new ToolCatalog({ method, embedding }) takes the same options — r.tools is a guarded handle over that shared catalog, reachable at r.tools.catalog.
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.
ratel({ method: "hybrid", embedding: { huggingface: "BAAI/bge-base-en-v1.5" } });
// Local checkpoint — same in-process engine, air-gapped. A bare path also works.
ratel({ method: "hybrid", embedding: { local: "/opt/models/bge" } });
// Ollama — the local server at http://localhost:11434.
ratel({ method: "hybrid", embedding: { ollama: "nomic-embed-text" } });
// OpenAI-compatible endpoint — the route for non-BERT or multilingual models.
ratel({
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
},
});One embedding covers both corpora: ratel() forwards it to the tool and skill catalogs alike. Which model each source loads, downloads, and how it fails is on Embedding models.
Search any method
r.tools.searchAsync(query, topK, method?) runs any method off the event loop; method overrides the core default for that call:
const hits = await r.tools.searchAsync("rotate the api key", 5);
const bm25 = await r.tools.searchAsync("rotate the api key", 5, "bm25");Synchronous r.tools.search stays BM25-only: a "semantic"/"hybrid" method, passed or inherited from the core 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 — await r.tools.register(...) first. search_capabilities always ranks with the construction-time default (Use the discovery tools).
Handle embedding failures
Every dense failure from await r.tools.register(...) or await r.tools.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 r.tools.register(...tools);
} catch (err) {
if (err instanceof DimensionMismatchError) {
// the model changed under an existing corpus: new core + 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: await theregisterpromise 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
The same core covers skills: ratel() forwards method and embedding to the shared SkillCatalog, await r.skills.register(...) embeds, r.skills.searchAsync(query, topK, origin?, method?) ranks. The tool and skill caches are independent (Register skills).
Next steps
Use the discovery tools
Hand any loop the three capability tools from r.modelTools(), rank the catalog host-side with r.tools.search and r.recall, and pick a retrieval method per call.
Framework integrations
Official adapters for the Vercel AI SDK and Mastra: ratel().adaptTo(...) speaks each framework's native tool and message shapes, with per-turn recall built in.