创建文档向量索引

JavaScriptBeginner
立即练习

简介

在前一个 Workers AI 嵌入实验中,文本变成了一个嵌入:由一组有序数字组成,用于表示不同含义之间的有用关系。嵌入不是原始文章,也不是生成的答案。只有当应用能够使用稳定的文档 ID 保存嵌入,并在之后查找相近向量时,嵌入才可以用于搜索。

Cloudflare Vectorize 是一个向量数据库。与围绕行和列设计的表不同,向量索引专门用于高效比较数值向量。创建索引时,必须确定两项兼容性设置:

  • 维度(dimensions) — 每个向量包含多少个数字;
  • 距离度量(distance metric) — Vectorize 如何判断哪些向量彼此最接近。

你将创建一个 384 维索引,用于存储 Cloudflare 托管的 @cf/baai/bge-small-en-v1.5 嵌入,并选择余弦距离,也就是 A04 中介绍的基于方向的比较方式。你还将为 categorypublished 添加元数据索引,插入三个很小的合成帮助文章向量,等待异步变更变得可读,并确认三维向量会被拒绝。

这是 Vectorize 课程中的第一个实验。如果你是直接进入本实验,请先完成将 LabEx 连接到 Cloudflare 账户,以熟悉如何使用 LabEx VM 终端、授权 Wrangler、确认学习账户并配置账户 ID。如果你不熟悉向量、维度或余弦相似度,请先完成 Workers AI A04。

Vectorize 可在 Workers Free 中使用。目前包含的额度远高于本实验所需的三个 384 维向量和只读检查,因此不需要 Workers Paid。本实验不会调用 Workers AI,也不会消耗 Neurons。

安装过程会在 /home/labex/project/document-vector-index 中安装 Node.js 22.22.0 和项目本地的 Wrangler 4.132.0。同时,安装过程还会提供独立的只读检查。安装过程不会授权 Wrangler、创建索引、写入向量或修改你的 Cloudflare 账户。

授权 VM 并命名索引

在本步骤中,你将授权全新的 VM,选择指定的学习账户,并记录一个唯一的一次性索引名称。

Cloudflare Dashboard 的登录状态属于浏览器。在这个全新的 VM 中,Wrangler 是独立的客户端,因此在管理 Vectorize 资源之前,需要先授予它有限的授权。

进入准备好的项目目录,并确认固定的 CLI 版本:

cd /home/labex/project/document-vector-index
npx wrangler --version

预期输出为 4.132.0。请求账户身份信息和 Workers 资源管理权限。在当前 Wrangler 版本中,workers:write OAuth 作用域包含本实验所需的 Vectorize 管理操作;由于本实验不执行推理,因此不请求 AI 作用域。

npx wrangler login --device --browser=false --scopes account:read user:read workers:write

打开显示的链接,输入当前代码,检查账户和权限,然后授权你的学习账户。接着检查结构化的身份数据:

npx wrangler whoami --json

确认 loggedIn: true,并识别指定的学习账户。生成一个唯一的一次性索引名称:

RUN="labex-c08-v01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

YOUR_ACCOUNT_ID 替换为该账户的实际 ID:

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN-tools",
  "account_id": "YOUR_ACCOUNT_ID",
  "compatibility_date": "2026-09-16",
  "vectorize": [
    { "binding": "DOCUMENTS", "index_name": "$RUN", "remote": true }
  ]
}
JSON

该绑定记录了后续实验将在 Worker 代码中使用的关系:DOCUMENTS 是应用侧使用的名称,而 index_name 是实际拥有的云资源名称。remote: true 表示本地 Worker 将连接真实的远程索引,而不是使用隔离的本地模拟环境。

创建索引及其可筛选字段

在本步骤中,你将创建固定的向量契约,并为之后的筛选准备两个元数据字段。

索引的维度和距离度量是固定的,因为每次比较都必须遵循同一个数值契约。BGE Small 会生成 384 个数字。余弦距离比较向量的方向,适合 A04 中介绍的面向含义的嵌入。

创建 V2 索引:

npx wrangler vectorize create "$RUN" --dimensions=384 --metric=cosine --update-config=false

向量还可以携带少量元数据,例如文档类别。保存元数据并不会自动使其可筛选。元数据索引(metadata index)会告诉 Vectorize 应该为哪些字段准备筛选能力。在插入向量之前创建以下字段:

npx wrangler vectorize create-metadata-index "$RUN" --propertyName=category --type=string | tee .labex/category-index-output.txt
npx wrangler vectorize create-metadata-index "$RUN" --propertyName=published --type=boolean | tee .labex/published-index-output.txt

--update-config=false 可防止 Wrangler 提议替换你已经写入的绑定。元数据索引的创建是异步的。每条命令都会将一个变更加入队列,因此成功消息只表示 Cloudflare 已接受该变更,并不表示所有读取操作都已经可以看到它。

创建一个可重复使用的等待脚本。它只运行只读的 vectorize info 命令,比较完全一致的变更 ID,并要求连续三次读取结果匹配后,才确认结果可信。额外的确认可以避免把短暂过时的读取副本误认为最终状态。等待脚本会在四分钟后报错并停止,而不是无限等待:

cat > scripts/wait-for-vectorize.mjs <<'JS'
import { execFileSync } from "node:child_process";

const [indexName, mutationId, expectedCountText] = process.argv.slice(2);
const expectedCount = Number(expectedCountText);
const wrangler = "./node_modules/wrangler/bin/wrangler.js";
let consecutiveMatches = 0;

for (let attempt = 1; attempt <= 120; attempt += 1) {
  const output = execFileSync(process.execPath, [wrangler, "vectorize", "info", indexName, "--json"], { encoding: "utf8" });
  const info = JSON.parse(output);
  if (info.processedUpToMutation === mutationId && info.vectorCount === expectedCount) {
    consecutiveMatches += 1;
  } else {
    consecutiveMatches = 0;
  }
  if (consecutiveMatches === 3) {
    console.log(`mutation ${mutationId} is consistently readable with ${expectedCount} vectors`);
    console.log(JSON.stringify(info, null, 2));
    process.exit(0);
  }
  await new Promise((resolve) => setTimeout(resolve, 2000));
}
throw new Error(`mutation ${mutationId} was not readable within four minutes`);
JS

METADATA_MUTATION_ID=$(sed -nE 's/.*Mutation changeset identifier: ([0-9a-f-]{36}).*/\1/p' .labex/published-index-output.txt | tail -n 1)
test -n "$METADATA_MUTATION_ID"
node scripts/wait-for-vectorize.mjs "$RUN" "$METADATA_MUTATION_ID" 0
npx wrangler vectorize get "$RUN"
npx wrangler vectorize list-metadata-index "$RUN"

最后的表格应显示:384 个维度、余弦距离、category 的类型为 String,以及 published 的类型为 Bool。使用 --type=boolean 创建的字段,在当前 API 中显示名称为 Bool。等待第二个元数据变更完成,可以避免下一步插入向量时仍在等待索引准备完成。

构建带标识的文档向量

在本步骤中,你将生成一个透明的小型向量测试数据集,其 ID 和元数据可以独立检查。

向量数据库不会替代源文档。每个向量都需要一个稳定的 ID,以便应用能够将它映射回真实内容。本实验使用三个合成的帮助文章 ID,并将它们的类别、发布状态、嵌入模型和池化选项记录为元数据。

实时嵌入将在 V03 中返回。本实验使用确定性向量,使存储行为可重复且不产生费用:每个文档沿着不同的坐标轴指向 1,其余位置填充 0,直到达到 384 个位置。

创建透明的测试数据生成器:

cat > scripts/create-vectors.mjs <<'JS'
import { writeFileSync } from "node:fs";

const DIMENSIONS = 384;
const MODEL = "@cf/baai/bge-small-en-v1.5";
const POOLING = "cls";
const documents = [
  { id: "password-reset", axis: 0, category: "account" },
  { id: "upload-pdf", axis: 1, category: "files" },
  { id: "billing-receipt", axis: 2, category: "billing" }
];

function unitVector(axis) {
  const values = Array(DIMENSIONS).fill(0);
  values[axis] = 1;
  return values;
}

const rows = documents.map((document) => ({
  id: document.id,
  values: unitVector(document.axis),
  metadata: {
    category: document.category,
    published: true,
    model: MODEL,
    pooling: POOLING
  }
}));

writeFileSync("vectors/documents.ndjson", rows.map(JSON.stringify).join("\n") + "\n");
console.log(`wrote ${rows.length} vectors with ${DIMENSIONS} dimensions each`);
JS
node scripts/create-vectors.mjs

NDJSON 表示按换行分隔的 JSON:每行包含一个完整的向量对象,而不是由一个外层 JSON 数组包裹所有对象。Wrangler 可以分批流式处理这种格式。不打印全部 1,152 个数字,只检查各个对象的标识和形状:

node - <<'JS'
const rows = require("fs").readFileSync("vectors/documents.ndjson", "utf8").trim().split("\n").map(JSON.parse);
console.table(rows.map(({ id, values, metadata }) => ({ id, dimensions: values.length, category: metadata.category, published: metadata.published })));
JS

三行数据都应报告 384 个维度。模型和 cls 池化元数据用于记录兼容性;Vectorize 不会代替你推断或验证这些语义信息。

插入向量并等待其变更完成

在本步骤中,你将插入一个批次,并等待其对应的异步变更对读取操作可见。

Vectorize 的写入是异步的。插入操作会先进入持久化的预写日志,并返回一个变更 ID。随后,后台处理会让该变更对读取操作可见。这种设计可以提高写入效率,但也意味着「已接受」和「可读取」是两个不同的时刻。

插入包含三个向量的批次,并保存完整结果。pipefail 可以避免 Wrangler 失败后,后面的成功 tee 命令掩盖真实错误:

set -o pipefail
npx wrangler vectorize insert "$RUN" --file=vectors/documents.ndjson 2>&1 | tee .labex/insert-output.txt

只有在 Wrangler 表示已将三个向量加入队列并打印出变更标识符后,才继续操作。如果 API 返回身份验证错误或网络错误,该结果无法说明插入是否成功:先确认 npx wrangler whoami --json,然后重新运行一次相同的插入代码块。没有真实的变更 ID 时,不要启动等待脚本。

提取已接受的变更,并且只在变更 ID 存在时等待:

MUTATION_ID=$(sed -nE 's/.*Mutation changeset identifier: ([0-9a-f-]{36}).*/\1/p' .labex/insert-output.txt | tail -n 1)
if [ -z "$MUTATION_ID" ]; then
  printf '%s\n' 'No mutation ID was returned; fix the insert error before waiting.' >&2
else
  printf 'Waiting for mutation %s\n' "$MUTATION_ID"
  node scripts/wait-for-vectorize.mjs "$RUN" "$MUTATION_ID" 3
fi

等待脚本输出的最终 JSON 应显示 vectorCount 为 3,并包含记录的变更 ID。要求连续三次读取匹配,可以降低短暂副本延迟对学习者所见结果的影响。相比固定等待一段时间,有界轮询更安全:变更处理较快时可以及时完成,处理较慢但状态正常时也有足够时间,同时不会产生重复写入。

读取文档并测试兼容性

在本步骤中,你将读取已接受的记录,观察不兼容的写入被拒绝,并将 CLI 状态与 Dashboard 对照起来。

通过应用 ID 读取已保存的记录:

保存完整记录,然后打印紧凑的表格,避免将 1,152 个数字全部输出到终端:

npx wrangler vectorize get-vectors "$RUN" --ids password-reset upload-pdf billing-receipt > .labex/stored-vectors.txt
node - <<'JS'
const text = require("fs").readFileSync(".labex/stored-vectors.txt", "utf8");
const rows = JSON.parse(text.slice(text.indexOf("[")));
console.table(rows.map(({ id, values, metadata }) => ({ id, dimensions: values.length, category: metadata.category, published: metadata.published })));
JS

每一行摘要都应保留原有 ID、384 个值的向量形状以及元数据。原始文件包含完整的值,可用于独立检查。get-vectors 读取已知记录;它不是相似度搜索。相似度查询将在 V03 中介绍。

现在创建一条只有三个值的、故意不兼容的记录:

cat > vectors/incompatible.ndjson <<'NDJSON'
{"id":"wrong-dimensions","values":[1,0,0],"metadata":{"category":"account","published":true}}
NDJSON
if npx wrangler vectorize insert "$RUN" --file=vectors/incompatible.ndjson > .labex/incompatible.log 2>&1; then
  STATUS=0
else
  STATUS=$?
fi
printf '%s\n' "$STATUS" > .labex/incompatible-exit.txt
sed -n '/invalid vector/p' .labex/incompatible.log
test "$STATUS" -ne 0

拒绝操作可以保护索引契约:三位向量无法与 384 位向量进行有意义的比较。确认已接受的记录仍然存在,并且被拒绝的 ID 没有出现:

npx wrangler vectorize info "$RUN"
npx wrangler vectorize list-vectors "$RUN" --count=10
npx wrangler vectorize get-vectors "$RUN" --ids wrong-dimensions

打开所选账户的 Cloudflare Dashboard,进入 AI → Vectorize。资源列表会将 CLI 名称与真实索引对应起来,显示 384 个维度和余弦距离,并报告总共三个向量;在这个小型示例中不会产生计费使用量。

Vectorize 资源列表中的一次性索引,包含 384 个维度、余弦度量和三个向量

打开 $RUN 中命名的索引。索引摘要会显示当前存储的三个向量。由于本实验首次使用 ID 读取,查询数仍为 0;相似度查询将在 V03 中开始。

Vectorize 索引摘要,显示当前存储的三个向量且查询数为 0

滚动到 Stored Vectors。该图表直观展示异步可见性:计数先保持为 0,插入变更处理完成后变为 3。

异步变更处理后,Stored Vectors 图表从 0 上升到 3

当前 Dashboard 不会列出单个向量 ID 或元数据索引定义。请使用前面 Wrangler 对 password-resetupload-pdfbilling-receiptcategorypublished 的读取结果;不要从只显示计数的图表中推断这些详细信息。Dashboard 页面有助于了解资源位置,而独立检查使用权威的 API 读取结果。

这里展示的是实验在云端完成后、某次一次性运行中的示例截图。你的随机索引名称和时间戳会不同;请以配置和实际拥有的 ID 为准,不要直接复制示例值。

删除一次性索引并退出登录

在本步骤中,你将删除确切的已拥有索引,证明经过身份验证后该索引已不存在,然后移除 VM 的授权。

该索引、它的元数据索引以及其中的向量共同组成一个一次性资源。在授权仍然有效时,删除 wrangler.jsonc 中保存的确切名称:

npx wrangler vectorize delete "$RUN" --force

通过经过身份验证的资源列表读取确认索引已不存在:

npx wrangler vectorize list --json > .labex/indexes-after-cleanup.json
node -e '
  const rows = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
  if (rows.some((row) => row.name === process.argv[2])) throw new Error("lab index still exists");
  console.log("lab index is absent");
' .labex/indexes-after-cleanup.json "$RUN"

这次成功的资源列表读取很重要:网络错误或授权错误都不能证明删除成功。在撤销 VM 授权之前,运行清理检查:

bash verify6-1.sh

最后删除 VM 的 Wrangler 登录状态,并检查结构化结果:

npx wrangler logout
npx wrangler whoami --json

预期结果为 loggedIn: false。Dashboard 的浏览器登录状态是独立的,仍可用于你的学习账户。

总结

你创建了一个 Vectorize V2 索引,其 384 维契约与所选嵌入模型一致,选择了余弦距离,并为后续筛选准备了两个元数据字段。你生成了带标识的确定性向量,以 NDJSON 格式插入它们,区分了已接受的异步变更和已处理的变更,并通过 ID 读取回已保存的记录。

你还验证了 Vectorize 会拒绝维度错误的向量,同时保留兼容的记录。最后,你在 Dashboard 中检查了真实资源,删除了确切的一次性索引,确认经过身份验证后该索引已不存在,并移除了全新 VM 的 Wrangler 授权。

下一个实验将在此生命周期基础上介绍 upsert 和删除操作,确保已修改或已停用的文档不会使索引保持过时状态。