pgvector
pgvector is the official Python support package for the pgvector Postgres extension. It does not talk to the database itself; it teaches your existing driver or ORM (Django, SQLAlchemy, SQLModel, Psycopg 2 and 3, asyncpg, pg8000, Peewee) how to send and receive the extension's vector, halfvec, bit and sparsevec column types, and adds query helpers like L2Distance or embedding.cosine_distance() so nearest-neighbor queries read like normal ORM code instead of hand-built SQL strings.
If your app already runs on Postgres, this is the least-drama way to add similarity search: a thin, well-tested adapter covering every Python driver that matters. Treat it as glue; the decisions that make or break you (extension version, index type, memory settings) happen on the Postgres side.
Use it if
- Your embeddings live next to relational data in Postgres and you want similarity search with joins, filters and transactions instead of running a separate vector database
- You use Django, SQLAlchemy or SQLModel and want vector columns, distance ordering and HNSW/IVFFlat index definitions expressed in the ORM rather than raw migration SQL
- Your corpus is small to mid-size (up to a few million vectors), where a properly indexed Postgres beats the operational cost of a dedicated vector engine
- You need the newer pgvector types: halfvec for half precision, sparsevec for sparse embeddings, bit for binary quantization, all wrapped for every supported driver
- The extension is the hard part and this package does not solve it: nothing works until pgvector is installed on the Postgres server, and on managed databases you are stuck with whatever version your provider ships
- You expect a vector database API: there is no collection abstraction, no built-in embedding, no index management beyond what you write in SQL; it is type adapters plus ORM helpers and everything else is on you
- You are at billion-scale or need heavily filtered ANN with strict latency: dedicated engines like Milvus or Qdrant offer better recall/latency trade-offs and horizontal scaling than one Postgres instance
- You query through a single raw driver and are fine with casting: passing vectors as strings and casting with ::vector works without any dependency, so a thin project may not need this at all
Setup reality
pip install pgvector is the easy half. The extension must exist server side first (a postgresql-XX-pgvector OS package, the pgvector/pgvector Docker image, or your cloud provider's extension list) and CREATE EXTENSION vector needs adequate privileges. After that, every driver has its own registration dance: register_vector(conn) per connection for Psycopg, a configure hook for pools, an async variant for asyncpg, and a SQLAlchemy connect event if you use vector arrays. Skipping registration gives you strings back instead of vectors. Index tuning also lives in Postgres, not Python: IVFFlat lists should be set after data is loaded and HNSW builds go much faster with a bigger maintenance_work_mem.
Patterns
Enable the pgvector extensionenable-extension
# Psycopg 3
conn.execute('CREATE EXTENSION IF NOT EXISTS vector')
# SQLAlchemy
from sqlalchemy import text
session.execute(text('CREATE EXTENSION IF NOT EXISTS vector'))This only works if the extension binaries are installed on the server and your role has permission; on managed Postgres, enable it from the provider's extension settings first.
Register vector types with Psycopg 3register-types-psycopg
from pgvector.psycopg import register_vector
register_vector(conn)
# for connection pools
from psycopg_pool import ConnectionPool
def configure(conn):
register_vector(conn)
pool = ConnectionPool(conninfo, configure=configure)Registration is per connection. Without it, vector columns come back as plain strings and inserts of Vector objects fail to adapt.
Insert vectors and query nearest neighbors with raw SQLinsert-and-query-nearest
from pgvector import Vector
conn.execute('CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3))')
embedding = Vector([1, 2, 3])
conn.execute('INSERT INTO items (embedding) VALUES (%s)', (embedding,))
rows = conn.execute(
'SELECT * FROM items ORDER BY embedding <-> %s LIMIT 5', (embedding,)
).fetchall()<-> is L2 distance, <=> is cosine distance, <#> is negative inner product. Pick the operator that matches the opclass of your index or the index will not be used.
Define a vector column in SQLAlchemysqlalchemy-vector-column
from pgvector.sqlalchemy import VECTOR
from sqlalchemy.orm import Mapped, mapped_column
class Item(Base):
__tablename__ = 'items'
id: Mapped[int] = mapped_column(primary_key=True)
embedding: Mapped[list[float]] = mapped_column(VECTOR(3))
item = Item(embedding=[1, 2, 3])
session.add(item)
session.commit()The dimension argument is enforced by Postgres on insert; also available: HALFVEC, BIT and SPARSEVEC for the other pgvector types.
Nearest neighbors and distance filters in SQLAlchemysqlalchemy-nearest-neighbors
from sqlalchemy import select
# top 5 by L2 distance
session.scalars(
select(Item).order_by(Item.embedding.l2_distance([3, 1, 2])).limit(5)
)
# everything within a distance threshold
session.scalars(
select(Item).filter(Item.embedding.l2_distance([3, 1, 2]) < 5)
)cosine_distance, max_inner_product, l1_distance, hamming_distance and jaccard_distance are also available on the column.
Create an HNSW or IVFFlat index from SQLAlchemysqlalchemy-hnsw-index
from sqlalchemy import Index
index = Index(
'items_embedding_idx',
Item.embedding,
postgresql_using='hnsw',
postgresql_with={'m': 16, 'ef_construction': 64},
postgresql_ops={'embedding': 'vector_l2_ops'}
)
index.create(engine)Use vector_cosine_ops for cosine and vector_ip_ops for inner product; the opclass must match the distance operator you query with. IVFFlat (postgresql_using='ivfflat', lists=N) should be built after the table has data.
Vector field and nearest-neighbor query in Djangodjango-model-and-query
from pgvector.django import VectorExtension, VectorField, L2Distance
class Migration(migrations.Migration):
operations = [VectorExtension()]
class Item(models.Model):
embedding = VectorField(dimensions=3)
Item.objects.order_by(L2Distance('embedding', [3, 1, 2]))[:5]
Item.objects.alias(d=L2Distance('embedding', [3, 1, 2])).filter(d__lt=5)HnswIndex and IvfflatIndex go in the model Meta.indexes with an opclasses list; CosineDistance and MaxInnerProduct mirror the other operators.
Register types on an asyncpg poolasyncpg-pool-setup
import asyncpg
from pgvector.asyncpg import register_vector
async def init(conn):
await register_vector(conn)
pool = await asyncpg.create_pool(dsn, init=init)
async with pool.acquire() as conn:
rows = await conn.fetch(
'SELECT * FROM items ORDER BY embedding <-> $1 LIMIT 5', embedding
)asyncpg uses $1 placeholders, not %s, and register_vector is awaited; forgetting the init hook is the usual cause of 'unknown type vector' errors.
Bulk load embeddings with COPYbulk-load-copy
with conn.cursor() as cur:
with cur.copy('COPY items (embedding) FROM STDIN WITH (FORMAT BINARY)') as copy:
copy.set_types(['vector'])
for embedding in embeddings:
copy.write_row([Vector(embedding)])
conn.commit()Binary COPY through Psycopg 3 is dramatically faster than row-by-row inserts for large loads; build the ANN index after loading, not before.
Work with sparse and half-precision vectorssparse-and-half-vectors
from pgvector import HalfVector, SparseVector
hv = HalfVector([1, 2, 3]) # halfvec column, 2 bytes per dim
sv = SparseVector({0: 1.0, 2: 2.0, 4: 3.0}, 6) # non-zero index: value, dims
sv.indices() # [0, 2, 4]
sv.values() # [1.0, 2.0, 3.0]
sv.to_numpy() # dense numpy arraySparseVector also accepts SciPy sparse arrays and its indices start at 0 in Python but are stored 1-based in Postgres text format; the classes convert for you.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| chromadb | PyPI | You want a batteries-included local vector store with collections and built-in embedding functions instead of managing Postgres |
| qdrant-client | PyPI | A dedicated vector database with strong filtered search and quantization once you outgrow a single Postgres instance |
| pymilvus | PyPI | Billion-scale ANN workloads that need horizontal scaling and GPU index options |