pgvector con Django, parte 2: il tuo primo vettore in 15 minuti
Dalla teoria al codice: PostgreSQL con pgvector in Docker, un progetto Django e il primo campo vettoriale. In pochi minuti avrai una mappa vera dentro un database.
Cosa costruiamo oggi
Nella prima puntata abbiamo immaginato una mappa del significato, dove le cose simili stanno vicine. Oggi la costruiamo davvero: sei punti (due animali, due veicoli, due piatti) salvati in un database, e una ricerca che trova «i più vicini».
Useremo solo tre numeri per punto invece di mille, così puoi leggerli a occhio. Il meccanismo, però, è identico a quello dei sistemi veri.
Ti servono tre cose installate: Docker, uv (il gestore di progetti Python moderno) e Python 3.14. È lo stesso stack che uso nel mio libro.
Passo 1: PostgreSQL con pgvector in un contenitore
Un PostgreSQL appena installato non conosce pgvector: è un'estensione che va aggiunta. Il modo più rapido è usare l'immagine ufficiale del progetto, pgvector/pgvector, che è PostgreSQL con l'estensione già dentro.
Crea una cartella e dentro un file chiamato 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:
Tre dettagli che fanno la differenza:
pg17significa PostgreSQL 17. Altre versioni (pg16,pg18) esistono con lo stesso schema.127.0.0.1:5433:5432espone il database solo sul tuo computer, e sulla porta 5433 invece della solita 5432, così non si scontra con un eventuale PostgreSQL già installato.- Il volume
dati_dbconserva i dati anche se il contenitore viene fermato.
Avvialo e controlla che sia in funzione:
docker compose up -d
docker compose ps
Passo 2: il progetto Django con 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
Qui pgvector è la libreria Python che insegna a Django cosa sono i vettori, e psycopg è il driver che parla con PostgreSQL.
Passo 3: collegare Django al database
Apri config/settings.py. In INSTALLED_APPS aggiungi due righe in fondo:
INSTALLED_APPS = [
# ... le app che ci sono già ...
"django.contrib.postgres",
"archivio",
]
django.contrib.postgres non serve ancora per questa puntata, ma servirà dalla quarta, quando creeremo gli indici: senza, Django si rifiuta di crearli. Conviene metterlo subito.
Poi sostituisci il blocco DATABASES con questo:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "archivio",
"USER": "archivio",
"PASSWORD": "cambiami",
"HOST": "127.0.0.1",
"PORT": "5433",
}
}
Sono gli stessi valori del compose.yaml. In un progetto vero la password non va scritta nel codice, ma letta da una variabile d'ambiente: per oggi va bene così.
Passo 4: attivare l'estensione da una migrazione
L'estensione va «accesa» una volta nel database. Il modo più pulito è farlo da una migrazione, così lo fa Django per te su qualsiasi database nuovo. Crea una migrazione vuota:
uv run python manage.py makemigrations archivio --empty -n estensione_vector
Apri il file creato in archivio/migrations/ e fallo diventare così:
from django.db import migrations
from pgvector.django import VectorExtension
class Migration(migrations.Migration):
dependencies = []
operations = [
VectorExtension(),
]
VectorExtension() esegue per te il comando che accende pgvector.
Passo 5: il primo modello con un vettore
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
Il campo VectorField(dimensions=3) crea nel database una colonna di tipo vector(3): una lista di esattamente tre numeri. Poi:
uv run python manage.py makemigrations archivio -n punto
uv run python manage.py migrate
Passo 6: riempire la mappa e cercare
Apri la shell di Django:
uv run python manage.py shell
E incolla questo (le coordinate sono inventate: animali in un angolo, veicoli in un altro, cibo nel terzo):
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] # un punto «da cucciolo»
vicini = Punto.objects.annotate(d=CosineDistance("posizione", cerca)).order_by("d")[:2]
for p in vicini:
print(p.nome, round(p.d, 3))
Dovresti vedere:
gatto 0.004
cane 0.004
Il punto cercato sta nell'angolo degli animali, e il database ti restituisce proprio i due animali. Il numero accanto è la distanza: più è piccolo, più le due cose si somigliano; vicino a zero vuol dire quasi identiche.
Che cosa è successo davvero
Il codice Django che hai scritto si traduce in una query SQL con un operatore nuovo, <=>, che significa «distanza coseno». In pratica PostgreSQL ha calcolato la distanza tra il punto cercato e tutti i punti della tabella, li ha messi in ordine e ti ha dato i primi due. Nessuna parola confrontata: solo numeri.
Se qualcosa non va
- L'estensione «vector» non è disponibile. Stai usando un PostgreSQL normale, non l'immagine
pgvector/pgvector. - Porta già occupata. Cambia il
5433in un altro numero, sia nelcompose.yamlsia nelle impostazioni di Django. - Permesso negato nel creare l'estensione. L'utente del database non ha i permessi per farlo. Con l'immagine Docker non succede, ma può capitare con un database gestito da un servizio esterno.
Prossima puntata
I punti inventati vanno bene per capire, ma i documenti veri non hanno coordinate: bisogna farle calcolare a un modello di intelligenza artificiale. E qui c'è una sorpresa: la scelta del modello cambia moltissimo i risultati. Nella parte 3 provo lo stesso esempio con tre modelli diversi, e uno di loro sbaglia.