pgvector: Trasforma PostgreSQL in un Vector Database
Nel 2026, il mantra della community e diventato chiaro: "Just Use Postgres". Snowflake ha acquisito Crunchy Data per 250 milioni di dollari, Databricks ha investito 1 miliardo su Neon, e Supabase ha raggiunto una valutazione di 5 miliardi. PostgreSQL non e più solo un database relazionale: con pgvector, diventa un vector database completo, capace di gestire embeddings, similarity search e operazioni AI senza aggiungere un singolo servizio al tuo stack.
Se stai già usando PostgreSQL (e statisticamente lo stai facendo), non hai bisogno di Pinecone, Qdrant o Weaviate per iniziare con la ricerca vettoriale. In questo articolo costruiremo un sistema di vector search completo partendo da zero: dall'installazione di pgvector alla creazione di indici ottimizzati, passando per la generazione di embeddings e l'integrazione con LangChain.
Panoramica della Serie
| # | Articolo | Focus |
|---|---|---|
| 1 | Sei qui - pgvector | Installazione, operatori, indexing |
| 2 | Embeddings in Profondità | Modelli, chunking, dimensioni |
| 3 | RAG con PostgreSQL | Pipeline RAG end-to-end |
| 4 | Hybrid Search | Vettori + full-text search |
| 5 | Scaling pgvector | Partitioning, sharding, performance |
| 6 | pgvector in Produzione | Monitoring, backup, CI/CD |
Cosa Imparerai
- Cos'è pgvector e perchè e l'estensione PostgreSQL più importante del 2026
- Come installare pgvector su Linux, Docker e servizi managed (Supabase, Neon, AWS RDS)
- Come creare tabelle con colonne vettoriali e inserire embeddings
- I tre operatori di distanza: Cosine, L2 ed Inner Product
- Come eseguire query di similarity search efficaci
- Come generare embeddings con Python (OpenAI e Sentence Transformers)
- HNSW vs IVFFlat: quale indice scegliere e come configurarlo
- Benchmark pgvector vs database vettoriali dedicati
- Un esempio completo end-to-end con Python
- Integrazione con LangChain per sistemi RAG
1. Il Trend "Just Use Postgres" nel 2026
Per anni, il mondo AI ha spinto verso database vettoriali dedicati: Pinecone, Weaviate, Qdrant, Milvus. Ogni progetto RAG sembrava richiedere un nuovo servizio da gestire, monitorare e pagare. Ma nel 2025-2026 qualcosa e cambiato radicalmente.
La community ha capito che i vettori non sono un tipo di database, ma un tipo di dato. Cosi come PostgreSQL gestisce JSON, array, geometrie e testi con ricerca full-text, può gestire anche vettori ad alta dimensionalità. L'estensione che rende tutto questo possibile si chiama pgvector.
perchè "Just Use Postgres" nel 2026
| Fattore | Database Vettoriale Dedicato | PostgreSQL + pgvector |
|---|---|---|
| Infrastruttura | Nuovo servizio da gestire | Estensione nel DB esistente |
| Costo | Piano separato (Pinecone da $70/mese) | Incluso nel costo PostgreSQL |
| Consistenza dati | Sincronizzazione tra DB necessaria | Transazioni ACID native |
| Query | API proprietaria | SQL standard + JOIN, WHERE, GROUP BY |
| Backup | Sistema separato | pg_dump include tutto |
| Team | Competenze specifiche richieste | Chiunque conosca SQL |
Il vantaggio chiave e la semplicità operativa. Con pgvector, i tuoi embeddings vivono nella stessa tabella dei tuoi dati relazionali. Puoi fare JOIN tra vettori e metadati, usare WHERE clause per filtrare, e avere consistenza transazionale. Nessun servizio esterno da sincronizzare, nessuna API diversa da imparare.
2. Cos'è pgvector
pgvector e un'estensione open-source per PostgreSQL che aggiunge il supporto nativo per vettori ad alta dimensionalità e ricerca per similarità. Creata da Andrew Kane nel 2021, e diventata rapidamente l'estensione PostgreSQL più scaricata per use case AI/ML.
Con pgvector puoi:
- Salvare vettori (embeddings) come tipo di dato nativo nella colonna di una tabella
- Eseguire ricerche di similarità usando tre metriche di distanza diverse
- Creare indici specializzati (HNSW e IVFFlat) per ricerche veloci
- Combinare la ricerca vettoriale con tutto ciò che PostgreSQL offre: JOIN, transazioni, trigger, viste materializzate
pgvector in Numeri (2026)
| Metrica | Valore |
|---|---|
| Versione attuale | 0.8.0+ (con iterative index scans) |
| Dimensioni vettore max | 16.000 |
| Tipi indice | HNSW, IVFFlat |
| Tipi vettore | vector, halfvec, sparsevec, bit |
| Metriche distanza | Cosine, L2 (Euclidea), Inner Product |
| Supportato da | AWS RDS, Supabase, Neon, Google Cloud SQL, Azure |
| Licenza | PostgreSQL License (open-source) |
La versione 0.8.0, rilasciata nel 2025, ha introdotto una funzionalità chiave: gli iterative index scans. Questa feature risolve il problema dell'overfiltering, ovvero quando un indice vettoriale non restituisce abbastanza risultati perchè la maggior parte viene scartata dai filtri WHERE. Con gli iterative scans, pgvector continua a esplorare l'indice fino a trovare abbastanza risultati che soddisfano i filtri, migliorando drasticamente la qualità delle query filtrate.
3. Installazione di pgvector
pgvector si installa in diversi modi a seconda del tuo ambiente. Vediamo i tre approcci principali: installazione nativa su Linux, Docker e servizi managed.
3.1 Installazione su Ubuntu/Debian
# Prerequisiti: PostgreSQL 14+ già installato
sudo apt-get update
sudo apt-get install -y postgresql-server-dev-16
# Clona e compila pgvector
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install
# Abilita l'estensione nel tuo database
sudo -u postgres psql -d il_tuo_database -c "CREATE EXTENSION vector;"
Per PostgreSQL 17, sostituisci postgresql-server-dev-16 con
postgresql-server-dev-17. pgvector supporta PostgreSQL dalla versione 13
alla 17.
3.2 Installazione con Docker
Il modo più veloce per iniziare. L'immagine ufficiale pgvector/pgvector
include PostgreSQL con pgvector pre-installato.
# docker-compose.yml
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_DB: vectordb
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret_password
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
pgdata:
-- init.sql: eseguito automaticamente al primo avvio
CREATE EXTENSION IF NOT EXISTS vector;
# Avvio rapido senza docker-compose
docker run -d \
--name pgvector-db \
-e POSTGRES_DB=vectordb \
-e POSTGRES_PASSWORD=secret \
-p 5432:5432 \
pgvector/pgvector:pg17
# Connetti e abilita pgvector
docker exec -it pgvector-db psql -U postgres -d vectordb \
-c "CREATE EXTENSION vector;"
3.3 Servizi Managed (Zero Setup)
Se non vuoi gestire l'infrastruttura, diversi servizi cloud offrono PostgreSQL con pgvector già abilitato. Questa e la scelta ideale per la produzione.
Servizi Managed con pgvector
| Servizio | pgvector Incluso | Piano Gratuito | Note |
|---|---|---|---|
| Supabase | Pre-installato | Si (500 MB) | Dashboard UI, API REST automatiche |
| Neon | Pre-installato | Si (0.5 GB) | Serverless, branch database |
| AWS RDS | Da abilitare | No | pgvector 0.8.0 su Aurora PostgreSQL |
| Google Cloud SQL | Da abilitare | No | Supporto pgvector 0.8.0 |
| Azure Database | Da abilitare | No | Flexible Server con pgvector |
-- Su Supabase, pgvector e già disponibile.
-- Basta abilitare l'estensione:
CREATE EXTENSION IF NOT EXISTS vector;
-- Verifica l'installazione
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- Risultato: 0.8.0
4. Creare una Tabella con Vettori
Con pgvector installato, puoi aggiungere colonne di tipo vector(n) alle tue
tabelle, dove n e la dimensionalità del vettore. Il valore di n
dipende dal modello di embedding che usi: OpenAI text-embedding-3-small produce
vettori a 1536 dimensioni, mentre text-embedding-3-large arriva fino a 3072.
-- Abilita l'estensione (se non già fatto)
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabella documenti per un sistema RAG
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(255),
category VARCHAR(100),
embedding vector(1536),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Aggiungi un commento alla colonna per documentazione
COMMENT ON COLUMN documents.embedding IS
'Embedding generato con OpenAI text-embedding-3-small (1536 dim)';
Inserire Documenti con Embeddings
L'inserimento funziona come un normale INSERT, passando il vettore come stringa di valori separati da virgola tra parentesi quadre.
-- Inserimento singolo
INSERT INTO documents (title, content, source, embedding)
VALUES (
'Introduzione a PostgreSQL',
'PostgreSQL e un database relazionale open-source potente e affidabile...',
'docs/postgres-intro.md',
'[0.023, -0.045, 0.067, 0.089, -0.012, ...]' -- vettore a 1536 dimensioni
);
-- Inserimento multiplo (batch)
INSERT INTO documents (title, content, source, category, embedding)
VALUES
('Capitolo 1', 'Contenuto cap 1...', 'libro.pdf', 'tutorial',
'[0.11, -0.22, 0.33, ...]'),
('Capitolo 2', 'Contenuto cap 2...', 'libro.pdf', 'tutorial',
'[0.44, -0.55, 0.66, ...]'),
('FAQ', 'Domande frequenti...', 'faq.md', 'supporto',
'[-0.12, 0.34, -0.56, ...]');
Dimensione del Vettore
La dimensionalità specificata nella definizione della colonna (vector(1536))
deve corrispondere esattamente alla dimensione degli embeddings che inserisci. Un tentativo
di inserire un vettore a 768 dimensioni in una colonna vector(1536) causera
un errore. Scegli la dimensione in base al modello di embedding che intendi usare.
Dimensioni dei Modelli di Embedding Popolari
| Modello | Provider | Dimensioni | Costo / 1M token |
|---|---|---|---|
| text-embedding-3-small | OpenAI | 1536 | $0.02 |
| text-embedding-3-large | OpenAI | 3072 | $0.13 |
| all-MiniLM-L6-v2 | HuggingFace (gratuito) | 384 | Gratuito (locale) |
| all-mpnet-base-v2 | HuggingFace (gratuito) | 768 | Gratuito (locale) |
| voyage-3 | Voyage AI | 1024 | $0.06 |
5. Operatori di Distanza
pgvector offre tre operatori per calcolare la distanza tra vettori. Ogni operatore corrisponde a una metrica matematica diversa e ha casi d'uso specifici.
I Tre Operatori pgvector
| Operatore | Metrica | Formula | Ordinamento | Caso d'Uso |
|---|---|---|---|---|
<=> |
Cosine Distance | 1 - cosine_similarity | ASC (0 = identici) | Testo, embeddings normalizzati |
<-> |
L2 (Euclidea) | sqrt(sum((a-b)^2)) | ASC (0 = identici) | Immagini, dati spaziali |
<#> |
Inner Product (negato) | -1 * dot_product | ASC (più negativo = più simile) | Vettori normalizzati, max performance |
-- COSINE DISTANCE <=> (il più usato per testo)
-- Risultato: 0 = identici, 2 = opposti
SELECT title, content,
embedding <=> '[0.1, 0.2, ...]' AS cosine_distance
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]' ASC
LIMIT 5;
-- L2 (EUCLIDEA) <->
-- Risultato: 0 = identici, valori crescenti = più lontani
SELECT title, content,
embedding <-> '[0.1, 0.2, ...]' AS l2_distance
FROM documents
ORDER BY embedding <-> '[0.1, 0.2, ...]' ASC
LIMIT 5;
-- INNER PRODUCT (NEGATO) <#>
-- Risultato: valori più negativi = più simili
SELECT title, content,
(embedding <#> '[0.1, 0.2, ...]') * -1 AS similarity
FROM documents
ORDER BY embedding <#> '[0.1, 0.2, ...]' ASC
LIMIT 5;
Quale Operatore Scegliere
Per il 90% degli use case con testo e AI, usa <=> (cosine distance).
I modelli di embedding come quelli di OpenAI producono vettori normalizzati, il che rende
cosine distance e inner product equivalenti in termini di risultato. Usa <->
(L2) solo quando lavori con dati spaziali o immagini dove la magnitudine del vettore conta.
L'inner product (<#>) e leggermente più veloce del cosine perchè evita
la normalizzazione, ma solo su vettori già normalizzati.
6. Query di Similarity Search
La similarity search e il cuore di pgvector. La query base e semplice: ordina i risultati per distanza dal vettore query e prendi i primi K. Ma in produzione, le query diventano più sofisticate combinando ricerca vettoriale con filtri SQL.
6.1 Query Base
-- Trova i 5 documenti più simili a un vettore query
SELECT
id,
title,
content,
1 - (embedding <=> $1) AS similarity -- $1 = vettore query
FROM documents
ORDER BY embedding <=> $1
LIMIT 5;
6.2 Query con Filtri (il vero potere di pgvector)
Qui pgvector brilla rispetto ai vector database dedicati. Puoi combinare la ricerca vettoriale con qualsiasi clausola SQL: WHERE, JOIN, GROUP BY, subquery.
-- Filtra per categoria e soglia di similarità
SELECT
id, title, content,
1 - (embedding <=> $1) AS similarity
FROM documents
WHERE category = 'tutorial'
AND created_at > NOW() - INTERVAL '30 days'
ORDER BY embedding <=> $1
LIMIT 5;
-- JOIN con altre tabelle: trova documenti simili con info autore
SELECT
d.title,
d.content,
a.name AS author_name,
1 - (d.embedding <=> $1) AS similarity
FROM documents d
JOIN authors a ON d.author_id = a.id
WHERE a.verified = true
ORDER BY d.embedding <=> $1
LIMIT 10;
-- Esclusione: trova simili ma NON il documento stesso
SELECT
id, title,
1 - (embedding <=> (SELECT embedding FROM documents WHERE id = 42)) AS similarity
FROM documents
WHERE id != 42
ORDER BY embedding <=> (SELECT embedding FROM documents WHERE id = 42)
LIMIT 5;
6.3 Iterative Index Scans (pgvector 0.8.0)
Una delle novità più importanti della versione 0.8.0 e il supporto agli iterative index scans. Quando combini ricerca vettoriale con filtri WHERE restrittivi, l'indice potrebbe non trovare abbastanza risultati nel primo passaggio. Con gli iterative scans, pgvector continua a esplorare l'indice fino a soddisfare il LIMIT richiesto.
-- Abilita iterative scan per HNSW
SET hnsw.iterative_scan = relaxed_order; -- o strict_order
-- Imposta il limite massimo di tuple da scansionare
SET hnsw.max_scan_tuples = 20000;
-- Ora le query con filtri restrittivi funzionano correttamente
SELECT id, title,
1 - (embedding <=> $1) AS similarity
FROM documents
WHERE category = 'raro' -- categoria con pochi documenti
ORDER BY embedding <=> $1
LIMIT 10;
-- Senza iterative scan: potrebbe restituire 0-3 risultati
-- Con iterative scan: restituisce sempre fino a 10 risultati
-- Modalità disponibili:
-- off = comportamento tradizionale (default)
-- strict_order = mantiene l'ordine esatto delle distanze
-- relaxed_order = ordine approssimato (migliori performance)
Quando Usare Iterative Scans
- relaxed_order: nella maggior parte dei casi, offre il miglior bilanciamento tra performance e qualità
- strict_order: quando l'ordine esatto delle distanze e critico (es. ranking di risultati per l'utente)
- off: quando non hai filtri WHERE o quando i filtri non sono restrittivi
7. Generare Embeddings con Python
Per inserire dati in pgvector, devi prima trasformare il testo in vettori numerici usando un modello di embedding. Vediamo due approcci: l'API OpenAI (a pagamento, alta qualità) e Sentence Transformers (gratuito, locale).
7.1 Con l'API OpenAI
import openai
import psycopg2
from psycopg2.extras import execute_values
# Configura il client OpenAI
client = openai.OpenAI(api_key="sk-...")
def get_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
"""Genera un embedding per un testo usando OpenAI."""
response = client.embeddings.create(
input=text,
model=model
)
return response.data[0].embedding # lista di 1536 float
# Connetti a PostgreSQL
conn = psycopg2.connect(
host="localhost",
database="vectordb",
user="admin",
password="secret_password"
)
cur = conn.cursor()
# Genera e inserisci embeddings
documents = [
("Introduzione a Python", "Python e un linguaggio di programmazione versatile..."),
("PostgreSQL Avanzato", "Le CTE ricorsive permettono di eseguire query gerarchiche..."),
("Docker per Sviluppatori", "I container Docker isolano le applicazioni..."),
]
for title, content in documents:
embedding = get_embedding(content)
cur.execute(
"INSERT INTO documents (title, content, embedding) VALUES (%s, %s, %s)",
(title, content, str(embedding))
)
conn.commit()
cur.close()
conn.close()
7.2 Con Sentence Transformers (Gratuito e Locale)
from sentence_transformers import SentenceTransformer
import psycopg2
import numpy as np
# Carica il modello (scaricato automaticamente la prima volta)
model = SentenceTransformer('all-MiniLM-L6-v2') # 384 dimensioni
# Genera embeddings per un batch di testi
texts = [
"Python e un linguaggio di programmazione versatile",
"Le CTE ricorsive permettono query gerarchiche in SQL",
"I container Docker isolano le applicazioni",
]
embeddings = model.encode(texts) # shape: (3, 384)
# Connetti e inserisci
conn = psycopg2.connect(
host="localhost", database="vectordb",
user="admin", password="secret_password"
)
cur = conn.cursor()
# NOTA: la colonna deve essere vector(384), non vector(1536)
for text, embedding in zip(texts, embeddings):
cur.execute(
"INSERT INTO documents (title, content, embedding) VALUES (%s, %s, %s)",
(text[:100], text, str(embedding.tolist()))
)
conn.commit()
cur.close()
conn.close()
OpenAI vs Sentence Transformers
| Aspetto | OpenAI API | Sentence Transformers |
|---|---|---|
| Costo | $0.02 / 1M token | Gratuito |
| qualità | Eccellente (stato dell'arte) | Buona (sufficiente per molti casi) |
| Privacy | Dati inviati a OpenAI | Tutto locale, nessun dato esce |
| Velocita | ~100ms per richiesta (rete) | ~10ms per testo (con GPU) |
| Offline | No (richiede internet) | Si (dopo il primo download) |
| Dimensioni | 1536 o 3072 | 384 o 768 (modello-dipendente) |
Regola d'Oro degli Embeddings
Usa lo stesso modello di embedding sia per indicizzare i documenti sia
per generare il vettore della query. Embeddings generati con modelli diversi vivono in
spazi vettoriali diversi e non sono confrontabili. Se indicizzi con
text-embedding-3-small, devi cercare con lo stesso modello.
8. Indexing: HNSW vs IVFFlat
Senza un indice, pgvector esegue una scansione sequenziale (brute force) confrontando il vettore query con ogni riga della tabella. Questo e preciso al 100% ma diventa lento con dataset grandi. Gli indici ANN (Approximate Nearest Neighbor) sacrificano una minima percentuale di precisione per ottenere ricerche ordini di grandezza più veloci.
8.1 Indice HNSW
HNSW (Hierarchical Navigable Small World) costruisce un grafo multi-livello di connessioni tra vettori. E l'indice consigliato per la maggior parte dei casi: veloce in lettura, buona precisione, e supporta inserimenti incrementali senza ricostruzione.
-- Indice HNSW con parametri di default
CREATE INDEX idx_documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops);
-- Indice HNSW con parametri personalizzati
CREATE INDEX idx_documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (
m = 24, -- connessioni per nodo (default: 16, range: 2-100)
ef_construction = 128 -- candidati durante costruzione (default: 64, range: 4-1000)
);
-- Regola ef_search per le query (default: 40)
SET hnsw.ef_search = 100; -- più alto = più preciso ma più lento
Parametri HNSW: Guida alla Configurazione
| Parametro | Default | Range | Effetto | Consiglio |
|---|---|---|---|---|
m |
16 | 2 - 100 | Più connessioni = più preciso, più memoria | 16-32 per la maggior parte dei casi |
ef_construction |
64 | 4 - 1000 | Più alto = indice migliore, build più lento | 64-256 (una tantum, vale la pena alzare) |
ef_search |
40 | 1 - 1000 | Più alto = ricerca più precisa, più lenta | 40-200 in base al trade-off voluto |
8.2 Indice IVFFlat
IVFFlat (Inverted File with Flat compression) divide i vettori in cluster e cerca solo nei cluster più vicini alla query. E più veloce da costruire di HNSW ma richiede la ricostruzione quando aggiungi molti dati nuovi. Ideale per dataset che cambiano poco frequentemente.
-- IMPORTANTE: IVFFlat richiede dati nella tabella PRIMA di creare l'indice
-- (necessità di fare clustering k-means sui dati esistenti)
-- Indice IVFFlat
CREATE INDEX idx_documents_embedding_ivf
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (
lists = 100 -- numero di cluster (default: 100)
);
-- Regola il numero di cluster da esplorare durante la ricerca
SET ivfflat.probes = 10; -- default: 1, consigliato: sqrt(lists)
-- Regola pratica per "lists":
-- rows < 1M: lists = rows / 1000
-- rows >= 1M: lists = sqrt(rows)
8.3 Operatori per Tipo di Distanza
L'operatore specificato nella creazione dell'indice deve corrispondere all'operatore usato nelle query. Ecco la mappa completa.
Mappa Operatori e Classi di Indice
| Distanza | Operatore SQL | Classe Indice (ops) |
|---|---|---|
| Cosine | <=> |
vector_cosine_ops |
| L2 (Euclidea) | <-> |
vector_l2_ops |
| Inner Product | <#> |
vector_ip_ops |
8.4 HNSW vs IVFFlat: Quale Scegliere
Confronto HNSW vs IVFFlat
| Aspetto | HNSW | IVFFlat |
|---|---|---|
| Velocita ricerca | Più veloce | Veloce |
| Precisione (recall) | ~95-99% | ~90-98% |
| Tempo di build | Lento | Veloce |
| Memoria | Alta | Media |
| Inserimenti incrementali | Supportati | Richiedono REINDEX |
| Iterative scans | Si (0.8.0) | Si (0.8.0) |
| Caso d'uso ideale | Uso generale, dati in crescita | Dataset statici, build veloce |
Consiglio Pratico
Per la maggior parte dei progetti, scegli HNSW. E più versatile, supporta inserimenti incrementali senza ricostruzione, e la versione 0.8.0 di pgvector ha migliorato significativamente le sue performance. Usa IVFFlat solo se il tempo di costruzione dell'indice e critico (dataset molto grandi caricati una volta sola) o se hai vincoli di memoria stringenti.
9. Performance: pgvector vs Database Vettoriali Dedicati
La domanda più frequente e: "pgvector e abbastanza veloce per la produzione?". La risposta nel 2026 e un chiaro si, per la stragrande maggioranza dei casi d'uso. Vediamo i numeri.
9.1 Benchmark su Larga Scala
I benchmark di pgvectorscale (estensione complementare a pgvector sviluppata da Timescale) su 50 milioni di vettori mostrano risultati impressionanti:
Benchmark: 50M Vettori a 99% Recall
| Sistema | QPS (Query/sec) | Recall | Note |
|---|---|---|---|
| pgvectorscale | 471 | 99% | 11.4x più veloce di Qdrant |
| Qdrant | 41 | 99% | Ottimizzato per vector search puro |
9.2 Benchmark a Scala Media (1M Vettori)
Benchmark: 1M Vettori - Throughput
| Sistema | Query/sec | Insert/sec | Costo Managed |
|---|---|---|---|
| Pinecone | ~5.000 | ~50.000 | Da $70/mese |
| Qdrant | ~4.500 | ~45.000 | Self-hosted o cloud |
| pgvector | ~3.000 | ~30.000 | Incluso nel costo PG |
A 1M di vettori, pgvector e leggermente più lento in throughput puro. Ma considerando che il costo e praticamente zero (e già incluso nel tuo PostgreSQL), il rapporto qualità-prezzo e imbattibile. Per la maggior parte delle applicazioni web, 3.000 query al secondo sono più che sufficienti.
Quando pgvector NON Basta
Considera un database vettoriale dedicato (Qdrant, Pinecone) quando:
- Hai oltre 50 milioni di vettori
- Hai bisogno di latenze p99 sotto i 5ms
- Il tuo workload e esclusivamente vector search (nessun dato relazionale)
- Hai bisogno di sharding distribuito nativo
Per tutto il resto, pgvector e la scelta giusta. Un sistema sotto 10 milioni di vettori con PostgreSQL già nello stack non ha bisogno di complessità aggiuntiva.
10. Esempio Completo: Knowledge Base con Python
Mettiamo tutto insieme in un esempio pratico. Costruiremo una knowledge base che: indicizza documenti con embeddings, permette ricerche per similarità e supporta filtri per categoria.
"""
Knowledge Base con PostgreSQL + pgvector
Requisiti: pip install openai psycopg2-binary
"""
import openai
import psycopg2
from dataclasses import dataclass
# === Configurazione ===
OPENAI_API_KEY = "sk-..."
DB_CONFIG = {
"host": "localhost",
"database": "vectordb",
"user": "admin",
"password": "secret_password",
}
EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 1536
@dataclass(frozen=True)
class SearchResult:
id: int
title: str
content: str
similarity: float
category: str
# === Funzioni di embedding ===
client = openai.OpenAI(api_key=OPENAI_API_KEY)
def get_embedding(text: str) -> list[float]:
"""Genera un embedding per un testo."""
response = client.embeddings.create(
input=text,
model=EMBEDDING_MODEL,
)
return response.data[0].embedding
def get_embeddings_batch(texts: list[str]) -> list[list[float]]:
"""Genera embeddings per un batch di testi (più efficiente)."""
response = client.embeddings.create(
input=texts,
model=EMBEDDING_MODEL,
)
return [item.embedding for item in response.data]
# === Setup del database ===
def setup_database() -> None:
"""Crea la tabella e gli indici."""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
cur.execute(f"""
CREATE TABLE IF NOT EXISTS kb_documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(100) DEFAULT 'general',
embedding vector({EMBEDDING_DIM}),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
""")
cur.execute("""
CREATE INDEX IF NOT EXISTS idx_kb_embedding_hnsw
ON kb_documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 24, ef_construction = 128);
""")
cur.execute("""
CREATE INDEX IF NOT EXISTS idx_kb_category
ON kb_documents (category);
""")
conn.commit()
cur.close()
conn.close()
print("Database setup completato.")
# === Inserimento documenti ===
def insert_documents(
documents: list[dict],
batch_size: int = 50,
) -> None:
"""Inserisce documenti con embeddings generati automaticamente."""
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
for i in range(0, len(documents), batch_size):
batch = documents[i : i + batch_size]
texts = [doc["content"] for doc in batch]
embeddings = get_embeddings_batch(texts)
for doc, emb in zip(batch, embeddings):
cur.execute(
"""INSERT INTO kb_documents (title, content, category, embedding)
VALUES (%s, %s, %s, %s)""",
(doc["title"], doc["content"],
doc.get("category", "general"), str(emb)),
)
print(f"Inseriti {min(i + batch_size, len(documents))}/{len(documents)} documenti")
conn.commit()
cur.close()
conn.close()
# === Ricerca per similarità ===
def search(
query: str,
limit: int = 5,
category: str | None = None,
min_similarity: float = 0.3,
) -> list[SearchResult]:
"""Cerca documenti simili alla query."""
query_embedding = get_embedding(query)
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Abilita iterative scan per filtri restrittivi
cur.execute("SET hnsw.iterative_scan = relaxed_order;")
if category:
cur.execute(
"""SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM kb_documents
WHERE category = %s
ORDER BY embedding <=> %s::vector
LIMIT %s""",
(str(query_embedding), category,
str(query_embedding), limit),
)
else:
cur.execute(
"""SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM kb_documents
ORDER BY embedding <=> %s::vector
LIMIT %s""",
(str(query_embedding), str(query_embedding), limit),
)
results = [
SearchResult(
id=row[0], title=row[1], content=row[2],
category=row[3], similarity=row[4],
)
for row in cur.fetchall()
if row[4] >= min_similarity
]
cur.close()
conn.close()
return results
# === Main ===
if __name__ == "__main__":
# 1. Setup
setup_database()
# 2. Inserisci documenti di esempio
sample_docs = [
{
"title": "Introduzione a PostgreSQL",
"content": "PostgreSQL e un RDBMS open-source con supporto ACID completo.",
"category": "database",
},
{
"title": "Indici in PostgreSQL",
"content": "I B-tree sono il tipo di indice default. HNSW e usato per vettori.",
"category": "database",
},
{
"title": "Cos'è il RAG",
"content": "RAG combina retrieval e generation per risposte basate su documenti.",
"category": "ai",
},
{
"title": "Docker per Sviluppatori",
"content": "Docker isola le applicazioni in container leggeri e portabili.",
"category": "devops",
},
{
"title": "Embeddings e Vector Search",
"content": "Gli embeddings trasformano testo in vettori per la similarity search.",
"category": "ai",
},
]
insert_documents(sample_docs)
# 3. Cerca documenti
print("\n--- Ricerca: 'come funzionano i database vettoriali' ---")
results = search("come funzionano i database vettoriali", limit=3)
for r in results:
print(f" [{r.similarity:.3f}] {r.title} ({r.category})")
print("\n--- Ricerca filtrata per categoria 'ai' ---")
results = search("tutorial su PostgreSQL", limit=3, category="ai")
for r in results:
print(f" [{r.similarity:.3f}] {r.title} ({r.category})")
11. Integrazione con LangChain
Se stai costruendo un sistema RAG, LangChain offre un'integrazione diretta con pgvector
attraverso il pacchetto langchain-postgres. Questo ti permette di usare
PostgreSQL come vector store senza scrivere SQL manualmente.
pip install langchain-postgres langchain-openai psycopg
from langchain_postgres import PGVector
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
# Configura il modello di embedding
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
openai_api_key="sk-...",
)
# NOTA: langchain-postgres usa psycopg3 (non psycopg2)
CONNECTION_STRING = "postgresql+psycopg://admin:secret@localhost:5432/vectordb"
# Crea il vector store
vector_store = PGVector(
embeddings=embeddings,
collection_name="langchain_docs",
connection=CONNECTION_STRING,
use_jsonb=True, # metadati JSONB per filtri efficienti
)
# Aggiungi documenti
docs = [
Document(
page_content="pgvector trasforma PostgreSQL in un vector database",
metadata={"source": "tutorial.md", "chapter": 1},
),
Document(
page_content="HNSW e l'indice consigliato per la maggior parte dei casi",
metadata={"source": "tutorial.md", "chapter": 2},
),
Document(
page_content="LangChain semplifica la costruzione di sistemi RAG",
metadata={"source": "guida-rag.md", "chapter": 1},
),
]
vector_store.add_documents(docs)
# Ricerca per similarità
results = vector_store.similarity_search(
"come creare un indice vettoriale",
k=3,
)
for doc in results:
print(f"[{doc.metadata['source']}] {doc.page_content[:80]}...")
# Ricerca con score
results_with_scores = vector_store.similarity_search_with_score(
"cos'è pgvector",
k=3,
)
for doc, score in results_with_scores:
print(f"[Score: {score:.4f}] {doc.page_content[:80]}...")
# Usa come retriever in una catena RAG
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 5},
)
# Il retriever può essere usato direttamente nelle chain LangChain
relevant_docs = retriever.invoke("installazione pgvector docker")
Nota su psycopg3
Il pacchetto langchain-postgres richiede psycopg3 (non psycopg2).
La stringa di connessione deve usare il formato postgresql+psycopg:// invece
di postgresql+psycopg2://. Se stai migrando da langchain-community,
aggiorna la connection string.
12. Best Practices per pgvector in Produzione
12.1 Dimensione dei Vettori
Vettori più piccoli occupano meno spazio, rendono gli indici più veloci e riducono i costi.
OpenAI text-embedding-3-small (1536 dim) offre il miglior rapporto
qualità-dimensione per la maggior parte dei casi. Se puoi tollerare una leggera perdita
di qualità, all-MiniLM-L6-v2 (384 dim) e un'ottima scelta gratuita.
12.2 Batch Insert
from psycopg2.extras import execute_values
# SBAGLIATO: un INSERT alla volta (lento)
for doc in documents:
cur.execute("INSERT INTO docs (content, embedding) VALUES (%s, %s)",
(doc.content, str(doc.embedding)))
# CORRETTO: batch insert con execute_values (10-50x più veloce)
data = [
(doc.content, str(doc.embedding))
for doc in documents
]
execute_values(
cur,
"INSERT INTO docs (content, embedding) VALUES %s",
data,
page_size=500, # righe per batch SQL
)
conn.commit()
12.3 Connection Pooling
In produzione, non aprire una nuova connessione PostgreSQL per ogni richiesta.
Usa un connection pool come PgBouncer o il pooling integrato di
psycopg2.
from psycopg2 import pool
# Crea un pool di connessioni (all'avvio dell'applicazione)
connection_pool = pool.ThreadedConnectionPool(
minconn=5, # connessioni minime
maxconn=20, # connessioni massime
host="localhost",
database="vectordb",
user="admin",
password="secret_password",
)
# Usa una connessione dal pool
conn = connection_pool.getconn()
try:
cur = conn.cursor()
cur.execute("SELECT ... ORDER BY embedding <=> %s LIMIT 5", ...)
results = cur.fetchall()
cur.close()
finally:
connection_pool.putconn(conn) # restituisci al pool
12.4 Maintenance
-- VACUUM per recuperare spazio dopo DELETE/UPDATE
VACUUM ANALYZE documents;
-- Ricostruisci l'indice IVFFlat dopo inserimenti massicci
REINDEX INDEX CONCURRENTLY idx_documents_embedding_ivf;
-- NOTA: HNSW non richiede REINDEX per nuovi inserimenti
-- Monitora la dimensione dell'indice
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'documents';
-- Verifica che l'indice venga usato
EXPLAIN ANALYZE
SELECT id, 1 - (embedding <=> '[0.1, 0.2, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 5;
-- Cerca "Index Scan using idx_..._hnsw" nell'output
Checklist per la Produzione
- Crea un indice HNSW con
m = 24eef_construction = 128 - Imposta
ef_searchtra 40 e 200 in base al trade-off latenza/precisione - Abilita
hnsw.iterative_scan = relaxed_orderse usi filtri WHERE - Usa batch insert con
execute_valuesper caricamenti massicci - Configura un connection pool (PgBouncer o psycopg2.pool)
- Schedula
VACUUM ANALYZEperiodico - Monitora le query con
EXPLAIN ANALYZEper verificare l'uso dell'indice - Usa
halfvecinvece divectorper risparmiare il 50% di spazio (se la precisione float16 e sufficiente)
Conclusioni
pgvector ha trasformato PostgreSQL da semplice database relazionale a piattaforma AI-ready. Con la versione 0.8.0 e le sue iterative index scans, il gap con i database vettoriali dedicati si e ridotto drasticamente, e per la maggior parte dei progetti sotto 10 milioni di vettori, non c'è ragione di aggiungere un servizio separato al tuo stack.
In questo articolo hai imparato a installare pgvector, creare tabelle con colonne vettoriali, eseguire similarity search con tre operatori di distanza, generare embeddings con Python, configurare indici HNSW e IVFFlat, e integrare tutto con LangChain. Hai anche visto i benchmark che confermano le performance di pgvector in produzione.
Nel prossimo articolo approfondiremo gli embeddings: come funzionano internamente, come scegliere il modello giusto, le strategie di chunking per documenti lunghi, e come ottimizzare la qualità della ricerca. Nel terzo articolo costruiremo una pipeline RAG completa con PostgreSQL come unico backend.
Risorse Aggiuntive
- pgvector GitHub: github.com/pgvector/pgvector - Documentazione ufficiale e changelog
- pgvectorscale: github.com/timescale/pgvectorscale - Estensione per performance avanzate
- LangChain PGVector: python.langchain.com - Integrazione Python ufficiale
- Supabase Vector: supabase.com/docs/guides/ai - Guida pgvector su Supabase
- Neon AI: neon.tech/docs/ai - pgvector su Neon serverless







