The model is a property of the index
Which is why changing one means reprocessing the corpus, and why it sits beside the metadata you indexed as a decision taken once. Semantic search is what it buys; the reference has the models.
Each configured index declares its model and its dimensions. Both are used for writes and for queries.
This removes the failure people actually hit: an index built with one model, queried with another.
Nothing errors when that happens. The query vector is perfectly valid; it simply lives in a different space from the corpus, and the neighbors that come back are neighbors of nothing in particular. Relevance degrades into noise, and every layer reports success.
No default, anywhere
embedTexts takes the model as a parameter with no default. An embedding whose model is implicit is an embedding nobody can reproduce — not in a test, not in a bug report, not next year.
The call production code makes pins the model to the index’s declared one, so the pinning is not something a call site has to remember.
Dimensions are fixed at creation
An index’s dimensions cannot be changed after it exists. Maximum 1,536 components, float32.
So the model choice and the dimension are one decision, made before the index is created, and changing your mind later means a new index and a full re-embed. The reprocess Worker exists for exactly that.
A response whose vector width does not match the index’s declaration is vector/dimension_mismatch — caught at the boundary rather than written.
The binding, not the REST client
Embedding runs over the AI binding on the Worker’s env.
@pithy-sh/cloudflare’s AI manager is deliberately not used here. It is the REST client: it needs an API token and an account id, which a Worker has neither of and should not be given.
The binding is typed structurally rather than against the exact platform shape, so a test can inject a fake. That is the only way to test any of this — Cloudflare ships no local emulation for Workers AI.
Batching
Vectors per upsert from a Worker are capped at 1,000. The HTTP API allows 5,000; the binding does not, and the number that matters is the one your code will hit.
Long source text belongs in D1, not in metadata — which is why this capability has a table. Metadata on one vector is capped at 10 KB, and the compact JSON must be under that.
Query size
| Query returns | Max topK |
|---|---|
| Values or metadata | 50 |
| Neither | 100 |
The default when you name no topK is 10. Small on purpose: this is a search page, not a scan.
Exceeding the ceiling is vector/topk_exceeded rather than a silently clamped result — a truncated result set that looks complete is the same class of bug as an unindexed filter.