Vectorize 向量資料庫入門:語意搜尋的第一塊拼圖 | Cloudflare 完整教學
Cloudflare Vectorize 是一個與 Workers 深度整合的全球分散式向量資料庫(vector database),專為儲存與查詢 embedding(向量嵌入) 而生。有了它,你就能在邊緣運算環境裡做語意搜尋——不是比對關鍵字,而是比對「意思」。這一篇我們從「什麼是向量、什麼是相似度」講起,教你用
wrangler建立 index、設定 binding,並跑一遍insert、upsert、query、getByIds、deleteByIds、metadata 過濾與 namespaces。
前言
上一篇《AI Gateway》我們替所有 AI 請求掛上了營運層——快取、限流、可觀測——讓 Workers AI 從「能跑」邁向「可上線」。但在那幾篇裡,有一個詞反覆出現卻一直沒好好交代:embedding。我們一直說「把文字轉成向量、做語意搜尋、做 RAG」,卻還沒回答一個最基本的問題:那些向量到底該存在哪裡、怎麼查最快?
這就是 Vectorize 登場的時刻。
Vectorize 是 Cloudflare 推出的全球分散式向量資料庫(vector database),和 Workers、Workers AI 同屬一個平台、透過 binding 直接呼叫。它專門解決一件事:儲存大量的向量(embedding),並在毫秒級找出「與查詢向量最相似」的那幾筆。傳統資料庫擅長「精確比對」(WHERE title = ‘Cloudflare’),向量資料庫擅長的則是**「語意相似」**——就算你搜「邊緣運算的冷啟動」,它也能找出寫著「Workers 啟動延遲」的文件,因為兩者「意思接近」。
打個比方。傳統關聯式資料庫像一本字典:你得知道確切的詞才查得到。向量資料庫則像一位博學的圖書館員:你描述「我想找一本講在網路邊緣跑程式、而且啟動很快的書」,他不需要你講出書名,就能憑「意思」把最貼近的幾本抽給你。而 embedding 就是把每一段文字、每一張圖,翻譯成這位圖書館員腦中「意思的座標」——一串數字,讓「相近的意思」落在空間中「相近的位置」。
讀完你會掌握:
- 向量與相似度的直覺——什麼是 embedding、什麼是向量相似度,以及
cosine/euclidean/dot-product三種度量該怎麼選。 - 建立 index 與 binding——用
wrangler vectorize create指定維度與度量,並在wrangler.jsonc綁定。 - 五個核心操作——
insert、upsert、query、getByIds、deleteByIds各自的用途與陷阱。 - 精準過濾——用 metadata filtering、namespaces 與
topK把搜尋結果收斂到你要的範圍。
本篇是 Vectorize 的第一篇,聚焦「把向量存好、查準」;至於把它接上 Workers AI 與 LLM、組成完整的 RAG 問答系統,我們留給下一篇。
核心概念
在寫任何一行程式碼之前,先把「向量資料庫在做什麼」這件事想清楚。理解 Vectorize 的關鍵只有一句話:它把「意思」變成空間中的「座標」,而搜尋就是「找出離你最近的鄰居」。
一、從 embedding 到向量相似度
embedding(向量嵌入) 是一個 AI 模型(embedding model)的輸出:輸入一段文字,輸出一串固定長度的浮點數,例如 [0.12, -0.45, 0.67, ...]。這串數字有多長,就叫這個向量的維度(dimensions)。以 Workers AI 的 @cf/baai/bge-base-en-v1.5 為例,它輸出的是 768 維的向量。
關鍵在於:模型被訓練成讓**「意思相近的文字」對應到「空間中相近的向量」**。「貓」和「狗」的向量會靠得很近,「貓」和「量子力學」則會離得很遠。於是「語意搜尋」就變成一個幾何問題:把查詢也轉成向量,然後在空間裡找出離它最近的那幾個點。 這就是向量資料庫的全部精髓——最近鄰搜尋(nearest-neighbor search)。
那「近」怎麼定義?這就是距離度量(metric) 要回答的。想像一個三維空間裡散落的點雲,每個點是一份文件;你的查詢也是一個點,系統要衡量「查詢點」和「每個文件點」之間有多接近:
● doc-A(0.83) ← 分數高 = 最相似
╱
查詢向量 ●───── ● doc-B(0.79)
╲
● doc-C(0.41) ← 分數低 = 較不相似
query() 回傳依相似度排序的 topK 個最近鄰
二、三種距離度量怎麼選
Vectorize 支援三種距離度量,建立 index 時就要決定,之後無法更改。逐條看它們的差異與適用場景:
| 度量 metric | 相似 = 分數高? | 典型範圍 | 最適合 |
|---|---|---|---|
cosine(餘弦相似度) | 是(高 = 相似) | 0.0 ~ 1.0 | 文字語意搜尋、問答系統 |
euclidean(歐幾里德距離) | 否(低 = 相似) | 0.0 ~ ∞ | 圖像相似度、空間座標資料 |
dot-product(點積) | 是(高 = 相似) | -∞ ~ ∞ | 推薦系統、已正規化的向量 |
cosine只比較兩個向量的「方向(夾角)」,忽略長度。這正是文字語意搜尋最想要的——我們在意「意思像不像」,不在意「文字長不長」。不確定選哪個時,選 cosine 準沒錯。euclidean算的是兩點在空間中的「直線距離」,對向量的絕對位置敏感,適合圖像相似度或空間資料。注意它是分數越低越相似(距離越短)。dot-product算內積,兼顧方向與強度,常用於推薦系統或已預先正規化(normalized)的向量。
一句話總結:做文字 / RAG → cosine;做圖像 / 空間 → euclidean;做推薦 → dot-product。
三、關鍵術語一次記牢
Vectorize 有幾個名詞會貫穿全文,先建立對照:
| 術語 | 是什麼 |
|---|---|
| index(索引) | 一個向量資料庫的容器,建立時綁定固定的 dimensions 與 metric |
| vector(向量) | 一筆資料,含 id(必填)、values(必填,長度 = 維度)、選填的 namespace 與 metadata |
| metadata | 附加在向量上的鍵值資料(如 category、isPublished),可拿來過濾 |
| namespace | 最輕量的分區,在向量搜尋「之前」過濾,適合多租戶隔離 |
| topK | query 最多回傳幾個最近鄰,預設 5、最大 100 |
| metric | 距離度量:cosine / euclidean / dot-product |
有幾個規格上限先放在心裡:單一 index 最多 2,000 萬個向量、維度上限 1536、每個 index 最多 10 個 metadata index、每筆 metadata 上限 10 KiB、向量 id 上限 64 bytes。這些數字目前(Vectorize V2,已 GA)對絕大多數應用都綽綽有餘。
實作範例
觀念清楚後,我們把一個最小可用的語意搜尋流程從頭跑一遍:(1) 用 CLI 建立 index 與 binding、(2) 寫入向量、(3) 查詢 topK、(4) 加上 metadata 過濾與 namespaces、(5) 用 getByIds / deleteByIds 管理資料。所有 TypeScript 範例的型別 Ai、Vectorize、VectorizeVector 皆來自 @cloudflare/workers-types。
1. 用 CLI 建立 index:指定維度與度量
第一步是用 Wrangler 建立一個向量索引。因為我們打算用 Workers AI 的 @cf/baai/bge-base-en-v1.5(輸出 768 維)做 embedding,所以 --dimensions 就是 768;文字語意搜尋用 --metric=cosine:
# 語法:wrangler vectorize create <index-name> --dimensions=<N> --metric=<metric>
# 建立 768 維 cosine 索引(文字語意搜尋最推薦)
npx wrangler vectorize create my-docs-index --dimensions=768 --metric=cosine
<index-name> 的命名規則:只能用小寫英文字母、數字、連字號,必須以字母開頭,長度上限 64 字元。其他常用的管理指令:
npx wrangler vectorize list # 列出所有索引
npx wrangler vectorize info my-docs-index # 查看索引維度、度量、向量數
npx wrangler vectorize delete my-docs-index # 刪除索引
再次強調(因為太重要):index 建立後,
dimensions與metric都無法修改。要改只能建立新 index 並遷移資料。動手前先確認好 embedding 模型的維度。
2. 設定 binding:讓 Worker 拿得到 index
在 wrangler.jsonc 裡把 index 綁定成一個變數。這裡順便把 Workers AI(ai binding)也綁上,因為我們要用它生成 embedding:
// wrangler.jsonc
{
"name": "semantic-search-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"ai": { "binding": "AI" },
"vectorize": [
{
"binding": "VECTORIZE", // 程式碼中透過 env.VECTORIZE 存取
"index_name": "my-docs-index" // 對應剛才 create 的索引名稱
}
]
}
對應的 TypeScript 型別定義:
export interface Env {
AI: Ai;
VECTORIZE: Vectorize;
}
3. 寫入向量:insert vs upsert
寫入是最容易踩雷的地方,先講清楚兩個方法的差別:
insert:插入新向量。若相同id已存在,保留舊資料、忽略這次的新資料。upsert:插入或更新。若相同id已存在,完全覆蓋舊資料;不存在則新增。
下面示範用 Workers AI 把文字轉成 embedding,再 upsert 進 Vectorize。注意 env.AI.run() 回傳的 result.data 是二維陣列,data[0] 才是那一段文字的向量——這是新手最常傳錯的地方:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const docs = [
{ id: "doc-001", title: "Cloudflare Workers 入門", text: "Workers 是在網路邊緣執行的無伺服器運算平台,冷啟動極快。" },
{ id: "doc-002", title: "什麼是邊緣運算", text: "邊緣運算把程式碼部署到離使用者最近的節點,降低延遲。" },
];
// 批次生成 embedding:一次傳入多段文字,效率遠優於逐一請求
const embResult = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: docs.map((d) => d.text),
});
// 組成 VectorizeVector 陣列;values 長度必須等於索引維度(768)
const vectors: VectorizeVector[] = docs.map((d, i) => ({
id: d.id,
values: embResult.data[i], // ← 對應 docs[i] 的向量,不是 result 也不是 result.data
metadata: {
title: d.title,
category: "tutorial",
isPublished: true,
},
}));
// upsert 較安全:不確定 id 是否存在、或更新後重算 embedding 時都用它
const mutation = await env.VECTORIZE.upsert(vectors);
return Response.json({ mutationId: mutation.mutationId });
},
} satisfies ExportedHandler<Env>;
資料量大時要分批寫入——Workers API 每次最多 1,000 個向量,而且批次遠比逐一寫入快:
const BATCH_SIZE = 1000;
for (let i = 0; i < vectors.length; i += BATCH_SIZE) {
await env.VECTORIZE.upsert(vectors.slice(i, i + BATCH_SIZE));
}
寫入是非同步、最終一致性的:
upsert立即回傳mutationId,但向量通常要等 5~10 秒才可被查詢。別在寫入後馬上 query 就期待看到最新資料。
4. 查詢:query 與 topK
查詢是 Vectorize 的核心。把「查詢文字」轉成向量,傳給 query(),它就回傳最相似的 topK 個結果(依分數由高到低排序):
async function search(env: Env, question: string) {
// Step 1:把問題轉成查詢向量(同一個 embedding 模型!)
const emb = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [question] });
const queryVector = emb.data[0];
// Step 2:找出最相似的 topK 個向量
const result = await env.VECTORIZE.query(queryVector, {
topK: 5, // 最多回傳幾個結果(預設 5,最大 100)
returnMetadata: "indexed", // "none" | "indexed" | "all"
returnValues: false, // 通常不需要把向量值原封帶回
});
// result.matches 依 score 由高到低排序
return result.matches.map((m) => ({
id: m.id,
score: m.score, // cosine:0~1,越高越相似
title: m.metadata?.title,
}));
}
returnMetadata 有三個選項,直接影響速度與 topK 上限:
| 選項 | 回傳什麼 | topK 上限 | 速度 |
|---|---|---|---|
"none" | 不回傳 metadata,只有 id 與 score | 100 | 最快 |
"indexed" | 回傳「已建 index」的 metadata(字串截至 64 bytes) | 100 | 快 |
"all" | 回傳完整 metadata | 50 | 較慢 |
日常首選 "indexed",在速度與資料量之間平衡最佳;只要 id 與分數時用 "none"。要注意:一旦用 "all" 或 returnValues: true,topK 上限會從 100 降為 50——這正是「明明設了 topK: 80 卻只拿回 50 筆」的元兇。
5. 精準過濾:metadata filtering 與 namespaces
只靠相似度往往不夠——你可能只想在「已發布的教學文章」裡搜尋。Vectorize 提供兩層過濾機制。
第一層:metadata index(必須先建立)。 想用某個 metadata 欄位過濾,就得在寫入向量之前,用 CLI 針對該欄位建立 metadata index:
# 針對 category(字串)與 isPublished(布林)建立 metadata index
npx wrangler vectorize create-metadata-index my-docs-index --property-name=category --type=string
npx wrangler vectorize create-metadata-index my-docs-index --property-name=isPublished --type=boolean
建好之後,query 就能帶 filter。過濾在相似度計算「之前」執行,確保回傳的 topK 全部符合條件:
const result = await env.VECTORIZE.query(queryVector, {
topK: 10,
returnMetadata: "indexed",
filter: {
category: "tutorial", // 等值(隱式 $eq)
isPublished: true,
},
});
支援的運算子:$eq(隱式)、$ne、$in、$nin、$lt、$lte、$gt、$gte。例如做範圍與集合過濾:
const result = await env.VECTORIZE.query(queryVector, {
topK: 20,
returnMetadata: "indexed",
filter: {
category: { $in: ["tutorial", "guide"] }, // 屬於這幾類
publishedAt: { $gte: 1704067200 }, // 2024 之後
status: { $ne: "archived" }, // 排除已封存
},
});
第二層:namespaces(最輕量的分區)。 namespace 在向量搜尋「之前」就先把範圍縮到某個分區,比 metadata 過濾更快,最適合多租戶隔離——例如以使用者 ID 當 namespace,查詢時就只在該用戶的資料裡搜:
// 寫入時指定 namespace
await env.VECTORIZE.upsert([
{ id: "note-1", values: queryVector, namespace: "user-12345", metadata: { title: "我的筆記" } },
]);
// 查詢時限定 namespace(區分大小寫!)
const result = await env.VECTORIZE.query(queryVector, {
topK: 10,
namespace: "user-12345", // 只在這個用戶的資料中搜尋
});
6. 精確取用與刪除:getByIds 與 deleteByIds
有時候你不是要「找相似」,而是要「精確拿到某幾筆」或「刪掉某幾筆」:
// getByIds:精確取得指定 ID 的向量(含 values 與 metadata)
// 若某個 ID 不存在,不會報錯,只是結果裡不包含它
const got = await env.VECTORIZE.getByIds(["doc-001", "doc-002"]);
// deleteByIds:刪除指定向量,一次最多 1,000 個(同樣是最終一致性)
await env.VECTORIZE.deleteByIds(["doc-001", "doc-002"]);
要刪大量資料時,一樣分批處理:
const IDS_PER_BATCH = 1000;
for (let i = 0; i < idsToDelete.length; i += IDS_PER_BATCH) {
await env.VECTORIZE.deleteByIds(idsToDelete.slice(i, i + IDS_PER_BATCH));
}
常見錯誤與最佳實踐
Vectorize 的坑幾乎都集中在「維度」「時序」「metadata index」這三件事上。以下是最高頻的幾個。
坑一:維度不符,寫入或查詢直接失敗 / 分數異常。
values 的長度必須等於建立 index 時指定的 dimensions。最常見的失誤是「建 index 用了 768 維,卻換了另一個輸出 384 維的 embedding 模型」,或「查詢向量與索引向量來自不同模型」。務必確保索引維度、寫入向量、查詢向量三者的 embedding 模型完全一致。如果查詢分數莫名其妙地低,第一個要懷疑的就是這裡。
// ❌ 索引是 768 維,卻傳入不同維度的向量 → 失敗
await env.VECTORIZE.upsert([{ id: "x", values: some384DimVector }]);
// ✅ 用與建立 index 時相同的模型(768 維)產生向量
const emb = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: ["..."] });
await env.VECTORIZE.upsert([{ id: "x", values: emb.data[0] }]);
坑二:寫入後立刻查詢,查不到剛寫的資料。
如前所述,寫入是最終一致性,需要 5~10 秒才可見。不要在 insert / upsert 之後同一個請求裡 query 期待看到新資料。若流程真的需要「寫入 → 立刻查」,把索引與查詢拆成兩個階段,或用 Cloudflare Workflows 處理非同步索引,避開一致性延遲。
坑三:忘了先建 metadata index,過濾完全沒作用。
metadata 過濾要生效,必須在寫入向量之前用 create-metadata-index 建好對應欄位;而且它只索引「建立之後」寫入的向量,已存在的向量不會被回溯索引。所以正確順序永遠是:create index → create-metadata-index(所有要過濾的欄位)→ 才開始寫入。若你事後才補建 metadata index,那批舊資料必須全部重新 upsert 一次才能被過濾到。
坑四:metric 選錯,結果排序不合直覺。
用 euclidean 卻以為「分數越高越相似」,結果拿到一堆最不相關的——因為 euclidean 是越低越相似。文字語意搜尋請一律用 cosine(越高越相似、範圍 0~1),除非你很清楚自己在做圖像或空間資料。而且別忘了 metric 建立後無法更改。
坑五:用 insert 更新資料,卻發現資料沒變。
insert 遇到已存在的 id 會忽略新資料。更新文件、重算 embedding 後想覆蓋,請用 upsert。不確定 id 存不存在時,也一律用 upsert 較安全。
最佳實踐小結: 建 index 前先確認 embedding 模型維度、選對 metric(文字→cosine);metadata index 一定在寫入前建好;寫入永遠批次(每批 ≤1,000)並用 upsert;查詢預設 returnMetadata: "indexed",需要更快就用 "none";多租戶優先用 namespace 過濾(比 metadata 快);高基數欄位(如毫秒時間戳)用「分桶」降低基數;接受寫入的最終一致性延遲,必要時用 Workflows 拆分索引與查詢。
小結
上一篇《AI Gateway》我們替所有 AI 請求掛上了快取、限流與可觀測的營運層。這一篇,我們正式打開 Cloudflare 的向量資料庫,把「語意搜尋」的第一塊拼圖擺上桌:
- 向量資料庫是什麼——把「意思」變成空間中的「座標」(embedding),搜尋就是「找出最近的鄰居」(nearest-neighbor)。文字用
cosine、圖像用euclidean、推薦用dot-product,且 metric 與維度建立後不可改。 - 建立 index 與 binding——
wrangler vectorize create --dimensions --metric,再於wrangler.jsonc綁vectorizebinding,透過env.VECTORIZE呼叫。 - 五個核心操作——
insert(存在則忽略)/upsert(覆蓋,較安全)/query(找 topK 最近鄰)/getByIds(精確取)/deleteByIds(刪除);寫入皆為最終一致性,需等 5~10 秒。 - 精準過濾——metadata filtering(記得先建 metadata index)、namespaces(最快、適合多租戶)、以及
topK與returnMetadata對速度與上限的影響。
到這裡,你已經能把 embedding 存進 Vectorize、用語意找出最相關的內容了。但單純的相似搜尋還不是完整的 AI 應用——真正強大的是把「搜尋到的相關內容」餵給 LLM,讓它據此生成準確、有依據的回答,這就是 RAG(檢索增強生成,Retrieval-Augmented Generation)。下一篇《RAG 架構實作》,我們就把 Workers AI 的 embedding、Vectorize 的語意搜尋、D1 的全文儲存與 LLM 的生成串成一條龍,做出一個生產級的知識庫問答系統。
想深入 Vectorize 的完整 API 與各項配額,可以參考 Cloudflare Vectorize 官方文件。把向量存好、查準之後,我們下一篇《RAG 架構實作》見。