IntermediateAnalysisDecision Guides

Choosing a vector store

pgvector, Qdrant, Weaviate or Elasticsearch. Whether you already run PostgreSQL, and how you filter, decide it - not throughput charts.

Updated Reviewed

The question, stated precisely

You are choosing which system stores your embeddings and answers nearest-neighbour queries, and therefore which system your team backs up, upgrades, monitors and pages on. That is the decision. It is not a decision about which index algorithm is better, because pgvector, Qdrant, Weaviate and Elasticsearch all offer an HNSW graph and all of them will return good enough neighbours for a corpus that fits on one machine. What differs is what happens when a WHERE clause meets that graph, and what it costs to operate.

Three things are explicitly not being decided here.

Not the embedding model. Dimension count, the choice between a hosted and a self-hosted encoder, and whether to normalise vectors are upstream decisions. They constrain the store only through dimension limits, and those are documented per store.

Not the chunking strategy. How you split documents determines retrieval quality far more than which store holds the results. It is the same work in every option here.

Not whether to retrieve at all. If the question is retrieval versus adapting the model, see retrieval or fine-tuning. This guide assumes you have concluded you need vector search as part of a RAG pipeline.

The three factors that genuinely decide it

1. Whether you already run PostgreSQL

This dominates, and it is not a statement about index quality. If you already operate PostgreSQL, pgvector removes a whole system from the architecture. The vectors sit in the same table as the document metadata, so retrieval is a join rather than two round trips reconciled in application code; writes are transactional, so a chunk and its embedding cannot diverge; and the vectors are covered by the write-ahead log, which means your existing streaming replication and point-in-time recovery already protect them. The pgvector README states the property directly: vectors get ACID compliance, point-in-time recovery and JOINs, and the extension supports PostgreSQL 13 and later (pgvector README).

The alternative is a second stateful service with its own upgrade cadence, its own backup mechanism, its own authentication model, and a dual-write problem you now own. Nothing in a dedicated store buys that back unless factor 2 applies to you.

The cost of taking pgvector is specific and known: the approximate index changes which rows come back, vacuuming an HNSW index is expensive, and indexed dimensions are capped below stored dimensions. Those mechanics belong on the pgvector page and are not repeated here. Read them before choosing, because they are the work you are accepting.

2. Filtering shape, which is where the stores actually differ

Almost every real query is filtered: one tenant, one document set, one date range. How each system combines the filter with the graph traversal is the substantive technical difference, and it is documented, not benchmarked.

Store When the filter is applied Documented mechanism
pgvector After the index scan The candidate set from the approximate index is filtered afterwards, so a selective WHERE can leave fewer rows than the LIMIT asked for. Iterative index scans, added in 0.8.0, keep scanning until enough rows are found (pgvector README)
Qdrant During traversal, using filter-aware edges The HNSW graph is extended with extra edges derived from indexed payload values, so filters can be applied while searching the graph (Qdrant indexing)
Weaviate Before the search, as an allow-list An inverted index in each shard builds a list of eligible ids, which is passed into the HNSW index; traversal follows edges normally but only admits ids on the list (Weaviate filtering)
Elasticsearch During approximate search The filter in a knn clause is applied during the search so that k matching documents are returned, rather than after, which could return fewer (Elastic kNN search)

Each of these has a documented escape hatch for the case where the filter is so restrictive that graph traversal degenerates. Weaviate switches to a flat brute-force search below a configurable cutoff, documented as around 15% of the dataset by default. Lucene, under Elasticsearch, bypasses the graph per segment when the filtered document count is at or below num_candidates, and abandons the graph mid-traversal if the number of nodes explored exceeds the number of documents satisfying the filter (Elastic kNN search). Qdrant’s planner uses a full_scan_threshold to prefer a full scan when the estimated matching set is small, and from v1.16.0 offers the ACORN algorithm for the case where combining two or more strict filters disconnects parts of the graph (Qdrant indexing). Weaviate has made its own ACORN implementation the default filter strategy from v1.34, with sweeping as the alternative (Weaviate filtering).

The pattern is worth naming: every option has a story for weak filters and a story for very strong ones. The awkward band is the middle, where the filter is too restrictive for an unaided graph and too broad to scan. That band is what Qdrant’s filter-aware edges and Weaviate’s allow-list are for, and it is the strongest technical argument for a dedicated store.

There is a catch on the Qdrant side that is easy to get wrong. Payload indexes should be created before ingesting data, because the extra filter-aware edges are only generated after the payload index exists; create the index later and the HNSW index has to be rebuilt to benefit (Qdrant indexing). Weaviate has a comparable irreversibility: indexRangeFilters is only available for new properties, and existing properties cannot be converted to use it (Weaviate filtering). Both are schema decisions taken at design time that are painful to revisit.

3. Operational burden, and whether the index can be updated in place

Ask two questions of any candidate: what happens as rows change, and who runs it.

On rebuild behaviour, the documented facts diverge. An HNSW graph is maintained incrementally in all four, but the surrounding structures are not. In Qdrant, adding a payload index after ingest requires an HNSW rebuild to gain filter-aware edges, as above. In Elasticsearch, vectors are stored per segment as an HNSW graph or as DiskBBQ clusters, and building those structures is compute-intensive enough that Elastic advises raising client timeouts on index and bulk operations (Elastic kNN search). With pgvector the equivalent cost is vacuum on the graph, which is why append-mostly embedding pipelines behave better than in-place updates.

On memory, Elastic is explicit that for HNSW all vector data must fit in the node’s page cache for efficient performance (Elastic kNN search). Treat that as the general rule, not an Elasticsearch quirk: the working set is the thing you size, and quantisation is how every one of these systems reduces it.

On multi-tenancy, which is where operational and filtering concerns meet, Qdrant documents three approaches inside one collection: partition by payload with a keyword index marked is_tenant (available from v1.11.0, which co-locates a tenant’s vectors in storage), user-defined sharding that gives a tenant its own shard, and a tiered combination. It advises against a collection per tenant because each collection carries resource overhead, and notes that Qdrant Cloud limits a cluster to 1000 collections by default. That is a real design constraint, and it is the sort of thing no benchmark surfaces.

What most comparisons over-weight

Raw QPS. A queries-per-second figure describes one dataset, one dimension count, one recall target, one filter selectivity and one machine, none of which are yours. Recall and throughput trade against each other continuously, so any single number is a point chosen by whoever published it. Vendor-run benchmarks are worse still, because the tuning effort is asymmetric by construction. If throughput is genuinely the deciding factor, measure it on your own vectors and your own filters, and report recall alongside latency or the number means nothing.

Index algorithm names. All four options here implement HNSW. Knowing that tells you almost nothing, because the differences that show up in production are in the filtering integration described above, in what quantisation is available, and in the operational surface. A comparison table whose distinguishing column is “supports HNSW” has not distinguished anything.

Feature breadth. Hybrid keyword-plus-vector retrieval, rerankers and built-in embedding inference are convenient, and every option is acquiring them. They rarely decide the architecture, because you can call an embedding API and fuse rankings yourself.

A decision path

Work down and stop at the first line that applies.

  1. If you already run PostgreSQL and your corpus fits comfortably on that server, use pgvector, because you remove a system instead of adding one, and transactional consistency between chunk and embedding is free, not engineered.
  2. If your queries are dominated by high-cardinality, moderately selective filters (per-tenant, per-collection, per-time-window) and you have measured recall falling short under pgvector’s post-index filtering, choose Qdrant or Weaviate, because filter-aware traversal and pre-filtered allow-lists exist specifically for that band.
  3. If you already run Elasticsearch or OpenSearch for keyword search and want one system for hybrid retrieval, add dense_vector fields there, because the filtering, aggregation and ranking machinery is already operated by your team.
  4. If none of the above applies, prefer the store your team can operate, and choose pgvector by default in an otherwise PostgreSQL-shaped stack. The decision is dominated by operational surface, not by retrieval quality.
  5. If tenant isolation is a hard requirement rather than a filter, decide the isolation mechanism first (partition, shard or separate table) and let the store follow, because that choice is harder to change than the store.

What would change the answer

  • The working set stops fitting one machine. Horizontal sharding of vectors is where dedicated stores and Elasticsearch have a native answer and pgvector requires partitioning or application sharding.
  • Recall under your real filters measured below requirement. This is the only measurement worth running, and it must use production query vectors and production filter selectivity.
  • The filter shape changes. A design that filters on one low-cardinality field is a different problem from one that filters on tenant plus date plus permission set. Qdrant’s own documentation notes that combining two or more strict filters can still disconnect graph components.
  • A schema decision becomes irreversible. Qdrant payload indexes before ingest and Weaviate’s new-properties-only range index are both cheaper to get right than to fix.
  • You acquire the second datastore anyway. If a queue, cache or search cluster arrives for other reasons, the argument in factor 1 weakens, because the marginal operational cost has already been paid.

Common wrong reasons

Choosing pgvector because it is “just Postgres”. It is a PostgreSQL extension with its own failure modes: approximate indexes change results silently, filtering is applied after the index scan, and indexed dimensions are capped below stored dimensions. Those are on the pgvector page, and choosing pgvector means owning them.

Choosing a dedicated store because vectors feel like they need one. Embeddings are a column type. The question is whether your filtering and scale actually exercise the mechanisms that a dedicated store adds. If your filters match most of the corpus, they do not.

Choosing Qdrant or Weaviate on a benchmark chart. Both have genuine filtering mechanisms worth choosing them for, described in their own documentation. Choosing them on published throughput instead means you cannot tell whether the thing you paid for is the thing that helps you.

Choosing Elasticsearch because it is already installed, without checking the resource shape. Approximate kNN wants vector data in the page cache and makes indexing compute-intensive. A cluster sized for logs is not automatically sized for embeddings.

Choosing on hosted-service convenience alone. Every option here has a managed form, so this is close to a tie, and it collapses into factor 1: does the managed service remove a component you would otherwise run, or add one.

Milvus is deliberately absent: its documentation did not load when this page was written, and describing it from memory would break the sourcing rule this site runs on.