Your semantic search works. You add a filter — only documents in this workspace — and it still works, on the twenty documents you tested with.
In production it returns four results where there should be forty. No error. No warning. Just fewer.
The failure
Vectorize supports filtering on metadata, and to filter on a field it needs a metadata index for that field. The catch is what happens when the index arrives after the vectors did.
Vectors written before a metadata index exists are not covered by it. Filter on that field and those vectors are silently excluded. You do not get an error, because nothing went wrong from Vectorize’s point of view — you asked for documents matching a filter and it gave you the ones it could see.
There is a second limit in the same area: ten metadata indexes per index, and hitting it also fails quietly.
Both of these are the worst kind of production bug. Nothing throws. The results are plausible. The only way to notice is to already know the number should have been forty.
Filterable fields are a schema decision
pithy add vector makes the fix structural: you declare your filterable fields once, as a schema.
Everything else follows from that declaration:
- The package provisions the metadata indexes from the schema, rather than you creating them by hand and hoping the order worked out.
- Every filter is typed against it, so a filter on a misspelled field is a compile error.
- A filter on a field the schema does not mark filterable is refused, rather than quietly returning less.
pithy vector provisionreconciles the live index against your config — and waits for each metadata index to go live before anything writes a vector.
That last clause is the one that closes the original hole. Indexes exist before vectors do, so there is no population of vectors that predates the index they will be filtered on.
The Worker refuses to boot
Here is the part I would want in any tool that has infrastructure and config that must agree.
provision records what it actually observed in the live index. At startup, the Worker compares your config against that record. If your config declares a filterable field the record does not have, the Worker refuses to boot, naming the field.
Consider what that prevents. You add a filterable field to your schema, deploy, and forget to re-provision. Without the check, your app comes up fine and starts returning short results on that filter — the exact silent failure the whole design exists to eliminate, reintroduced by a missed step.
With it, the deploy fails, and the message tells you which field is missing.
Embeddings, and the model you pinned
Embeddings come from Workers AI, and the model is pinned per index — for writes and for queries alike.
That pinning is not caution for its own sake. Vectors from two different embedding models are not comparable. Mixing them produces a search that returns confidently wrong neighbors, because the distances are meaningless across the boundary.
When you do change models, pithy vector reprocess re-embeds what the change left behind. It is a deliberate operation with a cost, which is the honest shape for “your entire index is now the wrong shape”.
What stays yours
Documents live in your own D1. The vector index holds embeddings and metadata; the text is in your database, joinable against everything else you know.
Chunking stays yours, and it should. How you split a document is the single biggest lever on retrieval quality, and it depends entirely on what your documents are. A library that chunked for you would be wrong for most people most of the time.
It pairs naturally with media: extracted document text and audio transcripts are the obvious things to embed, and they are already sitting in a table with an owner.
What it does not do
It does not do retrieval-augmented generation. It gives you relevant chunks; what you do with them is your application.
It does not rerank. The results come back by vector similarity, and if you want a second-stage model to reorder them, that is yours to add.
And it does not decide relevance thresholds. Every corpus has a different point at which “similar” stops meaning “related”, and only you can find yours.