Choose a vector database on renewal risk, not recall
Recall is a dial, not a differentiator
Most vector database evaluations we see are a spreadsheet of recall@10 against p99 latency across four engines, at a corpus size somebody guessed at. The winner gets a three-year commitment. The thing that actually hurts two or three years later is never the number in that spreadsheet.
Recall is a tunable, not a property of the engine. In pgvector, search quality is a session GUC: hnsw.ef_search defaults to 40, accepts anything from 1 to 1000, and changes per query without touching the index. Every serious engine exposes an equivalent — candidate list size, probe count, oversampling factor, rerank depth. HNSW and the IVF family are published algorithms implemented from the same papers, so recall differences between mature engines at a fixed latency budget are real but small, and smaller than what a reranking stage does to the same pipeline. We have never been able to attribute a user-visible retrieval quality complaint to the ANN engine. Chunking, filters and ranking, yes. The engine, no.
What does not commoditise is the cost of being wrong. That cost has three parts: how hard it is to get your vectors and, more importantly, your metadata back out; whether the filtering semantics you built on are portable; and what happens to your bill and your build times when you change embedding model or your index stops fitting in memory. None of those appear on a benchmark chart, and all three are knowable before you sign.
Getting your data out is a vendor-specific project
Every vector store has an export path. None of them produce a portable artefact.
Pinecone’s export is documented and the shape of it is instructive. It is available on Standard and Enterprise plans only, it writes to your own S3 or GCS bucket, and you start it by opening a support ticket with your destination URI, storage integration ID, project ID and backup ID. There is no self-service API call. The output is Parquet, genuinely portable as a container, and the docs say it is “already formatted for import” — into Pinecone. The schema is theirs.
Milvus is open source and its backup story is better, but the compatibility matrix in its own documentation draws the boundary: a backup can only be restored to the same or a newer Milvus version, so a backup taken from 2.5 will not restore into 2.4. Sensible engineering, and also a statement that the artefact is a Milvus artefact rather than a corpus.
Qdrant snapshots are tar archives containing a collection’s data and its pre-built index, so restores skip re-indexing entirely. The price of carrying the index is that snapshots are per-node: on a distributed cluster you create and restore one per node, by hand, against each node’s API. The alternative migration tool streams collection data between instances, and its documentation states the requirement most capacity plans miss — the target cluster needs twice the RAM and disk of the source collection during the migration, because original segments sit alongside newly built ones while optimisation runs.
Turbopuffer is the most honest of the set. Exporting a namespace means paging through the query API with a filter on id ascending, 10,000 rows at a time, in your own loop. Their backups page says they do not currently offer automated backups and that customers have historically rebuilt from their primary data source when they needed to.
That last sentence is the design rule, not a limitation. The vector store should be a derived index. If your chunk text, chunk boundaries, document IDs, ACLs and filter attributes only exist inside it, you have made an ANN index into a system of record and the export path is now on your critical path. We keep the canonical corpus in the lakehouse — the same streaming-first pattern with CDC and Iceberg we use elsewhere — and treat the vector store as something we can drop and rebuild. That costs a pipeline. It buys the ability to leave.
Filtering semantics are the part you cannot port
This is the one that surprises people, because filters look like the most standard thing in the stack. WHERE tenant_id = ? AND status = 'published' is not a hard idea. But the engines disagree about when the filter runs relative to the graph traversal, and that disagreement changes your results.
pgvector applies the filter after the index is scanned. Its own README works the arithmetic: if a condition matches 10% of rows, with HNSW and the default ef_search of 40, you get about four rows back on average. Not four wrong rows — four rows where you asked for ten. Version 0.8.0 added iterative index scans to fix the under-return, which keep scanning until enough results are found or hnsw.max_scan_tuples trips at a default of 20,000. The behaviour is bounded by a tuple budget, and you pick strict_order or relaxed_order depending on whether you want exact distance ordering or better recall.
Qdrant solves the same problem in a completely different place. It extends the HNSW graph with extra edges derived from indexed payload values, so filtered traversal stays connected instead of fragmenting, and its query planner switches to a payload-index scan entirely when the filter is selective enough that the graph would fall apart. The catch is in the build order: their indexing documentation states that payload indexes should be created before ingesting data, because the filterable HNSW graph only gets filter-aware edges if it is generated after the payload indexes exist. Add a payload index later and you need to rebuild the HNSW index to benefit from it.
Both are defensible designs. Neither is the other. Move a workload between them and, for the same query and the same corpus, the set of documents returned under a filter changes — most visibly at medium selectivity, which is where nearly all real access control and recency filtering lives. Your retrieval evals move with it, so dashboards will not tell you whether the migration was clean. That is before nested-object filters, array membership and full-text match conditions, where the semantics diverge further and the query DSLs stop mapping onto each other at all.
The mitigation is not to avoid filters. It is to keep the filter surface you depend on small and written down, and to hold retrieval evals in a form that runs against any engine.
Re-embedding is a pricing event, not just a compute one
Changing embedding model is presented as a compute problem: rent GPUs, re-encode the corpus, swap the index. The compute is usually the cheap part.
Weaviate Cloud bills on total vector dimensions — their pricing FAQ says so directly, and the Flex tier starts at $45 per month with vector dimensions from $0.00465 per million and storage from $0.12 per GiB. It is a clean, predictable model with a consequence people meet after the migration rather than before: on the same corpus with the same object count, moving from a 1024-dimension model to a 3072-dimension one triples the line item that model prices on. The engineering work is a weekend. The bill is permanent.
Self-hosting moves the same cost to memory and build time. pgvector’s HNSW build runs in two phases — an in-memory phase holding the whole graph, then, when it exhausts maintenance_work_mem, an on-disk phase that inserts each remaining vector one at a time exactly as a normal INSERT would. The build emits a notice when it crosses over, and the wording in the source is not subtle: “Building will take significantly more time.” The project’s own rule of thumb for sizing the in-memory phase, from its issue tracker, is roughly n × (dims × 4 + 8) × 1.3 bytes.
Run that for 20 million chunks. At 1024 dimensions the in-memory graph wants about 107 GB; at 3072 dimensions, about 320 GB. On managed Postgres the first is a large but ordinary machine and the second is a different procurement conversation — or you cross into the on-disk build and watch a build that took hours take far longer. The dimension count of your embedding model is an infrastructure decision, and it is the one people change most casually.
So the question to ask a vendor is not what a re-embed costs in tokens. It is: at 2× the dimensions and 1.5× the corpus, what does the monthly invoice say, how long is the index build, and can I run the old and new indexes side by side during cutover — because if I cannot, the migration is an outage.
The test: export everything, import it somewhere else, time it
Before you commit, do the migration once, on a slice of production-shaped data, into a second engine you have no intention of using. Most teams never do this, which is why they find out during an incident or a renewal negotiation. The point is not a number for the spreadsheet; it is that the exercise surfaces what is invisible from the outside:
- Does the metadata survive intact? Types, nulls, nested objects, arrays and anything with a timezone. Most of the damage is here.
- How much of the export is self-service? A step that requires a support ticket has a queue attached to it, and the queue is longest when the relationship is worst.
- What is the wall-clock time, including re-indexing on the target? An index that took nine hours to build takes nine hours to build again.
- Do your retrieval evals produce the same results? They will not, and the gap measures how much filter semantics you have quietly depended on.
Budget a week and use a corpus large enough to be slow. A 100,000-vector rehearsal proves nothing, because everything fits in memory at 100,000 vectors. We keep the eval set for this in the same harness we use for hallucination detection, so the migration check is a job we already run rather than a script somebody writes under pressure.
Where the Postgres boundary actually sits
This argument has an obvious failure mode: taken far enough it justifies choosing nothing, or defaulting to “just use Postgres” as though that were free. It is not free, and the specific ways it is not free are the ones above. Post-filter behaviour under medium-selectivity filters is a real limitation you tune around with iterative scans and a tuple budget. There is no built-in way to spread one index across machines, so your ceiling is one instance’s memory plus whatever quantisation you accept — and quantisation is a recall decision you now own.
The honest boundary is not a vector count. The count that matters depends on your dimensions, your filter selectivity and your query concurrency, and anyone quoting a single crossover number is quoting a benchmark run by an interested party. The boundary is a set of signals you can observe. If you already operate Postgres, your working set fits in memory you can buy, and filters are the reason you are here, pgvector is the low-regret answer and it keeps vectors in the same transaction as the rows they describe. When the HNSW build starts spilling out of maintenance_work_mem on hardware you can justify, when you need thousands of isolated tenants rather than a tenant_id column, or when the index has to outgrow one machine, a dedicated engine earns its operational cost — and at that point choose one quickly, on the criteria above.
The category is visibly in motion, which is the reason to care. Qdrant raised a $50M Series B in March 2026, and in May, Pinecone — which more or less created the category — announced Nexus and a query language called KnowQL, repositioning from vector database to knowledge engine for agents. We will not pretend to know how that ends. That is the point: you are picking a dependency for longer than you can forecast the vendor, so pick one whose exit you have already rehearsed.
What we’d do differently
We chose an engine on a benchmark once and paid for it in a place the benchmark did not cover. The evaluation was careful about recall and latency and said nothing about filters. When we later moved that workload, retrieval quality on filtered queries dropped in a way that took us most of a week to diagnose, because both systems were behaving exactly as documented and our evals had no filtered cases in them. The fix was three lines of configuration. Finding it was not.
The deeper mistake was letting the vector store become the only place some chunk metadata lived. Re-deriving it from source documents turned a planned two-day cutover into closer to three weeks, most of it spent reconciling records edited in place through an admin tool nobody remembered writing. We now treat any write that lands only in the vector store as a defect.
Two things we would do at the start, every time. Write the retrieval eval set before choosing the engine, and make sure a third of it is filtered queries. And run the export-and-reimport rehearsal during procurement, while the contract is unsigned and the vendor is still returning calls quickly — the results are a better negotiating position than anything in the benchmark.
References
- Export a backup — Pinecone Docs, accessed May 2026
- Milvus Backup overview and compatibility matrix — Milvus Docs, accessed May 2026
- Migration and recovery options and Indexing — Qdrant Docs, accessed May 2026
- Export documents and Cross-region backups — turbopuffer Docs, accessed May 2026
- pgvector README: filtering, iterative index scans and HNSW build — PGXN, and
src/hnswbuild.cplus the memory-sizing rule of thumb in issue #844 — pgvector on GitHub, accessed May 2026 - Vector database pricing — Weaviate, accessed May 2026
- We raised $50M to build composable vector search as core infrastructure — Qdrant, 12 March 2026
- Knowledge infrastructure for agents — Pinecone, 4 May 2026
We design retrieval systems you can migrate as part of our Data Engineering practice. Talk to us if you are up against a vector database renewal and want the exit rehearsed before you sign.