Search & RAG
Audience: apps that need "find it by words or by meaning" over encrypted records — and, optionally, an LLM answering questions over them. Bundle: core +
withSearch()(one factory, all three retrieval modes) +@noy-db/in-aifor the LLM loop.
Verified by: showcases/src/recipe-search-rag.recipe.test.ts — the code below is the same code that runs there.
Everything on this page happens client-side, after decryption: the index, the vectors, the ranking, and the tool dispatch all run inside the hub. The store holds only ciphertext throughout — search adds no leakage beyond reading the collection.
Prerequisites
searchStrategy: withSearch()— one opt-in enables lexical, semantic, and hybrid (see search / search — AI, two pages, one subsystem).- A
textIndexesallowlist on the collection for indexed lexicalretrieve(). - An
embeddingsencoder on the collection for semantic/hybrid — a{ dim, model, source, encode }object you supply. An on-device model keeps zero-knowledge end-to-end; a hosted embeddings API adds that provider as a party that can read your text — choose deliberately. @noy-db/in-aifor the RAG half:pnpm add @noy-db/hub @noy-db/in-ai.
Setup
import { createNoydb, withSearch } from '@noy-db/hub'
import { toBrowserIdb } from '@noy-db/to-browser-idb'
const db = await createNoydb({
store: toBrowserIdb(),
user: 'analyst',
secret: 'search-rag-pass-2026',
searchStrategy: withSearch(),
})
const vault = await db.openVault('firm')
const notes = vault.collection<Note>('notes', {
textIndexes: ['text'], // lexical retrieval reads this allowlist
embeddings: encoder, // your text→vector function (dim/model must match it)
})
The walkthrough's encoder is a deterministic stub (a bag-of-chars hash) so the paired test is reproducible — in an app you plug a real model; the shape is just:
const encoder = {
dim: 384, model: 'your-model-name', source: 'text' as const,
encode: async (text: string): Promise<Float32Array> => yourModel.embed(text),
}
Three retrieval modes, one call
await notes.retrieve('invoice payment', { mode: 'lexical' })
// BM25 over the textIndexes fields — matching *words*, ranked, with snippets
await notes.retrieve('invoice payment', { mode: 'semantic' })
// cosine similarity over the encrypted _vec side-cars — matching *meaning*
await notes.retrieve('invoice payment', { mode: 'hybrid' })
// both rankings fused (RRF): a record that appears in both lists rises
// above each modality's own leader — usually what an app wants by default
Zero-setup notes.search(field, query) also exists for ad-hoc scans with no index at all; the search page compares the paths.
The RAG half — permission-scoped tools for an LLM
@noy-db/in-ai turns the vault into function-calling tools — one per (collection × operation), in OpenAI / Anthropic / Vercel AI / JSON-Schema shapes — and dispatches the model's calls back. The LLM never touches keys; it sees only the decrypted records your host process chose to expose:
import { buildToolset, invokeToolCall } from '@noy-db/in-ai'
// Hand the model exactly these capabilities — an allowlist, not a firehose:
const tools = buildToolset(vault, {
format: 'openai',
operations: ['list', 'get'],
collections: ['notes'],
})
// → [{ function: { name: 'notes_list', … } }, { function: { name: 'notes_get', … } }]
// The model answers with a tool call; you dispatch it:
const result = await invokeToolCall(vault, { name: 'notes_get', args: { id: 'n-2' } })
// Out-of-allowlist calls are refused at dispatch too — defense in both directions:
await invokeToolCall(vault, { name: 'notes_delete', args: { id: 'n-2' } },
{ allowedOperations: ['list', 'get'] }) // → rejects
The retrieval modes above are the natural retriever for this loop: retrieve() picks the passages, the toolset fetches the records, your prompt assembles the context.
Verify
Run the paired walkthrough: all three modes return real ranked ids (the fusion never invents rows), the toolset emits exactly the allowlisted tools, dispatch round-trips a record, and an out-of-allowlist operation is refused.
See also
- search · search — AI — the two facets of the one
withSearch()subsystem, in depth. - in-ai — the adapter's full surface.
- Threat model — where a hosted encoder or LLM sits relative to the trust boundary.