Semantic Search

Search by meaning, not keywords, with a few lines of SDK code. Embeddings run on EmbeddingGemma-300M on WebGPU, in Node or in the browser. No server, no API keys. Try it live, this box searches these docs with the same APIs documented below:

Loading search…

Tip: press ⌘K (or /) anywhere in the docs to open this search as a command palette.

One-line embeddings

embed and embedBatch turn text into L2-normalized vectors. The embedding model downloads on first use and stays cached:

embed.ts
import { embed, embedBatch } from "@tryhamster/gerbil";
// One text -> one L2-normalized 768-dim vector (EmbeddingGemma-300M on WebGPU)
const { vector } = await embed("How do I cancel my subscription?");
// A whole corpus in one call
const docs = await embedBatch([
"Refunds are processed within 5 business days.",
"You can cancel anytime from the billing page.",
"We support SSO via SAML and OIDC.",
]);
console.log(docs[0].vector.length); // 768

Search a corpus

You never have to touch a vector yourself. search() embeds the query and the corpus, cosine-ranks, and returns scored matches. similarity() scores a pair, and findNearest() ranks candidates against a vector you already computed:

search.ts
import { Gerbil } from "@tryhamster/gerbil";
const g = new Gerbil(); // no loadModel() needed for embeddings
// Embed the query and the corpus, cosine-rank, return the top matches
const results = await g.search(
"How do I cancel my subscription?",
[
"Refunds are processed within 5 business days.",
"You can cancel anytime from the billing page.",
"We support SSO via SAML and OIDC.",
],
{ topK: 2 },
);
// [{ text: "You can cancel anytime...", score: 0.71, index: 1 }, ...]
// Score two texts directly
const { score } = await g.similarity("cancel my plan", "end my subscription");
// Rank candidates against a vector you already have
const { vector } = await g.embed("dog");
const nearest = await g.findNearest(vector, ["cat", "car", "tree"], { topK: 1 });

Persistent memory (vector store + recall)

For a corpus you keep and query over time, the @tryhamster/gerbil/memory module is a tiny on-device vector store: automatic chunking on write, metadata filters, pluggable persistence (in-memory, JSON file, or IndexedDB in the browser), and recall() for RAG, which packs the best matches into a token-budgeted context block ready to prepend to a prompt:

memory.ts
import { Gerbil } from "@tryhamster/gerbil";
import {
createFileStore,
createGerbilEmbedder,
createMemory,
} from "@tryhamster/gerbil/memory";
const g = new Gerbil();
const mem = createMemory({
embed: createGerbilEmbedder(g),
store: createFileStore("./memory.json"), // omit for in-memory
});
// Write: long text can be chunked automatically, one record per chunk
await mem.add("The user prefers TypeScript and dark mode.", {
metadata: { source: "onboarding" },
});
await mem.add(longDocument, { chunk: true });
// Semantic search over everything stored
const hits = await mem.search("what does the user like?", { k: 3 });
// [{ record: { text, metadata, ... }, score: 0.82 }, ...]
// Or recall(): packs the best matches into a token-budgeted context block
const { context, tokensUsed } = await mem.recall("what does the user like?", {
tokenBudget: 512,
});
const answer = await g.generate(`${context}\n\nAnswer the question: ...`);

See the Memory docs for redaction, import/export, and custom stores.

In React

The hooks run the same engine in the browser. useEngine({ embedding: true }) exposes embed() (with query vs document task types) and similarity(), and useMemory() gives you the full memory API backed by IndexedDB:

Search.tsx
"use client";
import { useEngine, useMemory } from "@tryhamster/gerbil/hooks";
function Search() {
// Embeddings in the browser: same engine, on WebGPU
const { embed, similarity, isReady } = useEngine({ embedding: true });
async function rank(query: string, corpus: string[]) {
const q = await embed(query, { taskType: "query" });
const vecs = await Promise.all(
corpus.map((t) => embed(t, { taskType: "document" })),
);
return vecs
.map((v, i) => ({
text: corpus[i],
score: v.reduce((s, x, j) => s + x * q[j], 0), // both L2-normalized
}))
.sort((a, b) => b.score - a.score);
}
// Persistent memory backed by IndexedDB, survives reloads
const memory = useMemory({ namespace: "my-app" });
// await memory.add("The user prefers TypeScript.");
// const { context } = await memory.recall("what does the user like?");
}

How the search box above works

  • ·Build time: a Node script chunks every docs page and embeds each chunk with embed(), then commits the vectors as a compact JSON file (~1 MB), so the deploy never needs a GPU.
  • ·Runtime: only your query is embedded, in your browser, then cosine-ranked against the shipped vectors. That is a few hundred dot products, instant even on a phone.
  • ·Graceful fallback: while the model downloads (or on browsers without WebGPU) the box answers with a plain substring match, then upgrades to semantic ranking the moment the engine is ready.