Vercel AI SDK ↗ は、大規模言語モデルでアプリケーションを構築するための TypeScript ツールキットです。ai-search-provider ↗ パッケージは AI Search を AI SDK につなぎ、同じ API からインデックス済みコンテンツに基づく応答の生成、チャンクの取得、ドキュメントの管理ができます。
このガイドでは、AI Search インスタンスを作成し、ドキュメントをアップロードしてインデックスし、AI SDK でクエリする Worker を作ります。
- Cloudflare アカウント ↗ に登録します。
Node.js↗ をインストールします。
Node.js のバージョンマネージャー
権限の問題を避け、Node.js のバージョンを切り替えられるよう、Volta ↗ や nvm ↗ などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。
create-cloudflare CLI(C3)で新しい Worker プロジェクトを作成します。C3 ↗ は、Cloudflare へのアプリケーションのセットアップとデプロイを支援するコマンドラインツールです。
次を実行し、ai-search-ai-sdk という名前のプロジェクトを作成します。
npm create cloudflare@latest -- ai-search-ai-sdkyarn create cloudflare ai-search-ai-sdkpnpm create cloudflare@latest ai-search-ai-sdkセットアップでは、次のオプションを選びます。
- What would you like to start with? では、
Hello World exampleを選びます。 - Which template would you like to use? では、
Worker onlyを選びます。 - Which language do you want to use? では、
TypeScriptを選びます。 - Do you want to use git for version control? では、
Yesを選びます。 - Do you want to deploy your application? では、
Noを選びます(デプロイ前にいくつか変更します)。
アプリケーションのディレクトリへ移動します。
cd ai-search-ai-sdkAI SDK と AI Search プロバイダーをインストールします。プロバイダーには 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-providerWorker と AI Search の間に binding を作成します。Bindings を使うと、Worker が Cloudflare Developer Platform 上のリソースとやり取りできます。
次を Wrangler 設定ファイル に追加します。
{
"$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 = trueこれで default namespace が env.AI_SEARCH にバインドされます。remote オプションを付けると、wrangler dev がデプロイ済みインスタンスへリクエストをプロキシします。AI Search はローカルでは動きません。ai_search_namespaces binding には 2026-03-27 以降の compatibility_date が必要です。新しい C3 プロジェクトはすでに満たしています。
インスタンスを作成し、ドキュメントをアップロードする /setup ルートを追加します。作成時に index_method を設定し、ベクトルとキーワードの両方をインデックスして ハイブリッド検索 を有効にします。
create() メソッドは、プロバイダークライアントではなく namespace binding(env.AI_SEARCH)にあります。すでに存在するインスタンスを作成すると例外になるため、次のコードは初回に作成し、次回以降は更新します。
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 は、wrangler types を実行したあと使えるグローバル型です。
AI SDK からインスタンスをクエリする方法は 3 つあります。アプリケーションに合うものを選んでください。
- 応答を生成する は、1 回の呼び出しで完全な回答を返します。
- 応答をストリーミングする は、生成されたトークンを順に送ります。長い回答やチャット UI に向きます。
- ツールとして検索する は、モデルが検索するタイミングを決めます。エージェント向けの使い方です。
instance.chat() を generateText に渡すと、AI Search が関連コンテンツを取得し、1 回の呼び出しで応答を生成します。
fetch ハンドラーのクエリ用プレースホルダーを、次のコードに置き換えます。
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 は取得したチャンクを AI SDK の source parts として sources に返します。生成テキストと並べて引用できます。インスタンスはベクトルとキーワードの両方をインデックスしているため、retrieval_type: "hybrid" は両方を使います。
長い応答では、generateText の代わりに streamText を使います。AI Search は、最初のテキストパートより前に、取得したチャンクを source parts として送ります。
// 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() は生成テキストだけを送り、sources は落とします。取得したチャンクもストリーミングするには、sendSources を有効にした UI メッセージストリームを返すか、result.fullStream を直接読みます。
// 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 });chat() では、AI Search は毎回インスタンスを検索します。検索するタイミングをモデルに任せるには、instance.search() を AI SDK の tool ↗ として公開し、function calling に対応するモデル(Workers AI モデルなど)に渡します。エージェントでは、検索とその他のツールをモデルが選ぶこのパターンを使います。
Workers AI プロバイダーと Zod をインストールします。workers-ai-provider はバージョン 3 を使います。最新のバージョン 4 は AI SDK v7 が必要ですが、ai-search-provider は v6 が必要なため、一緒にインストールすると npm が失敗します。
npm i workers-ai-provider@^3 zodyarn add workers-ai-provider@^3 zodpnpm add workers-ai-provider@^3 zodbun add workers-ai-provider@^3 zodWrangler 設定に Workers AI binding を追加します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"ai": {
"binding": "AI"
}
}[ai]
binding = "AI"次に検索ツールを定義します。コンテンツが必要なとき、モデルがこのツールを呼びます。
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),
});デプロイ前に、wrangler dev でリモートの AI Search binding をプロキシし、一連の流れがインスタンスに対して動くことを確認します。次の出力は 応答を生成する の場合です。
ローカルの開発サーバーを起動します。
npx wrangler devまず /setup にアクセスし、サンプルドキュメントをインデックスします。
curl http://localhost:8787/setup初回の /setup は、ドキュメントのインデックス中に 1〜2 分かかることがあります。完了すると、アイテムのキーと completed ステータスが返ります。
{ "key": "caching.md", "status": "completed" }次にインスタンスをクエリします。
curl "http://localhost:8787/?q=How+does+caching+work%3F"連携が正しく動くと、ドキュメントに基づく生成 text と、取得したファイルを参照する sources 配列が返ります(フィールドは省略しています)。
{
"text": "Cloudflare caches static assets at the edge...",
"sources": [{ "sourceType": "url", "url": "caching.md" }]
}sources が空の場合、ドキュメントのインデックスがまだ終わっていません。もう一度 /setup を実行してから、クエリを再試行してください。
Cloudflare アカウントでログインします。
npx wrangler loginWorker をデプロイし、インターネットからアクセスできるようにします。
npx wrangler deploy- チャットモデルはテキストのみです。ファイルや画像のメッセージパートには対応していません。
temperatureやmaxOutputTokensなどの生成オプションは、インスタンスの生成モデルにそのまま渡されます。maxOutputTokensは応答を切り詰め、finishReasonを"length"にします。モデルがオプションを無視しても、結果のwarnings配列は空のままです。未対応のオプションは静かに失敗します。- AI Search は、デフォルトでインスタンスに設定した生成モデルを使います。リクエスト単位で上書きするには、
instance.chat({ model: "..." })を渡します。