- ベクトル埋め込みと類似度検索の仕組み、距離メトリクスの選び方
- Vectorize のインデックス作成(次元・metric)と insert / upsert / query / metadata フィルタ
- Workers AI の埋め込みモデルと組み合わせた「手組み RAG」の構築
- AI Search(旧 AutoRAG)によるマネージド RAG パイプラインの構築
- 社内 FAQ へのセマンティック検索 + 回答生成を 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 は直交 |
euclidean | L2 距離。0 が同一で、正の値が大きいほど遠い |
dot-product | 負の内積。負の絶対値が大きいほど、また正なら小さいほど類似 |
公式ドキュメントでは、テキスト・文書検索には cosine、画像や音声認識には euclidean が適するとされています。
RAG(Retrieval Augmented Generation)は、この類似検索を LLM と組み合わせる手法です。プロンプトをそのまま LLM に渡すのではなく、次の流れで自前データの文脈を注入します。
- ドキュメント群を埋め込みモデルでベクトル化し、ベクトルデータベースに保存する(事前準備)
- ユーザーの質問を同じモデルでベクトル化する
- 質問ベクトルで類似検索し、意味が近いドキュメントを取得する
- 取得した内容をコンテキストとして質問と一緒に LLM へ渡し、回答を生成させる
このパイプラインは、ベクトルデータベース Vectorize で各段階を自分で実装する方法と、AI Search にデータを渡して全段階を任せる方法の 2 通りで組めます。本章では両方を実装して違いを体感します。
Vectorize — 自分で組むベクトル検索
Vectorize は Cloudflare のベクトルデータベースで、Workers Free / Paid どちらのプランでも利用できます。データは「インデックス」単位で管理し(ベクトルの投入先も類似検索の対象も 1 つのインデックス)、作成時に次の 3 つを決めます。
- 名前 — 小文字英数とダッシュの組み合わせで 32 文字未満・先頭は英字が推奨(例:
prod-search-index) - dimensions — 格納するベクトルの次元数。使う埋め込みモデルの出力次元に合わせる
- metric —
cosine/euclidean/dot-productのいずれか
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 フィルタと使い分けます。
filter を付けても返るのは topK 件のままで、「類似上位 100 件を取ってから絞り込む」のではありません(件数は理解のための例)。料金は「クエリしたベクトル次元数」と「保存したベクトル次元数」の 2 軸で課金され、2026-08 時点では Workers Free でも月 3,000 万クエリ次元 + 500 万保存次元まで無料です(Pricing)。1 ベクトルの最大次元数は 1536、1 インデックスあたり最大 2,000 万ベクトルという制限もあります(2026-08 時点、Limits)。
AI Search — マネージド RAG パイプライン
AI Search(旧称 AutoRAG)は、前節までの各段階をまるごと引き受けるマネージド検索サービスで、全プランで利用できます。内部では R2 と Vectorize による組み込みストレージとベクトルインデックスが自動でプロビジョニングされ、データを渡すと次のインデックス処理が自動で走ります。
- 取り込み — データソース(所有する Web サイトのクロール / R2 バケット)や Items API でのアップロードからコンテンツを読み込む
- Markdown 変換 — Workers AI の Markdown Conversion で、PDF や画像を含む多様な形式を構造化 Markdown に統一する
- チャンク分割 — 検索粒度を上げるために本文を小さなチャンクへ分割する
- 埋め込み — 各チャンクを埋め込みモデルでベクトル化する(キーワード検索有効時は BM25 用の索引も構築)
- 保存 — ベクトル・キーワード索引・本文を保存して検索可能にする
外部データソースは定期的に自動同期され、組み込みストレージへのアップロードは即時にインデックスされます。検索は 3 モードあり、既定はベクトル検索のみです。キーワード検索(BM25)や、両者を並走させて結果を融合するハイブリッド検索のほか、クエリ書き換え・リランキング・類似キャッシュも設定だけで有効化できます。
Worker からの binding は ai_search(単一インスタンス直結)と ai_search_namespaces(namespace 配下のインスタンスを実行時に作成・取得・削除できる)の 2 種類で、呼び出しの主役は次の 2 メソッドです。
search()— 関連チャンクをスコア付きで返す。検索エンジンとして使うchatCompletions()— 検索したチャンクをコンテキストに、テキスト生成モデルが回答まで生成して返す。stream: trueで SSE ストリーミングにも対応
Vectorize と AI Search の使い分けの目安です。
| 観点 | AI Search | Vectorize |
|---|---|---|
| 渡すもの | ファイル、またはデータソースの接続 | 自分で生成したベクトル |
| チャンク分割・埋め込み | 自動 | 自分で実装する |
| インデックス更新 | 自動・継続同期 | 自分で upsert する |
| 検索 | ベクトル + キーワード(ハイブリッド)、リランキング、metadata フィルタ | ベクトル類似検索 + metadata フィルタ |
| 回答生成 | 組み込み(任意) | 含まれない(Workers AI 等を自分で呼ぶ) |
| 向いている場面 | 検索や RAG を素早く追加したい | 検索パイプラインを完全に制御したい |
料金は 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 次元)を使います。
Worker プロジェクトと 1024 次元・cosine のインデックスを作成し、FAQ 4 件を埋め込みベクトルとして投入します。所要 15 分程度です。
- プロジェクトを作成する。対話プロンプトでは
Hello World example→Worker only→TypeScriptを選び、デプロイはNoにするnpm create cloudflare@latest -- faq-rag cd faq-rag - Vectorize インデックスを作成する。bge-m3 の出力に合わせて 1024 次元、テキスト向けの cosine を指定する
npx wrangler vectorize create faq-index --dimensions=1024 --metric=cosine - カテゴリで絞り込めるよう、ベクトルを投入する前に metadata index を作成する
npx wrangler vectorize create-metadata-index faq-index --property-name=category --type=string wrangler.jsoncに Vectorize と Workers AI の binding を追加する{ "vectorize": [ { "binding": "VECTORIZE", "index_name": "faq-index" } ], "ai": { "binding": "AI" } }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>;- デプロイして
/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 になっていれば投入完了です。
質問をベクトル化して類似 FAQ を検索し、ヒットした本文をコンテキストとして LLM に回答させます。所要 10 分程度です。
- 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 }); - 再デプロイして質問を投げる
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 による絞り込みが効きます。
チャンク分割も埋め込みも自分では書かず、FAQ の Markdown をアップロードするだけで検索と回答生成が動くことを確認します。所要 15 分程度です。
- 新しいプロジェクトを作成する(選択肢は 12-1 と同じ)
npm create cloudflare@latest -- faq-ai-search cd faq-ai-search wrangler.jsoncに namespace binding を追加する。AI Search はローカルでは動作しないため、remote: trueを付けてwrangler devからデプロイ済みインスタンスへプロキシさせる{ "ai_search_namespaces": [ { "binding": "AI_SEARCH", "namespace": "default", "remote": true } ] }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>;- デプロイし、
/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 "..." で検索を試せます。
つまずきポイント
Vectorize の insert / upsert は非同期で、戻り値の mutationId は「受け付けた」ことしか意味しません。検索に反映されるまで通常数秒かかります。npx wrangler vectorize info <インデックス名> の processedUpToMutation でどのミューテーションまで処理済みかを確認してから検証しましょう。
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)は一致している必要があります。
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 が向いています。
まとめと次章へ
- ベクトル埋め込みは意味を数値化した表現で、距離メトリクス(cosine / euclidean / dot-product)で類似度を測る。RAG は類似検索の結果をコンテキストとして LLM に渡す手法
- Vectorize は dimensions と metric を固定して作るベクトルデータベース。insert / upsert / query を自分で呼び、metadata index を事前に作ればフィルタ付き検索もできる
- Workers AI の埋め込みモデル(bge-m3 など)と組み合わせると、埋め込み生成から回答生成まで Worker 内で完結する手組み RAG が作れる
- AI Search は Markdown 変換・チャンク分割・埋め込み・継続同期・検索・回答生成までを引き受けるマネージド RAG。search() と chatCompletions() で Worker から数行で使える
- 検索パイプラインを細かく制御したいなら Vectorize、素早く RAG を組みたいなら AI Search が出発点
次章では、この章でも多用した AI 呼び出しに可観測性を与える AI Gateway と、状態を持つ AI エージェントを構築する Agents を扱います。