Semantic search & RAG
env.SEARCH lets your app find text by meaning, not just keywords. You upsert documents; Cloudrizz turns each into an embedding and stores it. A query returns the most relevant documents — even when they don't share any words with the question. It's the retrieval half of RAG: pull the right context, then hand it to env.AI to answer.
env.SEARCH is a paid-plan feature. Reference it in any api/*.js file and it's wired up on deploy — no index to create, no keys to manage. Embeddings are generated for you.Index documents
upsert takes one item or an array. Each item is { id?, text, metadata? }. The text is embedded automatically; metadata is free-form JSON you can filter on later. Re-using an id overwrites that document (so re-indexing is safe). If you omit id, one is generated.
await env.SEARCH.upsert([
{ id: "note_1", text: "Client wants a north-facing balcony.",
metadata: { projectId: "p_42", userId: "u_9" } },
{ id: "note_2", text: "Budget approved for phase two.",
metadata: { projectId: "p_42", userId: "u_7" } },
]);Chunk long text before indexing. Split a big document into passages (a few sentences to a paragraph each) and upsert one item per chunk — retrieval is far more precise on small chunks, and each has its own id like doc123#0, doc123#1.
Index a file from storage (PDF, Word, text…)
If your content lives in file storage, you don't have to extract and chunk it yourself — pass { index: true } when you store it. Cloudrizz reads the file, pulls out its text, chunks it, and indexes it automatically.
// Store a PDF and make it searchable in one call
await env.STORAGE.put("docs/handbook.pdf", pdfBytes, {
httpMetadata: { contentType: "application/pdf" },
index: true,
});
// Later — search across everything you've indexed
const { matches } = await env.SEARCH.query("how many vacation days do I get?");Supported file types:
- Text —
.txt,.md,.csv,.json,.html(tags stripped). - Word —
.docx. - PDF — text-based PDFs. Scanned PDFs (images of text) have no extractable text and are skipped — those need OCR, which isn't supported yet.
Search
query returns the best matches, most relevant first. Each match carries its text, your metadata, and a score (higher = closer in meaning).
const { matches } = await env.SEARCH.query("outdoor space facing the sun", {
topK: 5,
});
// matches: [{ id, score, text, metadata }, ...]
// note_1 ranks first even though it shares no words with the query.Scope a search with metadata
Pass where to restrict the search to documents whose metadata contains every key/value you give — “search within this project”, “only this user’s notes”. The filter is applied inside the search, so you get the top matches within that scope, not the global top matches filtered down.
// Only documents in project p_42
await env.SEARCH.query("budget", { topK: 5, where: { projectId: "p_42" } });
// Narrow further: this project AND this user
await env.SEARCH.query("balcony", {
where: { projectId: "p_42", userId: "u_9" },
});Build a RAG chat
Retrieval-Augmented Generation is just: search, then ask. Pull the most relevant context with env.SEARCH, put it in the prompt, and stream the answer with env.AI. The model now answers from your data.
export async function POST(request, env) {
const { question, projectId } = await request.json();
// 1. Retrieve the most relevant passages (scoped to the project)
const { matches } = await env.SEARCH.query(question, {
topK: 6,
where: { projectId },
});
const context = matches.map((m) => m.text).join("\n---\n");
// 2. Ask the model, grounded in that context — streamed back to the client
const ai = await env.AI.anthropic.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{
role: "user",
content: `Answer using only this context:\n${context}\n\nQuestion: ${question}`,
}],
});
return new Response(ai.body, { headers: { "content-type": "text/event-stream" } });
}API
upsert(items)— index one item or an array of{ id?, text, metadata? }. Returns{ upserted }.query(text, { topK = 5, where })— semantic search; returns{ matches: [{ id, score, text, metadata }] }.delete(ids)— remove documents by id.clear(where?)— delete everything, or only documents matching a metadata filter (e.g.clear({ projectId: "p_42" })).count(where?)— number of documents, optionally filtered →{ count }.
Notes
- One
upserttakes up to 100 items per call; split larger batches into chunks. - Deleting a file you indexed with
{ index: true }(viaenv.STORAGE.delete(key)) automatically removes its indexed text — no orphaned entries to clean up. - An
upsertor aquerycounts as one search operation against your plan's monthly allowance — see Limits & quotas. - Search finds meaning, not exact strings. For exact lookups (an email, an order number) query your database directly.
query_app_search tool — run a query to check retrieval, or upsert sample documents so a search/RAG feature isn't empty when you try it.