Guide

pgvector vs dedicated vector databases: when Postgres is enough

Postgres with pgvector is usually enough if you already run Postgres, your vectors fit comfortably on it and your metadata lives alongside them. A dedicated vector database earns its place when your own load tests show that vector search is overloading the database or needs features pgvector lacks.

The real choice

This is less a contest between two products than a decision about how many systems you want to run. Keeping vectors in Postgres means one database, one backup process and one set of permissions, at the price of sharing resources with everything else. A dedicated system separates that workload and offers features built for vector search, at the price of something else to deploy, secure and keep in sync.

The broader guide on embeddings and choosing a vector database covers embedding models and general criteria. This one looks specifically at the Postgres question. Version-sensitive details below come from the pgvector README at version 0.8.7, so check it again before relying on them.

What pgvector gives you

pgvector is an open-source Postgres extension, supporting Postgres 13 and later, that adds vector types, distance operators and approximate indexes.

  • Types. vector and halfvec for dense embeddings, sparsevec for sparse ones and bit for binary vectors.
  • Distance operators. These include <-> for Euclidean distance, <#> for negative inner product and <=> for cosine distance.
  • Indexes. HNSW and IVFFlat. Without an index, you get exact search, with perfect recall and a scan of every row in scope.

Because it is still Postgres, vectors sit next to your data, so you get joins, transactions and existing backup and access tooling. A document and its embedding can change in one transaction, and there is no pipeline that can drift out of sync. That property is easy to undervalue until you have debugged a stale external index.

Here is an illustrative setup with a tenant filter and an HNSW index. The 1024 dimensions are only an example and must match your embedding model:

-- Illustrative only. Adjust names, dimensions and options to your data.
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
    id bigserial PRIMARY KEY,
    tenant_id bigint NOT NULL,
    body text NOT NULL,
    embedding vector(1024) NOT NULL
);

CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON chunks (tenant_id);

SET hnsw.iterative_scan = strict_order;

SELECT id, body
FROM chunks
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;

Limits that are worth knowing

Dimensions

A vector column can hold up to 16,000 dimensions, but only up to 2,000 can be indexed. halfvec raises the indexable limit to 4,000, using half-precision values. If your embedding model produces more than that, you will need a shorter output from the model, if it offers one, or another route such as the binary quantisation options described in the README.

Filtered search

This is the most common surprise. With an approximate index, filters are applied after the index scan. By default HNSW considers 40 candidates (hnsw.ef_search), so if a filter matches only a small fraction of rows, a query for ten results may return fewer.

Version 0.8.0 added iterative index scans, which keep scanning until there are enough results or a limit is reached. For HNSW you enable them through hnsw.iterative_scan, with strict_order or relaxed_order, and hnsw.max_scan_tuples caps the work. The README also suggests ordinary indexes for selective filters, partial indexes for a few distinct values and partitioning for many. For tenant isolation specifically, it points to list partitioning or separate tables.

Tuning and memory

HNSW is built using options m and ef_construction, which default to 16 and 64, and searched with hnsw.ef_search. Higher values generally trade speed and resources for better recall. Index builds can be demanding on a busy primary, and search performs best when the index fits in memory. A vector-heavy workload can therefore compete with your transactions for cache, CPU and I/O, which you should measure instead of assuming either way.

Hybrid search

Postgres has built-in full-text search, and the README suggests combining it with vector search using reciprocal rank fusion or a cross-encoder. That works, and it avoids another system, though you assemble the query yourself. Ranking functions such as ts_rank_cd are not the same as the BM25 scoring offered by many search engines, so test keyword quality on your own queries if it matters. Hybrid search and reranking for RAG explains the fusion approach.

When Postgres is enough

Staying with pgvector is a strong default when most of the following are true:

  • You already run Postgres and have people who know how to operate it.
  • Vectors belong with relational data. You need joins to documents, users or permissions, or transactional updates.
  • Your collection fits comfortably. The index and its working data fit in the memory you can afford, leaving room for everything else.
  • Query volume is moderate. Your load tests hit your latency target with headroom.
  • Exact search may be practical. With a small collection, or where filters narrow each query to a small set of rows, you might not need an approximate index at all.
  • You value fewer moving parts. For an early product, one less system to secure and sync can matter more than peak search performance.

Beware of published thresholds such as "pgvector works up to N million vectors". The answer depends on dimensions, hardware, filters, recall targets and write rates, so a number from someone else's benchmark tells you little about your system.

When a dedicated database earns its place

Consider moving, or adding one, when you can point to a specific reason such as these:

  • Measured contention. Vector queries or index builds are hurting your transactional workload, and a read replica or a separate Postgres instance has not solved it. Try those first, as they are often cheaper than changing technology.
  • Scale beyond a comfortable single node. The index no longer fits in memory on hardware you are willing to pay for, and you want built-in sharding. Postgres can be scaled out, but that is its own project.
  • Selective filters at high volume. You cannot reach your recall and latency targets for your filter patterns after tuning.
  • Features you would otherwise build. Native sparse and dense fusion, built-in embedding or reranking, or specific quantisation options may matter. Availability varies by product and changes often, so check each vendor's current documentation.
  • No Postgres to begin with. If your stack does not use it, adopting it purely for vectors removes much of the main advantage. Compare it fairly with your other options, including vector support in whatever database you do run.
  • A managed service fits your team. If nobody wants to tune indexes, paying someone else to run search may be reasonable. Include data residency and exit costs in that decision.

The costs of a second system

Licence or hosting fees are the visible part. The less visible costs often decide the matter:

  • Synchronisation. Changes must reach the index, including deletes, and you need a way of detecting and repairing drift. A deleted document that remains searchable can be a compliance problem.
  • Permissions. Access rules must be copied across as metadata and enforced correctly. A mistake here leaks data between users.
  • Query splitting. Joins now happen in your application, and that often means fetching IDs from one system and then rows from another.
  • Operations. Backups, monitoring, upgrades and incident response all have to cover another service.

Test the decision on your own data

A short experiment beats any comparison article, including this one.

  1. Use realistic data. Your real embeddings at your expected size, or at a multiple of today's if you expect growth.
  2. Replay realistic queries. Include your filters, particularly the selective ones, at your expected concurrency and alongside normal database load.
  3. Measure recall against exact search. Fast but wrong results are not a win, so compare approximate results with an exact baseline.
  4. Test writes and rebuilds. Measure index build time, how searches behave during bulk updates, and how routine maintenance such as vacuum copes.
  5. Compare at a fixed recall. Latency numbers mean little unless both systems are tuned to the same recall.

Recall itself is simple to compute once you have both sets of results. The sample below is an illustrative sketch, and both arguments are assumed to be lists of result IDs per query:

# Illustrative only. Compares approximate results with an exact baseline.
def recall_at_k(approx_results, exact_results, k):
    total = 0.0
    for approx, exact in zip(approx_results, exact_results):
        expected = set(exact[:k])
        total += len(set(approx[:k]) & expected) / len(expected)
    return total / len(exact_results)

Remember that index recall is not the same as answer quality. It tells you how well the index approximates exact search, so you still need retrieval evaluation against real questions, and a better chunking strategy may help far more than a different database.

Keep the decision reversible

Put retrieval behind a small interface of your own, such as a function that takes a query, filters and a limit and returns passages with scores. Store your source text and embedding model version, so that you can rebuild an index anywhere. Then starting on pgvector is low-risk: if it stops being enough, you will have real measurements showing why, and a clear boundary at which to swap it.

If you are designing retrieval for a product, the RAG development page describes what I can help with.

Frequently asked questions

How many vectors can pgvector handle?

There is no single dependable figure. Capacity depends on dimensions, memory, filters, recall targets, write rate and the other work your database is doing. Load test with your own embeddings and queries instead of relying on a general threshold.

Should I use HNSW or IVFFlat in pgvector?

HNSW is a common starting point when you want good query performance, though it uses more memory and takes longer to build. IVFFlat is quicker and lighter to build, but should be created after data is loaded. Test both if the resources matter.

Can pgvector do hybrid search?

Yes. You can combine pgvector with Postgres full-text search and merge the rankings, for example using reciprocal rank fusion. You write more of this yourself than in systems with built-in fusion, and keyword ranking differs from BM25.

Should vector search run on my main Postgres database?

It can, particularly at small scale, as it keeps things simple. If searches or index builds start affecting your application, try a read replica or a separate Postgres instance before you move to a different database technology.