Vectorize 向量資料庫入門:語意搜尋的第一塊拼圖 | Cloudflare 完整教學

2026/09/06
Vectorize 向量資料庫入門:語意搜尋的第一塊拼圖 | Cloudflare 完整教學

Cloudflare Vectorize 是一個與 Workers 深度整合的全球分散式向量資料庫(vector database),專為儲存與查詢 embedding(向量嵌入) 而生。有了它,你就能在邊緣運算環境裡做語意搜尋——不是比對關鍵字,而是比對「意思」。這一篇我們從「什麼是向量、什麼是相似度」講起,教你用 wrangler 建立 index、設定 binding,並跑一遍 insertupsertquerygetByIdsdeleteByIds、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 綁定。
  • 五個核心操作——insertupsertquerygetByIdsdeleteByIds 各自的用途與陷阱。
  • 精準過濾——用 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(索引)一個向量資料庫的容器,建立時綁定固定的 dimensionsmetric
vector(向量)一筆資料,含 id(必填)、values(必填,長度 = 維度)、選填的 namespacemetadata
metadata附加在向量上的鍵值資料(如 categoryisPublished),可拿來過濾
namespace最輕量的分區,在向量搜尋「之前」過濾,適合多租戶隔離
topKquery 最多回傳幾個最近鄰,預設 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 範例的型別 AiVectorizeVectorizeVector 皆來自 @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 建立後,dimensionsmetric 都無法修改。要改只能建立新 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 與 score100最快
"indexed"回傳「已建 index」的 metadata(字串截至 64 bytes)100
"all"回傳完整 metadata50較慢

日常首選 "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 indexcreate-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.jsoncvectorize binding,透過 env.VECTORIZE 呼叫。
  • 五個核心操作——insert(存在則忽略)/ upsert(覆蓋,較安全)/ query(找 topK 最近鄰)/ getByIds(精確取)/ deleteByIds(刪除);寫入皆為最終一致性,需等 5~10 秒。
  • 精準過濾——metadata filtering(記得先建 metadata index)、namespaces(最快、適合多租戶)、以及 topKreturnMetadata 對速度與上限的影響。

到這裡,你已經能把 embedding 存進 Vectorize、用語意找出最相關的內容了。但單純的相似搜尋還不是完整的 AI 應用——真正強大的是把「搜尋到的相關內容」餵給 LLM,讓它據此生成準確、有依據的回答,這就是 RAG(檢索增強生成,Retrieval-Augmented Generation)。下一篇《RAG 架構實作》,我們就把 Workers AI 的 embedding、Vectorize 的語意搜尋、D1 的全文儲存與 LLM 的生成串成一條龍,做出一個生產級的知識庫問答系統。

想深入 Vectorize 的完整 API 與各項配額,可以參考 Cloudflare Vectorize 官方文件。把向量存好、查準之後,我們下一篇《RAG 架構實作》見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →