Vector Search in Postgres with pgvector

AIPostgresVector DatabaseRAG
Share on LinkedIn

The team wanted vector search for a RAG feature and evaluated Pinecone, Weaviate, and Qdrant. Then someone asked: "Can we just use our Postgres?" We had 800,000 document chunks, already ran Postgres 16 with replication, and every query needed to join embeddings with document metadata, user permissions, and audit timestamps. Adding pgvector took an afternoon. Operating a second database would have taken a quarter. For our scale and query patterns, pgvector wasn't a compromise — it was the right tool.

Setup

CREATE EXTENSION vector;

CREATE TABLE documents (
    id         BIGSERIAL PRIMARY KEY,
    content    TEXT NOT NULL,
    embedding  vector(1536),  -- OpenAI text-embedding-3-small dimension
    tenant_id  TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now()
);

The vector(n) type stores a fixed-dimension float array. pgvector validates dimension on insert.

Inserting embeddings

import psycopg2
from pgvector.psycopg2 import register_vector

conn = psycopg2.connect(DATABASE_URL)
register_vector(conn)

embedding = openai.embeddings.create(
    input="document text", model="text-embedding-3-small"
).data[0].embedding

with conn.cursor() as cur:
    cur.execute(
        "INSERT INTO documents (content, embedding, tenant_id) VALUES (%s, %v, %s)",
        ("document text", embedding, "acme")
    )

The pgvector Python package registers the vector type adapter for psycopg2, asyncpg, and SQLAlchemy.

Similarity search

pgvector supports three distance operators:

Operator Distance Use for
<-> L2 (Euclidean) General similarity
<#> Negative inner product Normalized vectors
<=> Cosine distance Text embeddings (most common)
SELECT id, content, embedding <=> $1 AS distance
FROM documents
WHERE tenant_id = 'acme'
ORDER BY embedding <=> $1
LIMIT 10;

Cosine distance (<=>) is the standard choice for text embeddings. Pass the query embedding as $1.

Indexing for performance

Without an index, every query is a sequential scan — fine for thousands of rows, unusable for millions.

HNSW (recommended)

CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

HNSW builds a multi-layer graph for approximate nearest neighbor search. Key parameters:

Set search-time recall with ef_search:

SET hnsw.ef_search = 100;  -- default 40, higher = better recall

IVFFlat (memory-constrained alternative)

CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- Must run after sufficient data exists
-- lists = sqrt(row_count) is a common starting point

IVFFlat partitions vectors into clusters. Queries search only the nearest clusters. Requires enough data at index creation time for meaningful clusters.

Hybrid queries: the pgvector advantage

The killer feature is SQL — vector similarity plus relational filters in one query:

SELECT d.id, d.content, d.embedding <=> $1 AS distance
FROM documents d
JOIN users u ON d.author_id = u.id
WHERE d.tenant_id = $2
  AND d.created_at > now() - INTERVAL '30 days'
  AND u.role IN ('editor', 'admin')
  AND d.embedding <=> $1 < 0.3
ORDER BY distance
LIMIT 10;

Try doing this in Pinecone. You'd retrieve vectors, then make a second query to Postgres for permissions, then merge in application code. With pgvector, it's one query, one round trip, and the planner optimizes both the btree filters and the vector index scan.

Full-text + vector hybrid search

Combine tsvector with vector search for keyword + semantic retrieval:

WITH vector_results AS (
    SELECT id, content, embedding <=> $1 AS vscore
    FROM documents
    WHERE tenant_id = $2
    ORDER BY vscore LIMIT 50
),
text_results AS (
    SELECT id, content, ts_rank(search_vector, plainto_tsquery('english', $3)) AS tscore
    FROM documents
    WHERE tenant_id = $2
      AND search_vector @@ plainto_tsquery('english', $3)
    ORDER BY tscore DESC LIMIT 50
)
SELECT COALESCE(v.id, t.id) AS id,
       COALESCE(v.content, t.content) AS content,
       COALESCE(1 - v.vscore, 0) AS vector_score,
       COALESCE(t.tscore, 0) AS text_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY (COALESCE(1 - v.vscore, 0) * 0.7 + COALESCE(t.tscore, 0) * 0.3) DESC
LIMIT 10;

Scaling limits

pgvector on a single Postgres instance handles:

Insert throughput on HNSW indexes is lower than dedicated vector databases because each insert updates the graph. Batch inserts help. For write-heavy workloads (>10k vectors/second), a dedicated store with async ingestion may be better.

When to graduate to a dedicated vector DB

Until then, pgvector in the Postgres you already run is simpler, cheaper, and more flexible.

Common production mistakes

Teams get vector db pgvector postgres wrong in predictable ways:

Production implementations of vector db pgvector postgres fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.

Resources

Frequently asked questions

What is pgvector and how does it work?

pgvector is a Postgres extension that adds a vector data type and similarity search operators to standard PostgreSQL. You store embeddings in a vector column, create an approximate nearest neighbor index (HNSW or IVFFlat), and query with distance operators like <=> for cosine distance. It runs inside your existing Postgres instance, so vector search joins with relational data in a single SQL query without a separate vector database.

When should I use pgvector instead of a dedicated vector database?

Use pgvector when you already run Postgres, your dataset is under roughly 10 million vectors, you need SQL joins between embeddings and relational data, and your team wants one database to operate. Switch to a dedicated vector database when you need horizontal sharding beyond a single Postgres instance, sub-10ms latency at hundreds of millions of vectors, or specialized features like multi-tenancy namespaces and built-in hybrid search pipelines.

Which pgvector index type should I use: HNSW or IVFFlat?

HNSW is the better default for most workloads in 2026. It offers higher recall at the same latency, supports concurrent inserts without major degradation, and does not require a training step. IVFFlat is faster to build and uses less memory, but needs a training pass on existing data to cluster centroids and its recall drops as data grows beyond the training set. Use IVFFlat only if memory is severely constrained or you need the fastest possible index build time.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →