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:
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:
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 callconst 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); // 768Search 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:
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 matchesconst 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 directlyconst { score } = await g.similarity("cancel my plan", "end my subscription");
// Rank candidates against a vector you already haveconst { 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:
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 chunkawait mem.add("The user prefers TypeScript and dark mode.", { metadata: { source: "onboarding" },});await mem.add(longDocument, { chunk: true });
// Semantic search over everything storedconst 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 blockconst { 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:
"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.