Vector Database · Research

Vector Database Comparison: pgvector, Qdrant, Weaviate, Milvus and Chroma

ThreatFrontier poster showing the SimpleHelp three-flaw attack chain against MSP remote support infrastructure
EV

Data security correspondent · Updated Sep 12, 2026, 12:37 PM EDT

Most teams adopt a specialised store before they need one. An architecture-first look at ANN indexing, filtered search, tenancy and the real thresholds.

A vector database stores embeddings and answers nearest-neighbour queries quickly: given a query vector, return the k closest stored vectors fast enough to sit inside a request. Everything else is scaffolding around that primitive.

The category exploded because retrieval-augmented generation made that primitive load-bearing for many teams at once, so teams now reach for a specialised system before establishing that they need one. A second datastore arrives with a second set of credentials, a second backup regime, a second thing to patch at two in the morning.

The thesis first: for a large fraction of production workloads, a vector extension on the database you already run is the correct answer. The interesting question is not which vector database is fastest, but what would make you outgrow the simple option.

One ground rule: this comparison is architectural, not numerical. Throughput, latency, pricing and feature lists move between releases and none survive contact with your data. Verify features and limits against current documentation, and performance against your own benchmark on your own vectors.

What the system actually has to do

Some shared vocabulary before the comparison.

Approximate nearest neighbour indexing

Exact search computes the distance from the query to every vector and keeps the best k. It needs no index, and its cost is linear in vectors times dimensions. For tens of thousands of vectors that is genuinely fine, which is worth internalising early.

Approximate nearest neighbour (ANN) search trades exactness for speed: it returns most of the true neighbours most of the time, and the fraction it gets right is recall. Two index families dominate.

HNSW, the hierarchical navigable small world graph, is a layered proximity graph: sparse upper layers allow long hops, dense lower layers refine, and a query enters at the top, walks greedily toward the target and descends. It is fast and accepts incremental inserts without training, but memory-hungry, holding vectors plus a graph of edges over them. Its knobs are graph degree and index quality, fixed at build time, plus a query-time search width trading latency for recall.

IVF, the inverted file index, partitions instead: cluster a sample, assign each vector to a cell, and search only the cells whose centroids sit nearest the query. Memory overhead is far lower, but it needs a training pass first, its quality depends on that sample matching reality, and a drifting corpus degrades the partitioning until you retrain. Its query knob is how many cells to probe.

Quantization is orthogonal and is how large indexes become affordable. Scalar quantization stores fewer bits per dimension, product quantization replaces subvectors with codebook entries, binary quantization goes furthest. All shrink memory substantially, all cost recall, and all pair with rescoring: pull a wider candidate set from the compressed index, then re-rank against full-precision vectors on disk. When a system fits an enormous index into modest memory, quantization is why, and recall is the question to ask.

The recall, latency and memory triangle

You pick two. High recall and low latency means an uncompressed graph in RAM, and you pay for the RAM. High recall cheaply means probing wider and waiting. Speed cheaply means quantizing hard and losing neighbours. This is why a throughput figure quoted without a recall level is meaningless: any system looks fast with recall turned down.

Filtered search

Here is what actually separates these systems, and what most comparisons skip.

Real queries are rarely "find the nearest vectors". They are "find the nearest for this tenant, in this language, published after this date, excluding archived documents". That is not an intersection you bolt on afterwards.

Post-filter, and a selective predicate leaves almost nothing: ask for ten, retrieve a hundred candidates, find three that match, then return three or re-query wider.

Pre-filter, and the graph stops working as built. HNSW navigates by hopping between neighbours; exclude most nodes and the traversal strands itself with no permitted neighbour to move to, so recall collapses even though the answer sits in the data. The pathological case is the one production hits most: a selective filter over a large corpus.

Systems diverge here. Some evaluate the predicate during traversal, skipping excluded nodes as results while still using them as stepping stones. Some index metadata and use estimated selectivity to choose between traversal and a direct scan of the subset. Some partition physically along a filter dimension. Test this; it is where the differences live.

Hybrid search

Dense retrieval has a known weakness: exact terms. Product codes, error identifiers, CVE numbers and rare acronyms are what a security or platform audience searches for, where embeddings blur and keyword indexes win.

Hybrid search runs a dense query and a sparse or keyword query, usually BM25, then fuses the results. The scores share no scale, so you either normalise and weight, which is sensitive to distribution shift, or fuse by rank position, with reciprocal rank fusion the standard choice.

The operational surface

What decides whether you are happy in a year: persistence and recovery time, replication for read scaling or failover, backups that restore into agreement with your source of truth, tenant isolation, horizontal scale, and whether index builds starve live queries.

pgvector

pgvector is a PostgreSQL extension. That sentence is both the architecture and the argument.

Vectors become a column type in an ordinary table beside the relational data they describe. A filtered query is a WHERE clause, a query spanning three tables is a join, and a document and its embedding are written in one transaction, so you cannot index a vector for a rolled-back row. You inherit backup, point-in-time recovery, replication, row-level security, roles and grants, your monitoring, and people who know how it fails.

The design centre is "you already run Postgres". If that holds, vector search costs one extension and one index, while a dedicated vector database costs an entire additional system.

It stops being right when the index outgrows affordable memory, when vector concurrency contends with your transactional workload, when you need the filtered-search strategies purpose-built engines have invested in, when you want scale-out of the index, or when hybrid search and reranking must be built-ins. One middle step first: move vector queries to a read replica so the workloads stop competing.

Qdrant

Qdrant is a purpose-built vector search engine written in Rust, run standalone or consumed as a managed service. Its design centre is filtered vector search done properly, with a small operational surface. Payloads are first-class and indexed, and the engine uses filter selectivity to pick an execution strategy rather than fixing one. It offers quantization with rescoring, on-disk storage, atomic index swaps through aliasing, and replication.

Choose it when filtered search is central, you have outgrown a database extension, and you want a dedicated engine without a distributed apparatus. It is the wrong choice when your vectors are few and your filters are really joins against data elsewhere: you will spend your time keeping two stores in agreement.

Weaviate

Weaviate puts an object and schema model at the centre: classes with typed properties and references, storing objects with vectors attached rather than vectors with metadata. Its modules let the database call an embedding model itself, so you write text and vectorisation happens server-side, and the pattern extends to reranking. Hybrid search is a built-in query mode with fusion handled for you, and it runs as a standalone service, self-hosted or managed.

Choose it when you want retrieval to be a capability of the datastore rather than a pipeline you own, when hybrid search is a requirement, and when a typed schema models your domain better than flat metadata. It is the wrong choice when you want the embedding step under your own version control, which for many teams is right: the model changes most often, and a change demands a controlled reindex.

Milvus

Milvus is built for large-scale distributed deployment: coordination, ingestion, query execution and index building are separate components over object storage, with a log layer for durability and replay. They scale independently, so a heavy index build need not compete with query serving.

Right when you genuinely operate at large scale: indexes in the high hundreds of millions or billions of vectors, sustained ingestion alongside queries, and the ability to run a distributed system with many moving parts. Wrong for almost everyone else: running it well means running its infrastructure well, and a smooth embedded dev mode should not persuade you the production topology is light. If your corpus fits on one large machine, you are buying complexity you will not use.

Chroma

Chroma optimises for the first hour. It runs embedded in your application process, the API is small and obvious, and semantic search over a directory of documents takes a few lines and no server, with client-server mode when that stops being enough. That is undervalued: many vector workloads are prototypes and internal tools, where the right operational investment is near zero.

Choose it for prototyping, for local development against the API your tests use, and for small applications with human-scale load. It is the wrong choice when you need durability, concurrency and backup guarantees you would put a business on, or when filtered performance at scale is a hard requirement. It competes with an in-process index plus a file on disk, a legitimate place to be.

Comparison at a glance

SystemDesign centreDeploymentFilteringOperational burdenBest fitOutgrow it when
pgvectorVectors beside relational dataExtension on your PostgresSQL predicates; graph strained by selective filtersNear zero if Postgres is yoursPostgres shops; filters that are joinsIndex outgrows memory; OLTP contention
QdrantFiltered search, dedicated engineStandalone serviceIndexed payloads, selectivity-awareModerate; one serviceFilter-heavy retrieval past an extensionScale needs separated components
WeaviateRetrieval as a datastore capabilityStandalone serviceTyped schema properties, hybrid-integratedModerate, plus modulesHybrid search, server-side embeddingYou want the embedding pipeline
MilvusDistributed scale, separated componentsCluster on object storageScalar indexes, filtered ANN per segmentHigh; many componentsVery large indexes, sustained ingestRarely upward; often over-provisioned
ChromaTime to first resultEmbedded, or client-serverMetadata predicates over collectionsVery lowPrototypes, internal tools, small appsDurability or filtered performance matter

Do you actually need a dedicated vector database?

Guidance in orders of magnitude; real thresholds depend on dimensionality, filter patterns, latency budget and hardware.

At thousands to tens of thousands of vectors you need no ANN index at all. Exact search over that many embeddings is a matrix multiplication an in-process library or a plain table answers inside a typical request budget. It is exact, there is no recall to tune, and it is trivially debuggable. Teams skip this and adopt an approximate index whose recall loss they never measure.

At low hundreds of thousands, an extension on your existing database is comfortable: the index fits in ordinary memory, filters are SQL, consistency is free. At single-digit millions it still works but tuning matters, and a dedicated engine becomes defensible, particularly if filtering is heavy. Beyond tens of millions, or at high sustained concurrency, a dedicated engine is the default, and at the top end a distributed one.

Four pressures push you off the simple option and nothing else should: the index no longer fits in memory you will buy; query concurrency contends with your transactional workload; filtered search is demanding enough that naive strategies miss your recall target; or you need horizontal scale of the index.

"We might scale later" is not on that list, and is a poor reason to take on a second datastore now: the cost is certain and immediate, the benefit uncertain and deferred. Keep the embedding pipeline separate from the store and that future migration is a reindex, not a rewrite.

The operational argument

A dedicated vector database is a stateful service. You deploy it, network it, terminate TLS, configure authentication, monitor it, capacity-plan its memory, back it up, test the restore, and upgrade it through releases where index formats change. Someone carries the pager for it.

Two stores with no shared transaction also drift. A document is updated and the embedding write fails, or the reverse, and the index disagrees with your source of truth. Nothing errors; it returns plausible but wrong documents, a correctness bug shaped like a quality complaint. You will need reconciliation and a staleness budget. Weigh that against the gain measured on your own workload at fixed recall, and if it is imperceptible at your scale, stay put.

Security and tenancy

Start from the fact that does the most damage when missed. Embeddings derive from your source documents, and the index is a copy of your corpus with none of the original permissions attached unless you attach them. Chunks are often stored beside their vectors close to verbatim, and retrieval hands them to a model that shows them to a user. Whatever access control governs the documents must govern the index, enforced at query time, not assumed.

Two tenancy models, each with a characteristic failure. A collection per tenant makes isolation structural, the stronger guarantee, but every index carries fixed overhead and thousands of tenants become their own operational problem. A shared collection with a tenant identifier in metadata scales better, but correctness now depends on every query path including the filter.

That second model has a well-known failure mode: one code path omits the filter and results cross tenants. Nothing errors; a user simply receives another organisation's documents, paraphrased by a model so the provenance is invisible. Defend structurally, not by care: funnel every vector query through one retrieval layer that requires tenant scope and refuses to run without it, and test the negative case. Prefer a system that can enforce scoping server-side by credential rather than by a client-supplied predicate: a filter the client can omit is one the client will eventually omit. The same holds inside a tenant, where per-user permissions belong in the index as filterable metadata and revocation lag is real exposure.

Then the basics. Several of these systems ship development configurations with authentication disabled, and instances have been found exposed on the public internet with no credentials. Never expose one without authentication and transport encryption. Encrypt at rest at the volume layer, and remember that backups inherit the corpus's sensitivity exactly. Watch logging: query text is user input that can contain credentials, and retrieved content in a debug log is your corpus in a log aggregator. Finally, an embedding is not a hash but a lossy, informative representation, and research has repeatedly demonstrated reconstruction of source text from embeddings. Classify the store at the level of the documents it came from.

How to benchmark fairly

Published benchmarks almost always use synthetic or public datasets, without filters, on a static corpus, single-tenant and fully cached. None of that is your workload.

Benchmark with your own vectors and dimensionality, the model you actually use, and your real filter patterns including the selective ones. Compute ground truth with exact search over a sample, tune each candidate to the same recall target, and compare latency and cost at that fixed recall; throughput compared across differently tuned systems compares nothing. Measure with the index built and warm, report build time and peak memory separately, drive realistic concurrency, and read tail percentiles, not means.

Migration and lock-in

Vectors are portable: arrays of floats any system takes. Index configuration, filtering semantics, fusion behaviour and query APIs are not, and that is where migration work lives.

Keep the embedding pipeline outside the store. Own a component that chunks documents, calls the model and emits vectors with metadata, and treat the store as a sink. Keep the source of truth in your primary database and the index derived and rebuildable. Migration then becomes: stand up the new store, replay the pipeline, dual-read and compare, cut over. You will use that reindex path more for model upgrades than migrations anyway, since changing the model invalidates every stored vector, so build it early.

A decision checklist

  • How many vectors today, and how many in twelve months, in orders of magnitude?
  • Do you run PostgreSQL, and has pgvector been measured and found wanting, or only assumed insufficient?
  • What fraction of queries carry a filter, and how selective are the worst ones?
  • Do users search for exact identifiers and codes? If so you need hybrid search.
  • What is the latency budget, and at what recall does the answer stop being acceptable?
  • Is the workload multi-tenant, and where is tenant scope enforced so it cannot be omitted?
  • Who operates this at three in the morning, and have they run one before?
  • What is the reindex path when the embedding model changes, and has it been run?

If the first two answers are "not many" and "yes, but unmeasured", stop reading comparisons and go measure. The most common correct outcome of a vector database evaluation is deciding you do not need one yet.