Cloudflare Vectorize 从零搭建语义搜索:完整代码教程
做AI应用经常遇到语义搜索的需求:用户输入”如何部署模型”,你要返回跟部署相关的文档,哪怕文档标题里没有”部署”两个字。关键词搜索搞不定这个,得靠向量检索。Cloudflare Vectorize是Cloudflare自家托管的向量数据库,跟Workers无缝集成,不用自己管服务器。这篇教程从创建索引到跑通查询,给出完整代码。
为什么选 Vectorize 而不是自建
自己搭pgvector或者Qdrant当然可以,但得管数据库运维、备份、扩容。Vectorize直接跑在Cloudflare边缘网络上,查询延迟低,按维度计费而不是按实例。根据Cloudflare官方定价页面,Workers Paid计划每月包含5000万查询维度和1000万存储维度,超出后查询维度每百万$0.01,存储维度每1亿$0.05。
实际算一下:1万条768维向量,每月查询3万次,总费用约$0.59/月。这个成本对独立开发者来说基本可以忽略。注意Vectorize存储和查询的是Float32格式,如果你用Float64传入会自动转换。
第一步:创建 Vectorize 索引
先装好wrangler CLI,在你的项目目录下执行:
npx wrangler vectorize create semantic-search-index --dimensions=768 --metric=cosine
这里两个参数很关键。dimensions必须跟你用的Embedding模型输出维度一致。Cloudflare Workers AI的@cf/baai/bge-base-en-v1.5模型输出768维,OpenAI的text-embedding-3-small输出1536维。如果维度对不上,插入和查询都会报错。
metric选cosine适合大多数文本语义搜索场景。另外两个选项是euclidean和dot-product,文本检索一般用cosine。索引创建后这两个参数不能改,要换得重建索引。
第二步:配置 wrangler.jsonc 绑定
在项目的wrangler.jsonc里添加Vectorize绑定:
{
"name": "semantic-search",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"vectorize": [
{
"binding": "SEARCH_INDEX",
"index_name": "semantic-search-index"
}
],
"ai": {
"binding": "AI"
}
}
同时绑定了AI(Workers AI),这样在Worker里可以直接调用Embedding模型,不用额外调OpenAI API。
第三步:写入向量数据
下面是完整的插入向量代码。把文本通过Workers AI生成embedding,然后upsert到Vectorize索引:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/insert" && request.method === "POST") {
const body = await request.json();
const documents = body.documents; // [{ id: "1", text: "内容..." }, ...]
const embeddings = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: documents.map(d => d.text),
});
const vectors = documents.map((doc, i) => ({
id: doc.id,
values: embeddings.data[i],
metadata: {
text: doc.text,
source: doc.source || "unknown",
created: Date.now(),
},
}));
const result = await env.SEARCH_INDEX.upsert(vectors);
return Response.json({ inserted: vectors.length, mutationId: result });
}
},
};
用upsert而不是insert。区别在于:insert遇到相同ID会跳过,upsert会覆盖。大多数场景你应该用upsert,这样重新跑数据导入不会出问题。
插入是异步操作,调用返回后向量不会立即可查。官方文档说通常几秒内生效。如果立即查询返回空结果,等几秒再试。
第四步:实现语义查询
查询接口的代码:
if (url.pathname === "/search" && request.method === "POST") {
const body = await request.json();
const query = body.query;
// 把查询文本转成向量
const queryEmbedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: [query],
});
// 查询Vectorize索引
const matches = await env.SEARCH_INDEX.query(
queryEmbedding.data[0],
{
topK: 5,
returnMetadata: "all",
}
);
return Response.json({
query,
results: matches.matches.map(m => ({
score: m.score,
id: m.id,
text: m.metadata?.text,
source: m.metadata?.source,
})),
});
}
topK设5返回最相似的5条结果。returnMetadata设”all”会把插入时存的metadata一起返回,这样不用再查一次数据库。如果只要部分metadata字段,可以用”indexed”选项,延迟更低但长文本字段可能被截断。
第五步:加 Metadata 过滤
Vectorize支持查询时按metadata过滤。比如只搜某个来源的文档:
const matches = await env.SEARCH_INDEX.query(
queryEmbedding.data[0],
{
topK: 10,
returnMetadata: "all",
filter: { source: "docs" },
}
);
filter支持$eq(等于)操作。官方文档建议:高基数字段(比如UUID、时间戳)适合做$eq过滤,因为筛选性强。低基数字段(比如只有两三个值的分类)过滤效果有限。
metadata的key有些限制:不能为空、不能含点号(.)、不能含双引号、不能以$开头。踩过这个坑,调试半天发现key写成了user.id,改成userId就好了。
部署和测试
部署到Cloudflare:
npx wrangler deploy
测试插入数据:
curl -X POST https://semantic-search.your-subdomain.workers.dev/insert \
-H "Content-Type: application/json" \
-d '{"documents":[{"id":"1","text":"如何部署机器学习模型到生产环境","source":"docs"},{"id":"2","text":"向量数据库选型对比","source":"docs"},{"id":"3","text":"RAG系统架构设计","source":"blog"}]}'
测试查询:
curl -X POST https://semantic-search.your-subdomain.workers.dev/search \
-H "Content-Type: application/json" \
-d '{"query":"模型上线流程"}'
查询”模型上线流程”应该返回id为1的文档(关于部署机器学习模型),虽然两个表述完全不同,但语义接近,cosine相似度会比较高。
成本控制建议
Vectorize按查询维度计费,不是按查询次数。每次查询消耗的维度数等于(索引中向量数 + 1) × 每个向量维度。所以索引越大,每次查询越贵。
控制成本的方法:选维度小的Embedding模型。bge-base-en-v1.5是768维,如果用OpenAI的text-embedding-3-small是1536维,同样数据量成本翻倍。中文场景可以试试bge-base-zh-v1.5,也是768维,中文效果更好。
另一个方法是控制索引规模。只存需要检索的内容,别把整篇文档的embedding都塞进去。把长文档分段,每段生成一个向量,metadata里记录原文位置。
常见踩坑
第一,维度不匹配。创建索引时dimensions设了768,但用的Embedding模型输出1536维,插入直接报错。创建索引前先确认模型输出维度。
第二,插入后立即查询返回空。Vectorize的插入是异步的,官方说几秒内生效。如果批量插入后马上测试,等5秒再查。
第三,metadata key包含点号。JSON里写"user.id"作为key会被拒绝。用下划线或驼峰命名。
第四,topK和returnMetadata的限制。设returnValues为true或returnMetadata为”all”时,topK最大50。不需要返回向量值的话,保持returnValues为false,topK可以到100。
