Semantic vector search

You need: vector composed, pithy vector provision run, and an AI binding.

Declare an index

Embedding models is the decision fixed at creation, choosing your metadata is the other, reprocessing is the way back from either, and the reference has the options.

vector({
  indexes: {
    docs: {
      model: "@cf/baai/bge-base-en-v1.5",
      dimensions: 768,
      metadata: z.object({
        ownerId: filterable(z.string().describe("Owner.")),
        title: z.string().describe("Title."),
      }),
    },
  },
})

dimensions cannot be changed after the index exists, so the model and the dimension are one decision made before you have data.

Provision, in this order

pithy vector provision

Index, then metadata indexes, then the Worker that writes vectors.

Write

POST /vector/docs/documents
{ "id": "doc-1", "text": "…", "metadata": { "ownerId": "u1", "title": "Q3 report" } }

The text is embedded with the index’s declared model — the same model queries use, which is the failure this removes.

Query

POST /vector/docs/query
{ "text": "how did we do last quarter", "topK": 10, "filter": { "ownerId": "u1" } }

A bare value is $eq. The long form takes $eq $ne $in $nin $lt $lte $gt $gte.

Two ceilings on topK

Query returnsMax
Values or metadata50
Neither100

The default is 10 — small on purpose: this is a search page, not a scan.

Exceeding it is vector/topk_exceeded, not a silently clamped result. A truncated result set that looks complete is the same class of bug as an unindexed filter.

Filtering an unindexed field does not error

Vectorize accepts the filter and returns a short, plausible result set.

Which is why filterable() brands the type — a filter naming an unmarked field is a compile-time error, with vector/unfilterable_field as the runtime guard behind it for callers arriving through an untyped boundary.

Ten metadata indexes per index. That hard cap is what makes filterability a provisioning-time decision — see vector metadata configuration.

Where the long text goes

Metadata is capped at 10 KB per vector, and the filter’s own JSON at under 2,048 bytes.

Long source text belongs in D1 — which is why this capability has a table. Store the text there, the embedding in Vectorize, and join on the id.

Searching your media

A transcript is exactly the shape a semantic index wants.

Run media enrichment to derive one, then index it — and your videos become findable by what was said in them.

ESC