ch36-vectorize
在 GitHub 上檢視·5 個檔案·8.0 KB
取得並執行
這個範例可以獨立 clone 執行,不依賴其他章節。
npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch36-vectorize
cd ch36-vectorize
npm install可用指令
npm run dev # wrangler dev
npm run types # wrangler types --env-interface CloudflareBindings說明
ch36 — Vectorize
Section titled “ch36 — Vectorize”Probe project for chapter 36.
⚠️ Vectorize has no local simulation. Every method throws
Binding VEC needs to be run remotely in wrangler dev. These routes need
"remote": true plus a real index.
npm installnpx wrangler vectorize create ch36-docs --dimensions 32 --metric cosinenpx wrangler vectorize create-metadata-index ch36-docs --property-name tenant --type stringnpx wrangler vectorize create-metadata-index ch36-docs --property-name year --type numbernpx wrangler vectorize create-metadata-index ch36-docs --property-name kind --type string
npx wrangler types --env-interface CloudflareBindingsexport CLOUDFLARE_API_TOKEN=...npx wrangler dev --port 9045Create the metadata indexes before inserting anything — see below.
Routes
Section titled “Routes”| Route | What it shows |
|---|---|
/shape | VectorizeIndexImpl and its full method list. |
/write-then-read | The one everyone hits. Write, then query immediately. |
/write-then-poll | Polls getByIds() every 500 ms so you can measure the real lag. |
/metadata-levels | none / indexed / all, plus the legacy boolean. |
/topk | 100 vs 101, and the drop to 50 with values or full metadata. |
/filter | Operators, and an oversized filter against the 2048-byte cap. |
/namespaces | The same vector in two namespaces, queried three ways. |
/insert-vs-upsert | Duplicate id: insert throws, upsert replaces. |
/dimensions | A wrong-dimension vector — the most common RAG runtime error. |
Writes are asynchronous
Section titled “Writes are asynchronous”public upsert(vectors: VectorizeVector[]): Promise<VectorizeAsyncMutation>;interface VectorizeAsyncMutation { mutationId: string }The type name says Async. The promise resolving means accepted, not
queryable. deleteByIds is the same, so “I deleted it and it’s still in the
results” is expected behaviour, not a bug.
Index in a Queue consumer or a Workflow, and verify separately:
const { mutationId } = await step.do("index", () => env.VEC.upsert(vectors));await step.sleep("settle", "10 seconds");await step.do("verify", async () => { const found = await env.VEC.getByIds(ids); if (found.length !== ids.length) throw new Error("not settled"); // step retries return found.length;});Metadata indexes must exist first
Section titled “Metadata indexes must exist first”npx wrangler vectorize create-metadata-index <index> --property-name tenant --type stringVectors written before the metadata index exists are not retroactively indexed — you have to re-upsert them. And there are only 10 metadata indexes per index, so budget them: index only the fields you actually filter on, and let everything else ride along in metadata for display.
Filters cap at 2048 bytes of JSON.
Note what the type excludes:
type VectorizeVectorMetadataValue = string | number | boolean | string[];// but filters are typed Exclude<VectorizeVectorMetadataValue, string[]>You can store arrays. You cannot filter on them.
returnMetadata takes both forms
Section titled “returnMetadata takes both forms”returnMetadata?: boolean | "all" | "indexed" | "none";true still compiles (backwards compatibility), but the three strings are the
real distinction:
| Value | Returns | Cost |
|---|---|---|
"none" | nothing | fastest |
"indexed" | only fields with a metadata index | cheap |
"all" | everything | drops topK from 100 to 50 |
returnValues: true also drops topK to 50 — and is almost never needed, since
you already have the query vector.
"indexed" is the right default.
Use upsert, not insert
Section titled “Use upsert, not insert”insert throws on an existing id; upsert replaces. Because indexing runs
behind a Queue or Workflow (both at-least-once), the operation must be
idempotent. insert is not.
Namespaces fail open
Section titled “Namespaces fail open”Forgetting namespace doesn’t error — it silently searches every tenant. Wrap
it once and forbid direct access:
export async function searchTenant(env, tenantId: string, vector: number[], topK = 5) { if (!tenantId) throw new Error("refusing to query Vectorize without a tenant"); return env.VEC.query(vector, { topK, namespace: tenantId, returnMetadata: "indexed" });}Limits that shape the design
Section titled “Limits that shape the design”- 1536 dimensions max — rules out many newer high-dimension embedding
models.
@cf/google/embeddinggemma-300m(ch34) fits. dimensionsandmetricare fixed at creation. Changing embedding models means a new index and a full re-index.- 10M vectors per index, 10 KiB metadata per vector, empty indexes are free.
Local development
Section titled “Local development”curl -s localhost:9045/shape | jq{ "ctorName": "VectorizeIndexImpl", "protoKeys": ["constructor","describe","query","queryById","insert","upsert", "getByIds","deleteByIds","_send","queryImplV2"], "describe": { "threwName": "Error", "threw": "Binding VEC needs to be run remotely" }}The binding is real — note queryImplV2 on the prototype, confirming the V1/V2
split lives in the implementation — but every call needs a remote connection.
Keep a separate -dev index and switch with a named environment (ch2).
原始碼
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ch36-vectorize",
"main": "src/index.ts",
"compatibility_date": "2026-07-24",
"observability": { "enabled": true },
"vectorize": [{ "binding": "VEC", "index_name": "ch36-docs" }],
// "ai" removed for offline probing -- see /embed
"r2_buckets": [{ "binding": "DOCS", "bucket_name": "ch36-docs" }]
}package.json
{
"name": "ch36-vectorize",
"private": true,
"type": "module",
"scripts": { "dev": "wrangler dev", "types": "wrangler types --env-interface CloudflareBindings" },
"devDependencies": { "wrangler": "^4.118.0", "typescript": "^5.9.2" }
}src/index.ts
const t = async (fn: () => unknown): Promise<Record<string, unknown>> => {
const started = Date.now();
try {
const v = await fn();
return { ok: v === undefined ? "(undefined)" : v, ms: Date.now() - started };
} catch (e) {
const err = e as Error & { code?: number };
return { threwName: err.name, code: err.code, threw: String(err.message ?? e).slice(0, 300), ms: Date.now() - started };
}
};
/** Deterministic pseudo-embedding so the probes need no AI calls. */
function fakeEmbedding(text: string, dims = 32): number[] {
const out = new Array(dims).fill(0);
for (let i = 0; i < text.length; i++) out[i % dims] += text.charCodeAt(i) / 255;
const norm = Math.hypot(...out) || 1;
return out.map((v) => v / norm);
}
export default {
async fetch(request: Request, env: CloudflareBindings, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const q = url.searchParams;
switch (url.pathname) {
case "/shape": {
const b = env.VEC as unknown as object;
return Response.json({
ctorName: Object.getPrototypeOf(b)?.constructor?.name ?? null,
protoKeys: Object.getOwnPropertyNames(Object.getPrototypeOf(b)),
describe: await t(() => env.VEC.describe()),
});
}
/**
* THE FIRST THING EVERY TUTORIAL GETS WRONG.
*
* Writes are asynchronous. insert/upsert return a mutationId, and the
* vectors are NOT queryable yet. This route writes then immediately
* queries, so you can watch it come back empty.
*/
case "/write-then-read": {
const id = `probe-${crypto.randomUUID().slice(0, 8)}`;
const vector = fakeEmbedding("cloudflare workers are v8 isolates");
const mutation = await t(() =>
env.VEC.upsert([
{ id, values: vector, namespace: "acme", metadata: { tenant: "acme", kind: "doc", year: 2026 } },
]),
);
// Immediately. This is the point of the probe.
const immediately = await t(() => env.VEC.query(vector, { topK: 3, namespace: "acme" }));
const byId = await t(() => env.VEC.getByIds([id]));
return Response.json({
note: "In production the immediate query typically returns nothing. upsert() resolves with a mutationId, not with durability.",
id,
mutation,
immediately,
byId,
});
}
// Poll until the write lands, so you can measure the real lag.
case "/write-then-poll": {
const id = `poll-${crypto.randomUUID().slice(0, 8)}`;
const vector = fakeEmbedding("durable objects are single threaded actors");
const started = Date.now();
await env.VEC.upsert([{ id, values: vector, namespace: "acme", metadata: { tenant: "acme" } }]);
const attempts: unknown[] = [];
for (let i = 0; i < 10; i++) {
const found = await env.VEC.getByIds([id]);
attempts.push({ afterMs: Date.now() - started, found: found.length });
if (found.length) break;
await new Promise((r) => setTimeout(r, 500));
}
return Response.json({ id, attempts });
}
// returnMetadata is NOT a boolean in spirit, even though the type allows one.
case "/metadata-levels": {
const vector = fakeEmbedding(q.get("q") ?? "workers");
const out: Record<string, unknown> = {};
for (const level of ["none", "indexed", "all"] as const) {
out[level] = await t(() =>
env.VEC.query(vector, { topK: 3, namespace: "acme", returnMetadata: level }),
);
}
// Still accepted by the type (boolean | VectorizeMetadataRetrievalLevel).
out["true (legacy boolean)"] = await t(() =>
env.VEC.query(vector, { topK: 3, namespace: "acme", returnMetadata: true }),
);
return Response.json(out);
}
// topK is capped at 100 -- but 50 when returning values or full metadata.
case "/topk": {
const vector = fakeEmbedding("workers");
return Response.json({
k100: await t(() => env.VEC.query(vector, { topK: 100 })),
k101: await t(() => env.VEC.query(vector, { topK: 101 })),
k51WithValues: await t(() => env.VEC.query(vector, { topK: 51, returnValues: true })),
k51WithAllMetadata: await t(() =>
env.VEC.query(vector, { topK: 51, returnMetadata: "all" }),
),
});
}
// Metadata filters. The field must have a metadata index created BEFORE
// the vectors were inserted.
case "/filter": {
const vector = fakeEmbedding(q.get("q") ?? "workers");
return Response.json({
eq: await t(() => env.VEC.query(vector, { topK: 5, filter: { tenant: "acme" } })),
operators: await t(() =>
env.VEC.query(vector, {
topK: 5,
filter: { year: { $gte: 2025 }, kind: { $in: ["doc", "faq"] } },
}),
),
// Filters are limited to 2048 bytes of JSON.
oversized: await t(() =>
env.VEC.query(vector, {
topK: 5,
filter: Object.fromEntries(
Array.from({ length: 200 }, (_, i) => [`f${i}`, "x".repeat(20)]),
) as never,
}),
),
});
}
// Namespaces are the tenant-isolation primitive.
case "/namespaces": {
const vector = fakeEmbedding("shared phrase");
await env.VEC.upsert([
{ id: "ns-a", values: vector, namespace: "acme", metadata: { tenant: "acme" } },
{ id: "ns-b", values: vector, namespace: "other", metadata: { tenant: "other" } },
]);
return Response.json({
note: "Same vector in two namespaces. A namespaced query must only ever see its own.",
acme: await t(() => env.VEC.query(vector, { topK: 5, namespace: "acme" })),
other: await t(() => env.VEC.query(vector, { topK: 5, namespace: "other" })),
noNamespace: await t(() => env.VEC.query(vector, { topK: 5 })),
});
}
// insert() throws on an existing id; upsert() replaces.
case "/insert-vs-upsert": {
const id = "dup-probe";
const v = fakeEmbedding("dup");
return Response.json({
firstInsert: await t(() => env.VEC.insert([{ id, values: v }])),
secondInsert: await t(() => env.VEC.insert([{ id, values: v }])),
upsert: await t(() => env.VEC.upsert([{ id, values: v }])),
});
}
// Dimension mismatch -- the most common runtime error in RAG code.
case "/dimensions": {
return Response.json({
wrongDims: await t(() =>
env.VEC.upsert([{ id: "bad-dims", values: fakeEmbedding("x", 7) }]),
),
describe: await t(() => env.VEC.describe()),
});
}
default:
return new Response(
"/shape /write-then-read /write-then-poll /metadata-levels /topk /filter\n" +
"/namespaces /insert-vs-upsert /dimensions\n",
{ status: 404 },
);
}
},
} satisfies ExportedHandler<CloudflareBindings>;.gitignore
node_modules/
.wrangler/
worker-configuration.d.tstsconfig.json
{
"compilerOptions": {
"target": "esnext", "lib": ["esnext"], "module": "esnext",
"moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
"strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true
},
"include": ["src/**/*.ts", "worker-configuration.d.ts"]
}