DEV Community

Multigrid
Multigrid

Posted on • Originally published at multigrid.ai

Creating a Vectorize Index on Cloudflare

Creating a Vectorize index is one command with two arguments. Both are documented as unchangeable after creation, which makes this the shortest command in the stack with the longest consequences.

Two arguments you cannot change later

Cloudflare states it plainly in its Vectorize best-practices documentation: the configuration of an index cannot be changed after creation, the number of dimensions an index is created for cannot change, and distance metrics cannot be changed after index creation.

There is no migration path and no alter statement. Changing either means creating a new index and re-embedding every document, which on a large corpus is a real job with a real bill attached. Treat the create command the way you would treat a primary key choice.

Choosing the dimension

The dimension is not yours to choose freely — it is dictated by the embedding model, because it is the length of the vectors that model emits. Cloudflare documents these output dimensions for its own embedding models: @cf/baai/bge-small-en-v1.5 at 384, @cf/baai/bge-base-en-v1.5 at 768, and @cf/baai/bge-large-en-v1.5 at 1024.

The constraint that catches people out is at the top end: Cloudflare documents a maximum of 1,536 dimensions per vector, at 32-bit precision. Several widely-used third-party embedding models emit more than that. If you intend to use one, you must reduce the dimension before storing — many providers support requesting a shorter vector directly, and where they do not, truncation is not generally safe unless the model was trained for it. Establish this before you build, not after the first upsert returns an error.

Where you have a genuine choice, smaller is usually right. Dimension multiplies directly into both halves of the Vectorize bill, so 1024 costs roughly 2.7 times what 384 does for identical traffic.

Choosing the metric

Cloudflare documents three distance metrics, and how to read each score:

  • cosine — ranges from −1 for most dissimilar to 1 for identical. Measures angle only, so vector magnitude is ignored.
  • euclidean — L2 distance, where 0 means identical and larger means further apart. Note the direction is inverted relative to cosine: for euclidean, lower is better.
  • dot-product — documented as a negative dot product score. Sensitive to magnitude as well as direction.

For text embeddings, cosine is the default answer and it is the right one for the BGE family, which produces normalised vectors. When vectors are normalised to unit length, cosine and dot product rank identically, so the choice only matters if magnitude carries meaning in your embedding — which for sentence embeddings it generally does not.

The practical reason to prefer cosine anyway is that the score is bounded and interpretable. A threshold of “discard matches below 0.7” is meaningful on a −1 to 1 scale and meaningless on an unbounded distance, and every retrieval pipeline eventually grows a threshold.

Creating and binding the index

  1. Make sure Wrangler is current. Cloudflare documents version 3.71.0 or later for the Vectorize commands; run npx wrangler --version to check.
  2. Create the index, naming the dimension to match your embedding model:

    npx wrangler vectorize create docs-768 --dimensions=768 --metric=cosine
    
  3. Add the binding to wrangler.jsonc. The binding is the property name on env; index_name must match the index you just created.

    {
      "vectorize": [
        { "binding": "DOCS", "index_name": "docs-768" }
      ]
    }
    
  4. Regenerate types with npx wrangler types so env.DOCS is typed.

  5. Confirm the index was created as you intended by calling describe(), which Cloudflare documents as returning the index’s configured dimensions and distance metric. Do this once and assert on it in a startup check — it is the only way to catch a mismatch before it becomes silent nonsense in your results.

Naming the index after its dimension, as docs-768 above, is a small habit with a large payoff. The dimension then appears in every binding, every log line and every code review, so a 1024-dimension model pointed at a 768-dimension index is visible rather than discovered.

When you have to change your mind

Sooner or later a better embedding model appears, or 384 dimensions stop retrieving well enough, and you need a dimension you cannot set. Because Cloudflare documents both dimension and metric as fixed at creation, the operation is not a migration in the database sense. It is building a second index and cutting over to it.

  1. Create the new index alongside the old one under a name that carries its dimension: npx wrangler vectorize create docs-1024 --dimensions=1024 --metric=cosine.
  2. Add it as a second binding — DOCS and DOCS_NEXT — so both exist in the same deploy. Nothing reads the new one yet.
  3. Start dual-writing. Every ingest path embeds with both models and upserts to both indexes, keeping the same vector ids on each side so the two stay comparable.
  4. Backfill the history in batches of up to the documented 1,000 vectors per call, and remember the mutations are asynchronous, so the backfill is queryable some time after it is accepted rather than immediately.
  5. Cut reads over by swapping index_name on the DOCS binding and deploying. The application code does not change, because it only ever referenced the binding name.
  6. Leave the old index in place for as long as you would want to roll back, then delete it.

Two costs are worth planning for. During the overlap you are paying for both indexes, and since stored dimensions are billed, the bill is roughly the sum of the two rather than the larger — a 768 index plus a 1024 index is billed as 1,792 dimensions per document. And during the overlap you are running two embedding models on every write, which doubles the inference cost of ingest for the duration.

Recall also that the metric is fixed by the same rule, so switching from euclidean to cosine costs exactly the same exercise as changing the dimension even though the vectors themselves would not change. That is the strongest argument for choosing cosine at the start unless you have a concrete reason not to.

The documented ceilings

Cloudflare’s Vectorize limits page documents, at the time of writing: a maximum of 20,000,000 vectors per index; 50,000 indexes per account on Workers Paid and 100 on Free; 50,000 namespaces per index on Paid and 1,000 on Free; index names up to 64 bytes; and vector ids up to 64 bytes.

The 20 million per index figure is the one that shapes architecture. It is generous for a documentation corpus and restrictive for per-customer chunked data at scale, and the escape hatch is namespaces rather than more indexes — a namespace segments an index and is supplied per vector, so one index can hold many tenants’ data with queries scoped to one of them. That also keeps you inside the index-count limit, which is much lower on the Free plan than people expect.

These are Cloudflare’s published Vectorize limits at the time of writing and differ by plan. Read current values from Cloudflare’s Vectorize limits page before designing around any of them.

With the index created, the next steps are filling it from an embedding model and querying it from a Worker.

Related

Top comments (0)