Cloudflare:Vectorize
2026年7月1日仕組み
テキストをVector(= 数字の配列)として扱い、テキストの部分一致ではなく意味の近さで検索できるようにするのが目的
流れとしてはWorkersでテキストを受取りVectorに変換、Vectorにidやmetadataを付与してインデックスに保管
用語:
Vetorizeに保存しているデータは 「Vector」
DBのテーブル/コレクション相当のものは 「Index」
注意点や詰まった部分:
テーブルの中身を見ることが出来ないので作成・更新されたのかがわかりづらい。
npx wrangler vectorize listでcreatedとmodifiedに差が生じていれば操作自体は成功している事に気付ける。
テキストを分割してVectorにするが「渡した
idと同じものを全て削除・更新」ということが出来ないので削除・更新ロジックが複雑になる。
npx wrangler vectorize create インデックス名 \
--dimensions 1024 \
--metric "cosine" \
--description "インデックスの説明" \
--update-config次元数は自由に決めるのではなくモデル側の仕様に合わせる。
使いたい埋め込みモデルを決める → 出力次元数を確認 → Vectorizeの
dimensionsを同じ値にする
384 , 768 , 1024 、数字が上がる毎に精度は上がる一方で検索速度やコストが上昇
// 削除
npx wrangler vectorize delete インデックス名
// 確認
npx wrangler vectorize listwrangler.jsonc :
"ai": {
"binding": "AI"
},
"vectorize": [
{
"binding": "VECTORIZE",
"index_name": "インデックス名",
"remote": true
}
]Vectorの生成
まずはクライアント等から受けたtextをVectorに変換
const embedding = await env.AI.run('@cf/baai/bge-m3', { text: texts });日本語を使う場合は多言語対応のモデルを使う。
次にVectorをインデックスに保管
await env.VECTORIZE.upsert([{ id: 'example-id', values: embeddings.data[0], metadata: { info: 'example-metadata' } }]);insertではなくupsertを使う方が良い。valuesにテキストから生成されたVectorを入れる。idは自分で付与する必要がある。metadataは検索時に一緒に返されるのであったほうが良い。
chunk
Vector生成したいテキストが長い場合は数分割にする。
const chunks = chunkText(content);
const embedding = await env.AI.run('@cf/baai/bge-m3', { text: chunks });// chunkText
export function chunkText(text: string, chunkSize = 800): string[] {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
chunks.push(text.slice(start, start + chunkSize));
start += chunkSize;
}
return chunks;
}アプリ側から受け取った
contentをchunkText関数で800文字毎に分割してVector生成
await env.VECTORIZE.upsert(
embedding.data.map((vec: number[], i: number) => ({
id: `${memoId}-chunk-${i}`,
values: vec,
metadata: { index: i, memoId },
}))
);重要:Chunkに分けたVectorは同一テキストから発生したことを
id・metadataに記しておく。取得・削除・更新の為
metadataには約3000文字以上は保存が出来ないので、実データが長文の場合はDBに分離する事になる。
Vectorの検索
アプリ側から受け取ったテキストから合致するVectorを取得
まず受け取ったqueryをVectorに変換
const embedding = await env.AI.run('@cf/baai/bge-m3', { text: [query] });次にインデックスから意味的に近いものを取得
const results = await env.VECTORIZE.query(embedding.data[0], { topK: 8, returnMetadata: true });topK:意味的に近いものを上位から何個取得するか
filter
アプリ側の検索窓からVector検索をするとして、Vectorの数字配列データが欲しいわけではなく欲しいのはSearhInputなどに入力されたqueryとアプリ側のどのデータが近いのかという情報なので、返すべきはインデックス保管時にmetadataに設定したid等のDBへ問い合わせるための識別子。*重要:3000文字以内であればmetadataに直接テキストを保存する方が楽
const memoIds = [
...new Set(
results.matches
.filter(
(m) => m.score !== undefined && m.score >= 0.6 && m.metadata?.memoId
)
.map((m) => m.metadata?.memoId)
),
];上記は意味的に近かった8件の中から、さらに
scoreが 0.6 以上だったものをインデックスから選ぶ...new Set:アプリ側に返すmemoIdは重複が必要無いため
*Vectorの削除
重要:
Vectorの削除には自分で作成した id を正確に指定しないといけない。
metadataの識別子を使って削除することは出来ない。問題になるのはVectorをchunkに分けて保存している場合
対策:
const { memoId, chunkCount } = await req.json();
const idsToDelete = Array.from(
{ length: chunkCount },
(_, i) => `${memoId}-chunk-${i}`
);
await env.VECTORIZE.deleteByIds(idsToDelete);アプリ側から削除したいデータのChunkCountを計算して渡すことにより、一意の
memoIdが持つ Vector(${memoId}-chunk-${i})を全て削除できる。アプリ側のテキストサイズの変更をVectorに同期しないとchunk数が噛み合わないので注意*chunkの長さが10を超えることは無いだろうという前提で
Array.form({ length:10 })のようにして削除を試行することは出来ない。存在しないidにアクセスすると機能しない為*
${memoId}-chunkのようなidにすればIndex数の把握は必要無くなるが、id は一意である必要があるのでそれは出来ない。
*Vectorの更新
アプリから受け取った識別子( memoId, postId)を持つVectorを全て削除したのち新しいVectorを作成する。
const idsToDelete = Array.from(
{ length: currentChunks },
(_, i) => `${memoId}-chunk-${i}`
);
await env.VECTORIZE.deleteByIds(idsToDelete);
const chunks = chunkText(content);
const embedding: any = await env.AI.run("@cf/baai/bge-m3", { text: chunks });
await env.VECTORIZE.upsert(
embedding.data.map((vec: number[], i: number) => ({
id: `${memoId}-chunk-${i}`,
values: vec,
metadata: { memoId, index: i },
}))
);エラーに詰まった場合
npx wrangler tail上記コマンドを実行すると、 index.tsに来たリクエストの成功や失敗、エラー原因等がリアルタイムでターミナルに表示される。
✘ [ERROR] Error: VECTOR_UPSERT_ERROR (code = 40012): invalid vector for id="155-chunk-0", expected 1536 dimensions, and got 1024 dimensions;