Cloudflare ハンズオン教科書|2026-08 版
第 12 章

Vectorize と AI Search — ベクトル検索と RAG

自前ドキュメントに答えさせる。RAG パイプラインを 2 通りで組む

この章で学ぶこと

ベクトル検索と RAG の基礎

キーワード検索では「休暇は何日もらえますか」という質問で「有給休暇の付与日数」という FAQ を引き当てられません。必要なのは意味の近さで探すセマンティック検索で、その基盤がベクトル埋め込み(vector embedding)です。

埋め込みは、テキストなどの入力を機械学習モデルに通して得られる数値の配列(ベクトル)です。モデルが「意味が近い」と判断した入力同士は、ベクトル空間上でも距離が近くなります。配列の要素数はモデルごとに決まっており、これを次元(dimensions)と呼びます。Workers AI では @cf/baai/bge-base-en-v1.5 が 768 次元、多言語対応の @cf/baai/bge-m3 が 1024 次元です。

ベクトル同士の近さを測る関数が距離メトリクス(distance metric)です。Vectorize は次の 3 種類をサポートします。

metricスコアの読み方
cosine−1(最も遠い)〜 1(同一)。0 は直交
euclideanL2 距離。0 が同一で、正の値が大きいほど遠い
dot-product負の内積。負の絶対値が大きいほど、また正なら小さいほど類似

公式ドキュメントでは、テキスト・文書検索には cosine、画像や音声認識には euclidean が適するとされています。

RAG(Retrieval Augmented Generation)は、この類似検索を LLM と組み合わせる手法です。プロンプトをそのまま LLM に渡すのではなく、次の流れで自前データの文脈を注入します。

  1. ドキュメント群を埋め込みモデルでベクトル化し、ベクトルデータベースに保存する(事前準備)
  2. ユーザーの質問を同じモデルでベクトル化する
  3. 質問ベクトルで類似検索し、意味が近いドキュメントを取得する
  4. 取得した内容をコンテキストとして質問と一緒に LLM へ渡し、回答を生成させる
① 事前準備(データ更新時だけ) FAQ 文書 埋め込みモデル 値 + metadata Vectorize 本文 1024 次元 upsert 必ず同じ埋め込みモデル ② 質問のたび(毎リクエスト) ユーザー 埋め込みモデル Vectorize LLM 質問文 query() 上位 3 件 回答 — 取得した本文を根拠に、出典付きで返す
図 12-1 上段と下段は動くタイミングが違います。上段はデータを入れ替えたときだけ、下段はリクエストのたびに走ります。両段で同じ埋め込みモデルを使うのが必須条件で、ここがずれると次元が合わずエラーになるか、合っていても検索結果が意味を失います。

このパイプラインは、ベクトルデータベース Vectorize で各段階を自分で実装する方法と、AI Search にデータを渡して全段階を任せる方法の 2 通りで組めます。本章では両方を実装して違いを体感します。

Vectorize — 自分で組むベクトル検索

Vectorize は Cloudflare のベクトルデータベースで、Workers Free / Paid どちらのプランでも利用できます。データは「インデックス」単位で管理し(ベクトルの投入先も類似検索の対象も 1 つのインデックス)、作成時に次の 3 つを決めます。

dimensions と metric は作成後に変更できません。埋め込みモデルを乗り換える場合はインデックスの作り直しと全ベクトルの再生成が必要になるため、先にモデルを決めてから作成します。Worker からは binding(wrangler.jsonc の vectorize キー)経由で操作します。主なメソッドは次のとおりです。

メソッド動作
insert(vectors)新規挿入。同じ ID が既にあれば既存側が残る(先勝ち)
upsert(vectors)挿入または上書き(後勝ち)。マージはされず、値も metadata も丸ごと置き換わる
query(vector, options)類似ベクトルを検索し、スコア付きで返す
queryById(id, options)インデックス内の既存ベクトルを起点に類似検索する
getByIds(ids) / deleteByIds(ids)ID 指定で取得・削除する
describe()インデックス設定を取得する

insert / upsert / delete は非同期のミューテーションで、戻り値は処理識別子の mutationId です。書き込みの検索への反映には通常数秒かかります。

query() のオプションは topK(返す件数、既定 5)、returnValues(ベクトル値を返すか、既定 false)、returnMetadata("none" / "indexed" / "all"、既定 "none")です。topK の上限は 100 ですが、returnValues: true または returnMetadata: "all" の場合は 50 に制限されます。returnValues: true は元のベクトル値で正確なスコアを計算する高精度モードを兼ね、その分レイテンシが増えます。

metadata とフィルタ、namespace

各ベクトルには最大 10KiB の metadata(キーと値のペア)を付与できます。元ドキュメントの ID や URL のほか、本文そのものを入れておけば検索後の追加ルックアップを省けます。クエリ時に metadata で絞り込むには、対象フィールドの metadata index を事前に作成しておく必要があります(1 インデックスあたり最大 10 個、型は string / number / boolean)。フィルタは $eq / $ne / $in / $nin / $lt / $lte / $gt / $gte の演算子をサポートし、フィルタ適用後の集合から topK 件が返ります。

別の絞り込み手段として namespace(ベクトルごとに 1 つ持てるパーティションキー)もあり、metadata index 不要で metadata フィルタより先に適用されます。1 軸の分割(テナント分離など)は namespace、複数条件の絞り込みは metadata フィルタと使い分けます。

query() が候補を狭める順序(横幅は候補件数のイメージ) インデックス全体 例: 10,000 件 namespace で分割(事前準備なしで効く) namespace 一致 例: 1,200 件 metadata filter で絞る(要 metadata index) filter 一致 例: 40 件 metric(例: cosine)で順位付け(ここで初めて類似度を見る) topK 件 例: 3 件
図 12-2 バーの長さが「まだ候補として残っている件数」です。上から 2 本の矢印までは候補を捨てているだけで、類似度が登場するのは最後の矢印だけ。だから filter を付けても返るのは topK 件のままで、「類似上位 100 件を取ってから絞り込む」のではありません(件数は理解のための例)。

料金は「クエリしたベクトル次元数」と「保存したベクトル次元数」の 2 軸で課金され、2026-08 時点では Workers Free でも月 3,000 万クエリ次元 + 500 万保存次元まで無料です(Pricing)。1 ベクトルの最大次元数は 1536、1 インデックスあたり最大 2,000 万ベクトルという制限もあります(2026-08 時点、Limits)。

AI Search(旧称 AutoRAG)は、前節までの各段階をまるごと引き受けるマネージド検索サービスで、全プランで利用できます。内部では R2 と Vectorize による組み込みストレージとベクトルインデックスが自動でプロビジョニングされ、データを渡すと次のインデックス処理が自動で走ります。

  1. 取り込み — データソース(所有する Web サイトのクロール / R2 バケット)や Items API でのアップロードからコンテンツを読み込む
  2. Markdown 変換 — Workers AI の Markdown Conversion で、PDF や画像を含む多様な形式を構造化 Markdown に統一する
  3. チャンク分割 — 検索粒度を上げるために本文を小さなチャンクへ分割する
  4. 埋め込み — 各チャンクを埋め込みモデルでベクトル化する(キーワード検索有効時は BM25 用の索引も構築)
  5. 保存 — ベクトル・キーワード索引・本文を保存して検索可能にする

外部データソースは定期的に自動同期され、組み込みストレージへのアップロードは即時にインデックスされます。検索は 3 モードあり、既定はベクトル検索のみです。キーワード検索(BM25)や、両者を並走させて結果を融合するハイブリッド検索のほか、クエリ書き換え・リランキング・類似キャッシュも設定だけで有効化できます。

Worker からの binding は ai_search(単一インスタンス直結)と ai_search_namespaces(namespace 配下のインスタンスを実行時に作成・取得・削除できる)の 2 種類で、呼び出しの主役は次の 2 メソッドです。

Vectorize と AI Search の使い分けの目安です。

観点AI SearchVectorize
渡すものファイル、またはデータソースの接続自分で生成したベクトル
チャンク分割・埋め込み自動自分で実装する
インデックス更新自動・継続同期自分で upsert する
検索ベクトル + キーワード(ハイブリッド)、リランキング、metadata フィルタベクトル類似検索 + metadata フィルタ
回答生成組み込み(任意)含まれない(Workers AI 等を自分で呼ぶ)
向いている場面検索や RAG を素早く追加したい検索パイプラインを完全に制御したい
Vectorize + Workers AI — 4 手順を自分で書く AI Search — 1 回の呼び出しに畳まれる 質問 AI.run(埋め込み) VECTORIZE.query() 本文を文脈に整形 AI.run(生成) 回答 質問文 1024 次元 topK=3 のマッチ metadata.text 生成テキスト 質問 chatCompletions() 埋め込み・検索・文脈整形・生成 回答 質問を渡すだけ 回答 + 出典チャンク
図 12-3 入口(質問)と出口(回答)は同じで、違うのは間にある辺の数だけです。左は 4 段の処理と段と段のデータ受け渡しを自分で書くぶん、埋め込みモデル・topK・プロンプトを好きに差し替えられます。右はその 4 段がまるごと 1 本の呼び出しの内側に入るので、差し替えられるのは設定として露出している範囲だけになります。

料金は 2026-08 時点でオープンベータ中につき上限内で無料です(ストレージ・ベクトルインデックス・クロールは同梱。Workers AI と AI Gateway の利用分は別課金)。Workers Free では 100 インスタンス、月 20,000 クエリ、1 ファイル最大 4MB などの上限があります(Limits & pricing)。

ハンズオン:社内 FAQ ボットを 2 通りで作る

小さな社内 FAQ を題材に、まず Vectorize + Workers AI で RAG を手組みし(12-1、12-2)、次に同じものを AI Search で構築します(12-3)。埋め込みには日本語を扱える多言語モデル @cf/baai/bge-m3(1024 次元)を使います。

ハンズオン 12-1|Vectorize インデックスを作り FAQ を投入する

Worker プロジェクトと 1024 次元・cosine のインデックスを作成し、FAQ 4 件を埋め込みベクトルとして投入します。所要 15 分程度です。

  1. プロジェクトを作成する。対話プロンプトでは Hello World example → Worker only → TypeScript を選び、デプロイは No にする
    npm create cloudflare@latest -- faq-rag
    cd faq-rag
  2. Vectorize インデックスを作成する。bge-m3 の出力に合わせて 1024 次元、テキスト向けの cosine を指定する
    npx wrangler vectorize create faq-index --dimensions=1024 --metric=cosine
  3. カテゴリで絞り込めるよう、ベクトルを投入する前に metadata index を作成する
    npx wrangler vectorize create-metadata-index faq-index --property-name=category --type=string
  4. wrangler.jsonc に Vectorize と Workers AI の binding を追加する
    {
      "vectorize": [
        { "binding": "VECTORIZE", "index_name": "faq-index" }
      ],
      "ai": { "binding": "AI" }
    }
  5. src/index.ts を次の内容に置き換える(この時点では /insert のみ実装)
    export interface Env {
      VECTORIZE: Vectorize;
      AI: Ai;
    }
    
    interface EmbeddingResponse {
      shape: number[];
      data: number[][];
    }
    
    const faqs = [
      { id: "1", category: "hr", text: "有給休暇は入社6か月後に10日付与されます。申請は勤怠システムから行います。" },
      { id: "2", category: "hr", text: "経費精算の締め日は毎月末日で、翌月5営業日までに申請が必要です。" },
      { id: "3", category: "it", text: "社用PCの初期パスワードは情報システム部が配布し、初回ログイン時に変更します。" },
      { id: "4", category: "it", text: "VPNに接続できない場合は、クライアント証明書の有効期限を確認してください。" },
    ];
    
    export default {
      async fetch(request, env, ctx): Promise<Response> {
        const path = new URL(request.url).pathname;
    
        if (path === "/insert") {
          // FAQ 本文をまとめてベクトル化する
          const modelResp: EmbeddingResponse = await env.AI.run("@cf/baai/bge-m3", {
            text: faqs.map((faq) => faq.text),
          });
          // metadata に本文も同梱し、検索後のルックアップを不要にする
          const vectors: VectorizeVector[] = modelResp.data.map((values, i) => ({
            id: faqs[i].id,
            values,
            metadata: { category: faqs[i].category, text: faqs[i].text },
          }));
          const inserted = await env.VECTORIZE.upsert(vectors);
          return Response.json(inserted);
        }
    
        return Response.json({ text: "12-2 で検索を実装します" }, { status: 404 });
      },
    } satisfies ExportedHandler<Env>;
  6. デプロイして /insert にアクセスする
    npx wrangler deploy
    curl "https://faq-rag.<YOUR_SUBDOMAIN>.workers.dev/insert"

<YOUR_SUBDOMAIN> は自分の workers.dev サブドメイン(deploy 時の出力に表示される)に置き換えます。{"mutationId":"..."} が返り、数秒後に npx wrangler vectorize info faq-index で vectorCount が 4 になっていれば投入完了です。

ハンズオン 12-2|セマンティック検索と回答生成で手組み RAG を完成させる

質問をベクトル化して類似 FAQ を検索し、ヒットした本文をコンテキストとして LLM に回答させます。所要 10 分程度です。

  1. 12-1 の src/index.ts で 404 を返していた最後の行を、次の検索 + 生成処理に置き換える
        const question =
          new URL(request.url).searchParams.get("q") ?? "休暇は何日もらえますか?";
    
        // 1. 質問を FAQ と同じモデルでベクトル化する
        const qEmb: EmbeddingResponse = await env.AI.run("@cf/baai/bge-m3", {
          text: [question],
        });
    
        // 2. 類似検索。metadata に入れた本文も一緒に取得する
        const result = await env.VECTORIZE.query(qEmb.data[0], {
          topK: 3,
          returnMetadata: "all",
        });
    
        // 3. ヒットした FAQ 本文をコンテキストに整形する
        const context = result.matches
          .map((m) => `- ${m.metadata?.text}`)
          .join("\n");
    
        // 4. コンテキスト付きで回答を生成する
        const { response: answer } = await env.AI.run(
          "@cf/meta/llama-3.1-8b-instruct",
          {
            messages: [
              { role: "system", content: `Context:\n${context}` },
              { role: "system", content: "コンテキストが質問に関連する場合は、それに基づいて日本語で回答してください。" },
              { role: "user", content: question },
            ],
          },
        );
    
        return Response.json({ question, answer, sources: result.matches });
  2. 再デプロイして質問を投げる
    npx wrangler deploy
    curl -G "https://faq-rag.<YOUR_SUBDOMAIN>.workers.dev/" --data-urlencode "q=休暇は何日もらえますか"

レスポンスの answer に「入社 6 か月後に 10 日付与」に基づく回答が、sources に cosine スコア付きのマッチ(1 に近いほど類似)が入っていれば成功です。IT 系 FAQ だけを対象にしたい場合は query() のオプションに filter: { category: "it" } を追加すると、12-1 で作った metadata index による絞り込みが効きます。

ハンズオン 12-3|AI Search で同じ FAQ ボットをマネージドに作る

チャンク分割も埋め込みも自分では書かず、FAQ の Markdown をアップロードするだけで検索と回答生成が動くことを確認します。所要 15 分程度です。

  1. 新しいプロジェクトを作成する(選択肢は 12-1 と同じ)
    npm create cloudflare@latest -- faq-ai-search
    cd faq-ai-search
  2. wrangler.jsonc に namespace binding を追加する。AI Search はローカルでは動作しないため、remote: true を付けて wrangler dev からデプロイ済みインスタンスへプロキシさせる
    {
      "ai_search_namespaces": [
        { "binding": "AI_SEARCH", "namespace": "default", "remote": true }
      ]
    }
  3. src/index.ts を次の内容に置き換える。/setup がインスタンス作成と FAQ 投入、それ以外のパスが質問への回答を担当する
    export interface Env {
      AI_SEARCH: AiSearchNamespace;
    }
    
    const faqMarkdown = [
      "# 社内 FAQ",
      "## 有給休暇",
      "入社6か月後に10日付与されます。申請は勤怠システムから行います。",
      "## 経費精算",
      "締め日は毎月末日で、翌月5営業日までに申請が必要です。",
      "## VPN 接続",
      "接続できない場合は、クライアント証明書の有効期限を確認してください。",
    ].join("\n\n");
    
    export default {
      async fetch(request, env): Promise<Response> {
        const url = new URL(request.url);
    
        // 初回のみ: インスタンスを作成して FAQ をアップロードする
        if (url.pathname === "/setup") {
          const instance = await env.AI_SEARCH.create({ id: "faq-search" });
          const item = await instance.items.uploadAndPoll("faq.md", faqMarkdown);
          return Response.json({ created: "faq-search", status: item.status });
        }
    
        // 検索 + 回答生成をまとめて実行する
        const question = url.searchParams.get("q") ?? "休暇は何日もらえますか?";
        const response = await env.AI_SEARCH.get("faq-search").chatCompletions({
          messages: [{ role: "user", content: question }],
          ai_search_options: { retrieval: { max_num_results: 3 } },
        });
    
        return Response.json({
          question,
          answer: response.choices[0].message.content,
          sources: response.chunks.map((chunk) => chunk.item.key),
        });
      },
    } satisfies ExportedHandler<Env>;
  4. デプロイし、/setup に一度だけアクセスしてから質問する
    npx wrangler deploy
    curl "https://faq-ai-search.<YOUR_SUBDOMAIN>.workers.dev/setup"
    curl -G "https://faq-ai-search.<YOUR_SUBDOMAIN>.workers.dev/" --data-urlencode "q=VPNにつながらない"

/setup は {"created":"faq-search","status":"completed"} を返し、質問には answer(生成された回答)と sources(出典ファイル名 faq.md)が返ります。CLI からも npx wrangler ai-search stats faq-search でインデックス状況の確認、npx wrangler ai-search search faq-search --query "..." で検索を試せます。

つまずきポイント

挿入した直後にクエリしても結果が 0 件になる

Vectorize の insert / upsert は非同期で、戻り値の mutationId は「受け付けた」ことしか意味しません。検索に反映されるまで通常数秒かかります。npx wrangler vectorize info <インデックス名> の processedUpToMutation でどのミューテーションまで処理済みかを確認してから検証しましょう。

metadata フィルタが効かない

metadata index は「作成後に投入されたベクトル」だけを索引するため、ベクトル投入後に create-metadata-index を実行しても既存ベクトルには効きません。インデックス作成直後・ベクトル投入前に作るのが原則で、後から追加した場合は該当ベクトルの再 upsert が必要です。

埋め込みとインデックスの「つなぎ目」でエラーになる

Workers AI の埋め込みモデルが返すのは { shape, data } のラッパーオブジェクトで、ベクトル本体は data[0] にあります。query(qEmb) や query(qEmb.data) はエラーになるため、必ず query(qEmb.data[0]) の形で渡します。また、インデックスの dimensions と渡すベクトルの次元数(bge-m3 なら 1024)は一致している必要があります。

wrangler dev で AI Search が動かない

AI Search はローカル実行をサポートしていません。ローカル開発はデプロイ済みインスタンスへのプロキシで実現されるため、binding 設定に remote: true を付け忘れないようにします。

理解度チェック

Q1. Vectorize インデックスの dimensions と metric を後から変更するには?

変更できません。新しい設定でインデックスを作り直し、全ベクトルを再生成して投入し直す必要があります。埋め込みモデルを決めてからインデックスを作るのが原則です。

Q2. 既存の ID と同じ ID のベクトルを insert した場合と upsert した場合、それぞれどうなる?

insert は先勝ちで、既存ベクトルが残り新しい値は反映されません。upsert は後勝ちで、既存ベクトルが丸ごと置き換わります(値も metadata もマージではなく全置換)。既存データの更新には upsert を使います。

Q3. AI Search の search() と chatCompletions() の違いは?

search() は関連チャンクをスコア付きで返す検索 API で、回答は生成しません。chatCompletions() は検索したチャンクをコンテキストにテキスト生成モデルが回答まで生成します。自前 UI の検索には search()、RAG チャットボットには chatCompletions() を使います。

Q4. Vectorize で「特定テナントのベクトルだけを検索対象にする」2 つの方法と、その適用順は?

namespace と metadata フィルタです。namespace は metadata フィルタより先に適用されます。1 軸の分離は namespace、複数条件の絞り込みは metadata index + filter が向いています。

まとめと次章へ

次章では、この章でも多用した AI 呼び出しに可観測性を与える AI Gateway と、状態を持つ AI エージェントを構築する Agents を扱います。