pgvector with Django, part 2: your first vector in 15 minutes
From theory to code: PostgreSQL with pgvector in Docker, a Django project and your first vector field. In a few minutes you will have a real map inside a database.
What we build today
In the first part we imagined a map of meaning, where similar things sit close together. Today we build it for real: six points (two animals, two vehicles, two dishes) stored in a database, and a search that finds "the nearest ones".
We will use only three numbers per point instead of a thousand, so you can read them at a glance. The mechanism, though, is identical to the one in real systems.
You need three things installed: Docker, uv (the modern Python project manager) and Python 3.14. It is the same stack I use in my book.
Step 1: PostgreSQL with pgvector in a container
A freshly installed PostgreSQL does not know pgvector: it is an extension that has to be added. The quickest way is to use the project's official image, pgvector/pgvector, which is PostgreSQL with the extension already inside.
Create a folder and, inside it, a file called compose.yaml:
services:
db:
image: pgvector/pgvector:pg17
environment:
POSTGRES_DB: archivio
POSTGRES_USER: archivio
POSTGRES_PASSWORD: cambiami
ports:
- "127.0.0.1:5433:5432"
volumes:
- dati_db:/var/lib/postgresql/data
volumes:
dati_db:
Three details that make a difference:
pg17means PostgreSQL 17. Other versions (pg16,pg18) exist with the same pattern.127.0.0.1:5433:5432exposes the database only on your own computer, and on port 5433 instead of the usual 5432, so it does not clash with a PostgreSQL you may already have installed.- The
dati_dbvolume keeps your data even if the container is stopped.
Start it and check that it is running:
docker compose up -d
docker compose ps
Step 2: the Django project with uv
uv init --python 3.14 --bare .
uv add "django>=6.1,<6.2" pgvector "psycopg[binary]"
uv run django-admin startproject config .
uv run python manage.py startapp archivio
Here pgvector is the Python library that teaches Django what vectors are, and psycopg is the driver that talks to PostgreSQL.
Step 3: connect Django to the database
Open config/settings.py. In INSTALLED_APPS add two lines at the bottom:
INSTALLED_APPS = [
# ... the apps that are already there ...
"django.contrib.postgres",
"archivio",
]
django.contrib.postgres is not needed yet for this part, but it will be from part 4, when we create indexes: without it, Django refuses to create them. Better to add it now.
Then replace the DATABASES block with this:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "archivio",
"USER": "archivio",
"PASSWORD": "cambiami",
"HOST": "127.0.0.1",
"PORT": "5433",
}
}
These are the same values as in compose.yaml. In a real project the password does not belong in the code but in an environment variable: for today, this is fine.
Step 4: switch the extension on from a migration
The extension has to be "switched on" once in the database. The cleanest way is to do it from a migration, so Django does it for you on any new database. Create an empty migration:
uv run python manage.py makemigrations archivio --empty -n estensione_vector
Open the file it created in archivio/migrations/ and make it look like this:
from django.db import migrations
from pgvector.django import VectorExtension
class Migration(migrations.Migration):
dependencies = []
operations = [
VectorExtension(),
]
VectorExtension() runs the command that turns pgvector on, for you.
Step 5: the first model with a vector
In archivio/models.py:
from django.db import models
from pgvector.django import VectorField
class Punto(models.Model):
nome = models.CharField(max_length=50)
posizione = VectorField(dimensions=3)
def __str__(self):
return self.nome
The VectorField(dimensions=3) field creates a column of type vector(3) in the database: a list of exactly three numbers. Then:
uv run python manage.py makemigrations archivio -n punto
uv run python manage.py migrate
Step 6: fill the map and search
Open the Django shell:
uv run python manage.py shell
And paste this (the coordinates are made up: animals in one corner, vehicles in another, food in the third):
from archivio.models import Punto
from pgvector.django import CosineDistance
Punto.objects.bulk_create([
Punto(nome="gatto", posizione=[0.9, 0.1, 0.0]),
Punto(nome="cane", posizione=[0.8, 0.2, 0.1]),
Punto(nome="auto", posizione=[0.1, 0.9, 0.1]),
Punto(nome="moto", posizione=[0.0, 0.8, 0.3]),
Punto(nome="pizza", posizione=[0.1, 0.1, 0.9]),
Punto(nome="pasta", posizione=[0.2, 0.0, 0.8]),
])
cerca = [0.85, 0.15, 0.05] # a "puppy-like" point
vicini = Punto.objects.annotate(d=CosineDistance("posizione", cerca)).order_by("d")[:2]
for p in vicini:
print(p.nome, round(p.d, 3))
(The names are Italian: *gatto* is cat, *cane* is dog, *auto* is car, *moto* is motorbike.) You should see:
gatto 0.004
cane 0.004
The point we searched for sits in the animals' corner, and the database hands you exactly the two animals. The number next to each is the distance: the smaller it is, the more alike the two things are; close to zero means almost identical.
What really happened
The Django code you wrote turns into an SQL query with a new operator, <=>, which means "cosine distance". In practice PostgreSQL computed the distance between your point and every point in the table, sorted them and gave you the first two. No words compared: only numbers.
If something goes wrong
- Extension "vector" is not available. You are using a plain PostgreSQL, not the
pgvector/pgvectorimage. - Port already in use. Change
5433to another number, both incompose.yamland in Django's settings. - Permission denied creating the extension. The database user lacks the rights to do it. It does not happen with the Docker image, but it can with a database managed by an outside service.
Next part
Made-up points are fine for understanding, but real documents have no coordinates: an artificial intelligence model has to compute them. And here there is a surprise: the choice of model changes the results enormously. In part 3 I try the same example with three different models, and one of them gets it wrong.