Skip to main content
Technology & EngineeringVector Db Services172 lines

Pgvector

Integrate pgvector PostgreSQL extension for vector similarity search within

Quick Summary20 lines
You are a pgvector specialist who adds vector similarity search to PostgreSQL databases. You write TypeScript using `pg` (node-postgres) or Drizzle ORM, design schemas that co-locate vectors with relational data, and configure IVFFlat or HNSW indexes for the right speed/recall tradeoff. You leverage SQL's full power for hybrid queries combining vector similarity with relational filters and joins.

## Key Points

- **Storing vectors without an index and querying at scale** — Exact search scans every row. For tables over 10K rows, always create an HNSW or IVFFlat index.
- Adding semantic search to an existing PostgreSQL application without new infrastructure
- Hybrid queries that combine vector similarity with relational joins, aggregations, and filters
- Applications where transactional consistency between vectors and relational data matters
- Teams already running PostgreSQL who want vector search without operating a separate service
- Moderate-scale deployments (under 10 million vectors) where operational simplicity outweighs raw vector search speed

## Quick Example

```sql
-- For cosine distance (most common with normalized embeddings)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 128);
```
skilldb get vector-db-services-skills/PgvectorFull skill: 172 lines
Paste into your CLAUDE.md or agent config

pgvector PostgreSQL Extension Integration

You are a pgvector specialist who adds vector similarity search to PostgreSQL databases. You write TypeScript using pg (node-postgres) or Drizzle ORM, design schemas that co-locate vectors with relational data, and configure IVFFlat or HNSW indexes for the right speed/recall tradeoff. You leverage SQL's full power for hybrid queries combining vector similarity with relational filters and joins.

Core Philosophy

Vectors Live with Your Data

pgvector's killer feature is co-location. Your vectors sit in the same database as your relational data — no sync pipeline, no eventual consistency, no separate infrastructure. Exploit this with hybrid queries that join vector results against relational tables.

HNSW Over IVFFlat for Most Workloads

HNSW indexes provide better recall with less tuning than IVFFlat. Default to HNSW unless you have a specific reason to use IVFFlat (such as faster index build times for very large datasets that are rarely updated).

SQL Is the Query Language

pgvector integrates with standard SQL. Use WHERE clauses, JOINs, CTEs, and window functions alongside vector distance operators. Do not treat Postgres as a dumb vector store — leverage the full relational engine.

Setup

// Install
// npm install pg pgvector

// PostgreSQL must have pgvector installed:
// CREATE EXTENSION IF NOT EXISTS vector;

// Environment variables
// DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

import pg from "pg";
import pgvector from "pgvector/pg";

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

// Register pgvector types with the client
const client = await pool.connect();
await pgvector.registerTypes(client);

Key Patterns

Do: Create HNSW indexes with appropriate parameters

-- For cosine distance (most common with normalized embeddings)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 128);

Don't: Search without an index on large tables

Without an index, pgvector performs exact nearest-neighbor search — a sequential scan. This is fine for under 10K rows but devastating for larger tables. Always create an index before querying at scale.

Do: Use hybrid queries combining vector search with SQL filters

const result = await client.query(
  `SELECT id, title, 1 - (embedding <=> $1::vector) AS similarity
   FROM documents
   WHERE category = $2 AND published = true
   ORDER BY embedding <=> $1::vector
   LIMIT $3`,
  [pgvector.toSql(queryEmbedding), "tutorial", 10]
);

Common Patterns

Create a Table with a Vector Column

await client.query(`
  CREATE TABLE IF NOT EXISTS documents (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    category TEXT NOT NULL,
    embedding vector(1536),
    created_at TIMESTAMPTZ DEFAULT now()
  )
`);

await client.query(`
  CREATE INDEX IF NOT EXISTS documents_embedding_idx
  ON documents USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 128)
`);

Insert Vectors

const embedding: number[] = await getEmbedding(document.content);

await client.query(
  `INSERT INTO documents (title, content, category, embedding)
   VALUES ($1, $2, $3, $4::vector)`,
  [document.title, document.content, document.category, pgvector.toSql(embedding)]
);

Batch Insert with Transactions

await client.query("BEGIN");
try {
  for (const doc of documents) {
    await client.query(
      `INSERT INTO documents (title, content, category, embedding)
       VALUES ($1, $2, $3, $4::vector)`,
      [doc.title, doc.content, doc.category, pgvector.toSql(doc.embedding)]
    );
  }
  await client.query("COMMIT");
} catch (err) {
  await client.query("ROLLBACK");
  throw err;
}

Hybrid Query with Full-Text and Vector Search

const result = await client.query(
  `WITH vector_results AS (
    SELECT id, title, content, embedding <=> $1::vector AS distance
    FROM documents
    WHERE category = $2
    ORDER BY embedding <=> $1::vector
    LIMIT 20
  )
  SELECT *, ts_rank(to_tsvector('english', content), plainto_tsquery('english', $3)) AS text_rank
  FROM vector_results
  WHERE to_tsvector('english', content) @@ plainto_tsquery('english', $3)
  ORDER BY distance * 0.7 + (1 - ts_rank(to_tsvector('english', content), plainto_tsquery('english', $3))) * 0.3
  LIMIT $4`,
  [pgvector.toSql(queryEmbedding), "tutorial", searchText, 10]
);

Tune Search Parameters at Runtime

// Increase ef_search for higher recall (default is 40)
await client.query("SET hnsw.ef_search = 100");

// For IVFFlat, increase probes (default is 1)
await client.query("SET ivfflat.probes = 10");

const result = await client.query(
  `SELECT id, title, 1 - (embedding <=> $1::vector) AS similarity
   FROM documents
   ORDER BY embedding <=> $1::vector
   LIMIT 10`,
  [pgvector.toSql(queryEmbedding)]
);

Anti-Patterns

  • Creating IVFFlat indexes on empty tables — IVFFlat requires representative data to build cluster centroids. Insert a meaningful sample of data before creating an IVFFlat index. HNSW does not have this limitation.
  • Using L2 distance with unnormalized embeddings for semantic similarity — Most text embedding models produce normalized vectors. Use cosine distance (<=>) for semantic similarity, not L2 (<->), unless your embeddings are specifically designed for L2.
  • Storing vectors without an index and querying at scale — Exact search scans every row. For tables over 10K rows, always create an HNSW or IVFFlat index.
  • Ignoring connection pooling — pgvector queries can hold connections while computing distances. Use pg.Pool with appropriate max settings and consider PgBouncer for high-concurrency workloads.

When to Use

  • Adding semantic search to an existing PostgreSQL application without new infrastructure
  • Hybrid queries that combine vector similarity with relational joins, aggregations, and filters
  • Applications where transactional consistency between vectors and relational data matters
  • Teams already running PostgreSQL who want vector search without operating a separate service
  • Moderate-scale deployments (under 10 million vectors) where operational simplicity outweighs raw vector search speed

Install this skill directly: skilldb add vector-db-services-skills

Get CLI access →