Intermediate 11 minStack

pgvector: vector search in PostgreSQL

Direct response

pgvector is an open-source PostgreSQL extension (PostgreSQL license, permissive) that adds vector storage and search to a database you already administer, with indexable columns up to 2,000 dimensions (4,000 in half precision). For a local corpus of a few hundred thousand chunks, it means one fewer service to operate and SQL filters that work, with no synchronization to maintain with your relational data.

pgvector is a PostgreSQL extension that adds vector storage and search to a database you already administer. For most local document-search projects, that means one fewer service to run, one fewer backup to organize, and SQL filters that actually work—including on access permissions. The project, maintained under the PostgreSQL license and hosted on GitHub, had more than 23,000 stars at the end of September 2026, at version 0.8.6, compatible with PostgreSQL 13 and later.

By Mohamed Meguedmi·Update 2026-09-28·Tested on Windows, macOS, and Linux

#The argument: don't add a service

A local document-processing setup already runs a model server, an encoding step, and document storage. Adding a dedicated vector database means one more container, one more port, one more backup, and one more thing that can become out of sync with your relational data when a document is deleted.

If PostgreSQL is already there — and in a business application, it almost always is — pgvector eliminates this entire category of problems. Your text chunks live in a table alongside the documents they came from, with foreign keys keeping them consistent, and a deletion propagates as you’d expect, with no separate cleanup task to write or monitor. The project also adds exact and approximate search, single-precision, half-precision, binary, and sparse vectors, five distances (L2, dot product, cosine, L1, Hamming, Jaccard), and inherits PostgreSQL’s ACID compliance, point-in-time recovery, and joins for free.

#How it works in practice

The Local RAG Kit

Your documents, your AI: a reliable local RAG over your PDFs, notes and mail — nothing leaves your machine.

  • Lifetime online access
  • PDF + files
  • Lifetime updates

You store a vector column alongside your usual columns. A query sorts rows by their distance from the question vector and returns the closest ones. Three distance operators cover the common cases, and the one you choose must match the embedding model’s convention: this is the most common source of quietly mediocre results. For vectors normalized to length 1 (as with OpenAI and most recent embedding models), the dot product is the fastest to calculate and produces the same ranking as cosine similarity.

The puzzle pieces
ItemWhat it isWorth watching
Vector columnA fixed-dimensional floating-point arrayThe dimension comes from the embedding model and doesn’t change without re-encoding everything
Distance operatorCosine, dot product, or Euclidean (L2)Must match the model
HNSW indexGraph index, fast queriesSlow, memory-hungry build; the sensible default since version 0.5
IVFFlat indexPartition index, inexpensive to buildTo be created once representative data is available
No indexExact traversal of every linePerfectly viable up to a few tens of thousands of lines

#The minimum SQL to get started

  1. 01
    Enable the extension
    CREATE EXTENSION IF NOT EXISTS vector; — a single command, to run once per database.
  2. 02
    Add the column
    ALTER TABLE chunks ADD COLUMN embedding vector(1024); — the dimension must exactly match that of the embedding model used.
  3. 03
    Load the data before indexing
    Bulk loading via COPY is faster without an existing index; create the index once a representative number of rows is present.
  4. 04
    Create the index
    CREATE INDEX CONCURRENTLY ON chunks USING hnsw (embedding vector_cosine_ops); — the CONCURRENTLY option prevents writes from being blocked during construction, which can take a long time on a large table.
  5. 05
    Query
    SELECT contenu FROM chunks WHERE document_id IN (SELECT id FROM documents WHERE utilisateur_autorise($1)) ORDER BY embedding <=> $2 LIMIT 5; — authorization filtering and similarity sorting in the same query.

#Dimension limits, in concrete terms

pgvector defines four column types, each with its own indexable dimension limit. The standard vector type (single precision, 4 bytes per element) can be indexed up to 2 000 dimensions, covering most common open-source embedding models (384 to 1 024 dimensions). A larger model such as OpenAI's text-embedding-3-large, with 3 072 dimensions by default, exceeds this limit: the documented workaround is either to reduce the dimension when generating the embedding (the API supports this) or switch to the halfvec type, which stores values in half precision (2 bytes per element, half the space) and can be indexed up to 4 000 dimensions. The bit type (binary vectors, Hamming or Jaccard distances) supports up to 64 000 indexable dimensions, while sparsevec (sparse vectors) supports 1 000 indexed nonzero elements—beyond that, PostgreSQL still stores the column (up to 16 000 dimensions for vector, halfvec, and sparsevec) but cannot index it, requiring an exact scan.

→
Quantization is more than a storage detail
Switching from vector to halfvec cuts the size of each row in half, fitting more indexes in memory and speeding up large-scale queries without changing your embedding model. Binary quantization (bit type) goes further: it compresses the index for search, then reranking against the full vectors restores the lost precision—the method documented by the project itself to keep a large index entirely in memory instead of spilling it to disk.

#Filtering and access rights

A real-world question rarely concerns the entire corpus: you want the passages from this service, after this date, in the documents this user is authorized to read. In a dedicated vector database, this is a metadata filter with its own syntax and edge cases. In PostgreSQL, it is a WHERE clause alongside a similarity sort, joined to your users table if necessary.

A pitfall documented by the project itself is worth knowing before you discover it in production: with an approximate index (HNSW or IVFFlat), the filter is applied after the index scan, not before. If a condition retains only 10% of rows and the default hnsw.ef_search parameter is 40, only 4 rows surface on average—not the ten requested. The official workaround, available since version 0.8.0, is called iterative index scanning (SET hnsw.iterative_scan = strict_order), which automatically reruns the scan until it finds enough results instead of silently returning a truncated set.

!
Permissions are not set in the prompt
When several people query the same index, the permissions filter is what prevents a model from citing a document to someone who should not see it. No prompt instruction can replace this filter, and expressing it in SQL against your existing authorization model is much safer than reimplementing it.

#Where it stops

Large-scale memory
Millions of full-precision vectors are bulky; halfvec and binary quantization reduce the footprint, but dedicated engines push compression further natively. This is the clearest gap.
Index build time
An HNSW index on a very large table is slow and resource-intensive; in production, building it with CREATE INDEX CONCURRENTLY avoids blocking writes during the operation.
Competition
Putting heavy vector search alongside your transactional workload puts both on the same server. Read replicas help; separating the roles helps more.
The vector dimension
Indexed vectors have a limit based on their type (2,000 for vector, 4,000 for halfvec). Common models work; a very high-dimensional model must be reduced or quantized.
Hybrid search
PostgreSQL supports full-text search (tsvector), and you can combine it with vector distance, but the ergonomics are up to you: you have to run the two queries separately and then merge the rankings, for example with Reciprocal Rank Fusion, instead of getting a native hybrid score in a single query.
Horizontal scaling
Beyond a single server, the documented approach uses PostgreSQL read replicas or a distribution tool such as Citus or PgDog—an additional component, contrary to the original argument.
i
VACUUM can be slow on an HNSW index
The official documentation explicitly points this out: cleaning (VACUUM) a table whose vector index uses HNSW can take time on a large dataset. Speeding it up means running REINDEX INDEX CONCURRENTLY before VACUUM instead of letting the maintenance operation run on its own—a deployment detail few tutorials mention before overnight maintenance overruns its window.

#pgvector or a dedicated database

The question isn’t which one is objectively better, but which one fits your situation today. pgvector wins when PostgreSQL is already the application’s system of record: billing, accounts, and source documents. A dedicated engine wins when the vector volume or query throughput exceeds what a single transactional server can handle without degrading the rest of the application, or when the team prefers to isolate the AI component from the rest of the information system for operational rather than pure performance reasons.

Choose based on the situation
SituationChoice
PostgreSQL already in place, fewer than a few hundred thousand chunkspgvector, comfortably
Vectors must remain consistent with relational datapgvector: transactions provide it for free
Fine-grained filtering based on existing permissionspgvector
An embedding model with more than 4,000 dimensions and no option for reductionCheck the sparsevec type, or use a dedicated database designed for this use case
Millions of vectors, many queriesA dedicated engine (Qdrant, Milvus)
Prototype in a notebookAny of them; this choice is reversible

#FAQ

Is pgvector fast enough for RAG?+
For a typical local corpus—from a few dozen to a few hundred thousand chunks—yes, with an HNSW index, and often even without an index at the lower end of that range. Retrieval is rarely the slow step in a local pipeline: model generation dominates perceived response time.
pgvector or Qdrant?+
Use pgvector if PostgreSQL is already in place and your data is relational: fewer services, transactional consistency, native SQL filters, and a single backup to manage. Use Qdrant when scale, aggressive memory quantization, or a dedicated service designed entirely for vector workloads matter more than the operational simplicity of a single database. Both honestly meet the same need up to several hundred thousand vectors.
Which index should you choose, HNSW or IVFFlat?+
HNSW has been the default for several versions: better query performance at the cost of slower construction and higher memory usage. IVFFlat is cheaper to build but must be created after representative data has already been loaded; otherwise, partitions may be poorly distributed.
Can I filter by metadata?+
Yes, in ordinary SQL, including joins. This is one of the best reasons to choose it, especially for filtering by access rights. However, note that with an approximate index, this filter is applied after the index scan, which can return fewer results than expected without iterative scanning.
What happens if I change the embedding model?+
Everything must be re-encoded: the vector dimensions and geometry belong to the model, not the database. This is a GPU batch operation, not a schema migration, and you must recreate the column if the new dimension exceeds the declared one. Plan a cutover window, because the old index remains valid until re-encoding is complete.
My embedding model has more than 2,000 dimensions. What should I do?+
The standard vector type indexes up to 2,000 dimensions. Beyond that, use the halfvec type (up to 4,000, in half precision) or reduce the dimension during generation if your provider allows it, as OpenAI does for text-embedding-3-large. Without an index, PostgreSQL still stores up to 16,000 dimensions, but uses an exact scan.
How do you diagnose a slow vector query?+
The documentation recommends EXPLAIN (ANALYZE, BUFFERS) before the query to see whether the index is actually being used and how many blocks are read. An exact query without an index benefits from increasing max_parallel_workers_per_gather; a slow approximate query often points to an index that is still being built or insufficient memory to keep it cached.
Did this guide help you?

Feedback, an error, or a clarification? Let us know—it improves the guide for everyone.