The Vercel AI SDK ↗ is a TypeScript toolkit for building applications with large language models. The ai-search-provider ↗ package connects AI Search to the AI SDK, so you can generate responses grounded in your indexed content, retrieve chunks, and manage documents from the same API.
This guide builds a Worker that creates an AI Search instance, uploads and indexes a document, and then queries it with the AI SDK.
- Sign up for a Cloudflare account ↗.
- Install
Node.js↗.
Node.js version manager
Use a Node version manager like Volta ↗ or nvm ↗ to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.
Create a new Worker project using the create-cloudflare CLI (C3). C3 ↗ is a command-line tool designed to help you set up and deploy new applications to Cloudflare.
Create a new project named ai-search-ai-sdk by running:
npm create cloudflare@latest -- ai-search-ai-sdkyarn create cloudflare ai-search-ai-sdkpnpm create cloudflare@latest ai-search-ai-sdkFor setup, select the following options:
- For What would you like to start with?, choose
Hello World example. - For Which template would you like to use?, choose
Worker only. - For Which language do you want to use?, choose
TypeScript. - For Do you want to use git for version control?, choose
Yes. - For Do you want to deploy your application?, choose
No(we will be making some changes before deploying).
Go to your application directory:
cd ai-search-ai-sdkInstall the AI SDK and the AI Search provider. The provider requires AI SDK v6 (ai@^6):
npm i ai ai-search-provideryarn add ai ai-search-providerpnpm add ai ai-search-providerbun add ai ai-search-providerCreate a binding between your Worker and AI Search. Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform.
Add the following to your Wrangler configuration file:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"ai_search_namespaces": [
{
"binding": "AI_SEARCH",
"namespace": "default",
"remote": true
}
]
}[[ai_search_namespaces]]
binding = "AI_SEARCH"
namespace = "default"
remote = trueThis binds the default namespace to env.AI_SEARCH. The remote option lets wrangler dev proxy requests to your deployed instance, since AI Search does not run locally. The ai_search_namespaces binding requires a compatibility_date of 2026-03-27 or later, which new C3 projects already satisfy.
Add a /setup route that creates an instance and uploads a document. Enable hybrid search at creation by setting index_method to index both vectors and keywords.
The create() method is on the namespace binding (env.AI_SEARCH), not on the provider client. Creating an instance that already exists throws, so the following code creates it and, on the next run, updates it instead.
import { createAISearchNamespace } from "ai-search-provider";
import { generateText, streamText } from "ai";
const INSTANCE_NAME = "knowledge-base";
const SAMPLE_DOC = `# Caching on Cloudflare
Cloudflare caches static assets at the edge. Use Cache Rules to control what is
cached, set an Edge Cache TTL to control how long objects stay in cache, and
purge the cache after a deploy.`;
// Create the instance with hybrid search, or update it if it already exists.
async function ensureInstance(env) {
// index_method with both vector and keyword enables hybrid search.
const hybrid = { index_method: { vector: true, keyword: true } };
try {
await env.AI_SEARCH.create({ id: INSTANCE_NAME, ...hybrid });
} catch {
await env.AI_SEARCH.get(INSTANCE_NAME).update(hybrid);
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
// createAISearchNamespace adapts the binding to the AI SDK provider API.
const aiSearch = createAISearchNamespace({ binding: env.AI_SEARCH });
// Visit /setup once to create the instance and index a document.
if (url.pathname === "/setup") {
await ensureInstance(env);
const instance = aiSearch.get(INSTANCE_NAME);
// upload() queues the file and returns immediately. Indexing runs in
// the background, so poll the item's status until it is searchable.
const { id, key } = await instance.items.upload("caching.md", SAMPLE_DOC);
let info = await instance.items.get(id).info();
while (info.status === "queued" || info.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 2_000));
info = await instance.items.get(id).info();
}
return Response.json({ key, status: info.status });
}
// Query the instance (see the next step).
return new Response("Visit /setup first, then query with ?q=");
},
};import { createAISearchNamespace } from "ai-search-provider";
import { generateText, streamText } from "ai";
interface Env {
AI_SEARCH: AiSearchNamespace;
}
const INSTANCE_NAME = "knowledge-base";
const SAMPLE_DOC = `# Caching on Cloudflare
Cloudflare caches static assets at the edge. Use Cache Rules to control what is
cached, set an Edge Cache TTL to control how long objects stay in cache, and
purge the cache after a deploy.`;
// Create the instance with hybrid search, or update it if it already exists.
async function ensureInstance(env: Env) {
// index_method with both vector and keyword enables hybrid search.
const hybrid = { index_method: { vector: true, keyword: true } };
try {
await env.AI_SEARCH.create({ id: INSTANCE_NAME, ...hybrid });
} catch {
await env.AI_SEARCH.get(INSTANCE_NAME).update(hybrid);
}
}
export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
// createAISearchNamespace adapts the binding to the AI SDK provider API.
const aiSearch = createAISearchNamespace({ binding: env.AI_SEARCH });
// Visit /setup once to create the instance and index a document.
if (url.pathname === "/setup") {
await ensureInstance(env);
const instance = aiSearch.get(INSTANCE_NAME);
// upload() queues the file and returns immediately. Indexing runs in
// the background, so poll the item's status until it is searchable.
const { id, key } = await instance.items.upload("caching.md", SAMPLE_DOC);
let info = await instance.items.get(id).info();
while (info.status === "queued" || info.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 2_000));
info = await instance.items.get(id).info();
}
return Response.json({ key, status: info.status });
}
// Query the instance (see the next step).
return new Response("Visit /setup first, then query with ?q=");
},
} satisfies ExportedHandler<Env>;AiSearchNamespace is an ambient type available after you run wrangler types.
There are three ways to query your instance from the AI SDK. Pick the one that fits your application:
- Generate a response returns the complete answer in one call.
- Stream a response sends tokens as they are generated, which suits long answers and chat interfaces.
- Search as a tool lets the model decide when to search, which is what you want in an agent.
Pass instance.chat() to generateText, and AI Search retrieves relevant content and generates a response in one call.
Replace the query placeholder in your fetch handler with the following:
const query = url.searchParams.get("q") ?? "How does caching work?";
// chat() returns an AI SDK model that retrieves matching chunks and generates
// a grounded answer in one call, so there is no separate search step.
const { text, sources } = await generateText({
model: aiSearch.get(INSTANCE_NAME).chat({
ai_search_options: {
// "hybrid" ranks results from both the vector and keyword indexes.
retrieval: { retrieval_type: "hybrid", max_num_results: 5 },
},
}),
messages: [{ role: "user", content: query }],
});
// `sources` holds the retrieved chunks, so you can cite them alongside `text`.
return Response.json({ text, sources });const query = url.searchParams.get("q") ?? "How does caching work?";
// chat() returns an AI SDK model that retrieves matching chunks and generates
// a grounded answer in one call, so there is no separate search step.
const { text, sources } = await generateText({
model: aiSearch.get(INSTANCE_NAME).chat({
ai_search_options: {
// "hybrid" ranks results from both the vector and keyword indexes.
retrieval: { retrieval_type: "hybrid", max_num_results: 5 },
},
}),
messages: [{ role: "user", content: query }],
});
// `sources` holds the retrieved chunks, so you can cite them alongside `text`.
return Response.json({ text, sources });AI Search returns the retrieved chunks as AI SDK source parts in sources, so you can cite them alongside the generated text. Because the instance indexes both vectors and keywords, retrieval_type: "hybrid" uses both.
For longer responses, use streamText instead of generateText. AI Search emits the retrieved chunks as source parts before the first text part.
// streamText returns right away; tokens stream in as they are generated.
const result = streamText({
model: aiSearch.get(INSTANCE_NAME).chat(),
messages: [{ role: "user", content: query }],
});
// toTextStreamResponse() streams the generated text only.
return result.toTextStreamResponse();// streamText returns right away; tokens stream in as they are generated.
const result = streamText({
model: aiSearch.get(INSTANCE_NAME).chat(),
messages: [{ role: "user", content: query }],
});
// toTextStreamResponse() streams the generated text only.
return result.toTextStreamResponse();toTextStreamResponse() sends the generated text and drops the sources. To stream the retrieved chunks as well, return a UI message stream with sendSources enabled, or read result.fullStream directly:
// sendSources forwards each retrieved chunk as a source-url part. They arrive
// before the first text part, so you can render citations as the answer streams.
return result.toUIMessageStreamResponse({ sendSources: true });// sendSources forwards each retrieved chunk as a source-url part. They arrive
// before the first text part, so you can render citations as the answer streams.
return result.toUIMessageStreamResponse({ sendSources: true });With chat(), AI Search searches your instance on every request. To let the model decide when to search instead, expose instance.search() as an AI SDK tool ↗ and pass it to a model that supports function calling, such as a Workers AI model. This is the pattern to use in an agent, where the model chooses between searching and other tools.
Install the Workers AI provider and Zod. Use version 3 of workers-ai-provider: the latest version 4 requires AI SDK v7, but ai-search-provider requires v6, so npm fails to install them together.
npm i workers-ai-provider@^3 zodyarn add workers-ai-provider@^3 zodpnpm add workers-ai-provider@^3 zodbun add workers-ai-provider@^3 zodAdd a Workers AI binding to your Wrangler configuration:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"ai": {
"binding": "AI"
}
}[ai]
binding = "AI"Then define a search tool. The model calls it when it needs to retrieve content:
import { createWorkersAI } from "workers-ai-provider";
import { generateText, tool, stepCountIs } from "ai";
import { z } from "zod";
const instance = aiSearch.get(INSTANCE_NAME);
// The tool-caller must support function calling. Use a dedicated model here
// rather than the AI Search chat model, which retrieves on every request.
const workersai = createWorkersAI({ binding: env.AI });
const { text } = await generateText({
model: workersai("@cf/zai-org/glm-5.2"),
messages: [{ role: "user", content: query }],
tools: {
search_knowledge_base: tool({
description: "Search the indexed knowledge base for relevant content.",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
// The model decides when to call this; it searches the instance.
execute: async ({ query }) =>
instance.search({
query,
ai_search_options: { retrieval: { max_num_results: 5 } },
}),
}),
},
// Cap the tool-call loop so the model cannot invoke tools indefinitely.
stopWhen: stepCountIs(5),
});import { createWorkersAI } from "workers-ai-provider";
import { generateText, tool, stepCountIs } from "ai";
import { z } from "zod";
const instance = aiSearch.get(INSTANCE_NAME);
// The tool-caller must support function calling. Use a dedicated model here
// rather than the AI Search chat model, which retrieves on every request.
const workersai = createWorkersAI({ binding: env.AI });
const { text } = await generateText({
model: workersai("@cf/zai-org/glm-5.2"),
messages: [{ role: "user", content: query }],
tools: {
search_knowledge_base: tool({
description: "Search the indexed knowledge base for relevant content.",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
// The model decides when to call this; it searches the instance.
execute: async ({ query }) =>
instance.search({
query,
ai_search_options: { retrieval: { max_num_results: 5 } },
}),
}),
},
// Cap the tool-call loop so the model cannot invoke tools indefinitely.
stopWhen: stepCountIs(5),
});Before you deploy, confirm the whole flow works against your instance with wrangler dev, which proxies the remote AI Search binding. The output below is from the generate a response option.
Start a local development server:
npx wrangler devFirst, index the sample document by visiting /setup:
curl http://localhost:8787/setupThe first /setup can take a minute or two while the document indexes. When it finishes, you get back the item key and a completed status:
{ "key": "caching.md", "status": "completed" }Then query the instance:
curl "http://localhost:8787/?q=How+does+caching+work%3F"A working integration returns generated text grounded in your document, along with a sources array referencing the file it retrieved (fields trimmed):
{
"text": "Cloudflare caches static assets at the edge...",
"sources": [{ "sourceType": "url", "url": "caching.md" }]
}If sources comes back empty, the document has not finished indexing yet. Run /setup again, then retry the query.
Log in with your Cloudflare account:
npx wrangler loginDeploy your Worker to make it accessible on the Internet:
npx wrangler deploy- The chat model is text-only. File and image message parts are not supported.
- Generation options such as
temperatureandmaxOutputTokensare passed through to the instance's generation model.maxOutputTokenstruncates the response and setsfinishReasonto"length". If a model ignores an option, the result'swarningsarray stays empty, so an unsupported option fails silently. - AI Search uses the generation model configured on the instance by default. Pass
instance.chat({ model: "..." })to override it per request.