Guide pratiche
Guida all’implementazione della ricerca vettoriale
Una guida pratica per portare in produzione la ricerca per similarità vettoriale: dalla scelta di un modello di embedding al confronto tra HNSW, IVF e PQ, dalle scansioni iterative di pgvector 0.8.6 alla fusione della ricerca ibrida con BM2

AI.info Team ·
Di cosa tratta questa guida
Quella che segue è una guida alla realizzazione pratica, non un’introduzione teorica. Presuppone che tu sappia già che cos’è un embedding e perché funziona la similarità coseno, e passa in rassegna le decisioni da prendere per portare la ricerca vettoriale in produzione: quale modello di embedding usare, come archiviare e indicizzare i vettori, quale tipo di indice e quale database scegliere, come filtrare in base ai metadati, come aggiungere la ricerca per parole chiave e come ottimizzare il recall senza procedere per tentativi.
Scegliere un modello di embedding
Tre variabili contano più della posizione nelle classifiche dei benchmark: dimensionalità, costo e adeguatezza del modello al tuo ambito (testo generico, codice o contenuti multilingue).
- OpenAI
text-embedding-3-small— 1.536 dimensioni, con un prezzo di circa 0,02 dollari per 1 milione di token in input. Una buona scelta predefinita per la ricerca semantica generica quando costo e latenza contano più di un miglioramento marginale della precisione.
| Modello | Dimensioni | Nota sul prezzo |
|---|---|---|
text-embedding-3-small |
1.536 | ~0,02 dollari per 1 milione di token in input |
text-embedding-3-large |
3.072 | ~6,5 volte il prezzo di -small |
text-embedding-3-large (troncato) |
1.024 | Troncamento Matryoshka, meno spazio di archiviazione e minore latenza |
- OpenAI
text-embedding-3-large— 3.072 dimensioni, con un prezzo pari a circa 6,5 volte quello di-small. Entrambi i modelli offrono un parametrodimensionsche consente di troncare l’output (per esempio, richiedendo 1.024 dimensioni anziché 3.072) grazie all’apprendimento di rappresentazioni Matryoshka: si sacrifica un po’ di precisione in cambio di minori esigenze di archiviazione e ricerche più rapide, senza dover riaddestrare nulla. - Cohere
embed-v4/ modelli aperti come BGE di BAAI e Nomic Embed — vale la pena valutarli con benchmark se hai bisogno di supporto multilingue o vuoi ospitarli sulla tua infrastruttura per evitare i costi delle API per chiamata quando i volumi sono elevati.
Regola pratica: scegli la dimensionalità minima degli embedding che raggiunge il tuo obiettivo di recall su un insieme di query reali tenuto da parte per la valutazione. Ogni dimensione in più moltiplica lo spazio di archiviazione e la memoria necessaria per l’indice per ciascun vettore del corpus: perciò, applicare un modello da 3.072 dimensioni a 50 milioni di documenti comporta una spesa infrastrutturale sensibilmente diversa rispetto a un modello da 1.536.
Fissa la scelta del modello prima di scrivere anche una sola riga: gli embedding prodotti da modelli diversi (o ottenuti troncando lo stesso modello a valori diversi di dimensions) non sono confrontabili e non possono essere mescolati in un unico indice.
Generare e archiviare i vettori
La suddivisione in blocchi precede la generazione degli embedding e incide sulla qualità del recupero dei risultati quanto il modello. Per i testi in prosa, blocchi da 200–500 token con una sovrapposizione del 10–20% sono un punto di partenza ragionevole; per il codice o i documenti strutturati, suddividi in corrispondenza dei confini naturali (funzioni, sezioni) anziché usare un numero fisso di token.
Genera gli embedding in batch invece di chiamare l’API per ogni documento:
import json
import openai
import psycopg
client = openai.OpenAI()
def embed_batch(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
resp = client.embeddings.create(model=model, input=texts, dimensions=1536)
return [d.embedding for d in resp.data]
BATCH_SIZE = 96 # keep batches well under the API's request size/token ceiling
with psycopg.connect(DATABASE_URL) as conn:
with conn.cursor() as cur:
for i in range(0, len(chunks), BATCH_SIZE):
batch = chunks[i : i + BATCH_SIZE]
vectors = embed_batch([c.text for c in batch])
cur.executemany(
"INSERT INTO documents (content, metadata, embedding) VALUES (%s, %s, %s)",
[(c.text, json.dumps(c.meta), v) for c, v in zip(batch, vectors)],
)
conn.commit()
Archivia il testo originale insieme al vettore, non soltanto il vettore: ti servirà per il reranking, per mostrarlo e per rigenerare gli embedding quando passerai a nuovi modelli. Aggiungi anche le colonne model e embedding_version: prima o poi cambierai modello di embedding e, se non ne tieni traccia, le righe generate con modelli diversi altereranno i punteggi di similarità senza che te ne accorga.
Tipi di indice: HNSW, IVF e PQ
Tre algoritmi coprono quasi tutti i sistemi in produzione:
- HNSW (Hierarchical Navigable Small World) costruisce un grafo di prossimità a più livelli. La complessità delle query è approssimativamente logaritmica rispetto alle dimensioni del corpus e, per la maggior parte dei carichi di lavoro, offre il miglior compromesso tra velocità e recall senza richiedere molta messa a punto. Il costo: tempi di costruzione più lunghi e un maggiore consumo di memoria rispetto a IVF.
- IVF (Inverted File Index) raggruppa i vettori con k-means in celle chiamate
listse, al momento della query, cerca soltanto nelleprobescelle più vicine. È più rapido da costruire e richiede meno memoria, ma il recall peggiora in modo lineare quando riduci il numero di celle esaminate; inoltre, prima di poter costruire l’indice serve un campione di addestramento rappresentativo. - PQ (Product Quantization) comprime i vettori suddividendo ciascuno in sottovettori e quantizzando ogni sottovettore in modo indipendente: è una tecnica di compressione da applicare a IVF (come
IndexIVFPQin FAISS), più che un indice autonomo. Sacrifica precisione in cambio di un notevole risparmio di memoria su scala di miliardi di vettori.
Regola pratica: inizia con HNSW. Passa a IVF/PQ solo quando il corpus è abbastanza grande (decine di milioni o più di vettori) perché la memoria richiesta da HNSW diventi il collo di bottiglia, oppure quando usi direttamente FAISS e hai bisogno di indici compositi come IVF4096,PQ64.
pgvector vs. FAISS vs. Pinecone/Weaviate/Qdrant
Non sono intercambiabili: risolvono problemi diversi.
- pgvector è un’estensione di Postgres: i vettori risiedono nello stesso database dei dati relazionali, quindi join, transazioni e filtri sui metadati si gestiscono con normale SQL. È la scelta migliore se usi già Postgres e non hai bisogno di superare la scala gestibile da un’istanza Postgres ben ottimizzata (che, con HNSW e repliche di lettura, arriva molto più lontano di quanto si pensi).
- FAISS è una libreria, non un database: non offre di serie persistenza, replica, filtri o un’API di rete. La integri nel tuo servizio e gestisci direttamente la persistenza dei dati e i filtri. Usala se vuoi il massimo controllo sulla composizione dell’indice (per esempio, configurazioni IVFPQ personalizzate) o se esegui ricerche di similarità offline o in batch, anziché rispondere a query in tempo reale.
- Pinecone, Weaviate, Qdrant sono database vettoriali gestiti o installabili sulla propria infrastruttura, con filtri, ricerca ibrida, multitenancy (namespace/collection) e scalabilità orizzontale integrati. Sceglili quando la ricerca vettoriale è una funzionalità centrale del prodotto, su larga scala, e non vuoi gestire direttamente l’infrastruttura degli indici.
Per decidere rapidamente:
| Situazione | Scelta |
|---|---|
| Usi già Postgres, corpus < ~10–50 milioni di vettori | pgvector |
| Ti serve il pieno controllo dei meccanismi interni dell’indice, pipeline offline | FAISS |
| Ti servono scalabilità gestita, isolamento tra tenant e ricerca ibrida pronti all’uso | Pinecone / Weaviate / Qdrant |
| SaaS multitenant con isolamento rigoroso per cliente | Namespace di Pinecone o collection di Qdrant/Weaviate — per Qdrant, usa collection separate solo per un numero limitato di tenant che richiedono un isolamento rigoroso; il partizionamento tramite payload è efficiente per molti tenant piccoli e di dimensioni simili |
pgvector nella pratica
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
embedding_model TEXT NOT NULL,
embedding vector(1536) NOT NULL
);
CREATE INDEX documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
CREATE INDEX documents_metadata_gin
ON documents USING gin (metadata jsonb_path_ops);
m (valore predefinito 16, intervallo 2–100) controlla la connettività del grafo; ef_construction (valore predefinito 64, deve essere ≥ 2×m) controlla quanto è approfondita la ricerca durante la costruzione. Entrambi si impostano solo al momento della costruzione: modificarli significa ricostruire l’indice.
| Parametro | Valore | Nota |
|---|---|---|
HNSW m |
16 (predefinito), intervallo 2-100 | Connettività del grafo |
HNSW ef_construction |
64 (predefinito) | Deve essere ≥ 2×m |
Costante RRF k |
60 | Valore predefinito convenzionale nella fusione dei ranking |
Filtri sui metadati
Ogni applicazione reale deve combinare «semanticamente simile a X» con «e appartenente al tenant Y» oppure «e creato dopo la data Z». Ci sono due approcci:
- Applicare prima il filtro con un indice ordinario, poi eseguire la ricerca vettoriale — funziona bene quando il filtro è molto selettivo (per esempio, per isolare i tenant) e il database riesce a combinare in modo efficiente una scansione B-tree/GIN con l’indice vettoriale.
- Applicare il filtro dopo la ricerca ANN — recuperare più candidati del necessario (per esempio, i primi 50 invece dei primi 10) e filtrarli in seguito. È il comportamento predefinito di una query HNSW ingenua e, quando un filtro è selettivo, può restituire meno risultati del previsto senza che sia evidente: l’attraversamento del grafo può esaurire l’elenco dei candidati prima di trovare abbastanza corrispondenze.
pgvector 0.8.0 (ottobre 2024) ha introdotto la scansione iterativa dell’indice, che consente a una query HNSW/IVFFlat con filtri di continuare ad ampliare a blocchi l’attraversamento del grafo, invece di fermarsi troppo presto, migliorando sensibilmente il recall con clausole WHERE selettive. I parametri sono hnsw.iterative_scan e ivfflat.iterative_scan, e sono rimasti invariati nell’attuale serie 0.8.x, fino alla versione 0.8.7, rilasciata il 1 ottobre 2026. Nei database gestiti, Pinecone indicizza automaticamente i metadati al momento dell’upsert, senza richiedere un passaggio separato per definire lo schema, e raccomanda i namespace anziché i filtri sui metadati per l’isolamento dei tenant su larga scala, perché una query limitata a un namespace è meno costosa della scansione con filtro di un indice condiviso.
Ricerca ibrida: BM25 + vettori densi
Gli embedding densi non colgono sempre le corrispondenze esatte: codici SKU dei prodotti, codici di errore e nomi propri su cui il modello di embedding è stato addestrato poco. La ricerca per parole chiave (BM25 o la ricerca full-text di Postgres) trova queste corrispondenze, ma non coglie le parafrasi e i sinonimi. Si possono combinare i due metodi e fonderne i risultati con la Reciprocal Rank Fusion (RRF):
score(d) = Σ 1 / (k + rank(d))
La somma considera la posizione del documento d nella classifica di ciascun metodo di ricerca, con k = 60 come valore predefinito convenzionale. RRF usa le posizioni in classifica anziché i punteggi grezzi, evitando così il problema delle scale incompatibili dei punteggi BM25 e delle similarità coseno. Non richiede messa a punto ed è il metodo di fusione predefinito in Elasticsearch e Qdrant.
Un’implementazione manuale di RRF in SQL, che combina la ricerca per similarità di pgvector con la ricerca full-text di Postgres:
WITH vector_hits AS (
SELECT id, row_number() OVER (ORDER BY embedding <=> $1) AS rnk
FROM documents
ORDER BY embedding <=> $1
LIMIT 50
),
text_hits AS (
SELECT id, row_number() OVER (ORDER BY ts_rank_cd(content_tsv, q) DESC) AS rnk
FROM documents, plainto_tsquery('english', $2) q
WHERE content_tsv @@ q
ORDER BY ts_rank_cd(content_tsv, q) DESC, id
LIMIT 50
)
SELECT COALESCE(v.id, t.id) AS id,
COALESCE(1.0 / (60 + v.rnk), 0) + COALESCE(1.0 / (60 + t.rnk), 0) AS rrf_score
FROM vector_hits v
FULL OUTER JOIN text_hits t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT 10;
Weaviate implementa nativamente la stessa idea tramite un parametro alpha (0 = solo BM25, 1 = solo vettori, valore predefinito del server 0,75), con due algoritmi di fusione selezionabili: rankedFusion e il più recente relativeScoreFusion. Qdrant offre query ibride attraverso la sua Query API, combinando nativamente vettori densi e sparsi (in stile BM25) con RRF. Se il database supporta nativamente la ricerca ibrida, conviene usarla invece di implementarla manualmente: richiede meno codice da mantenere e di solito è più veloce.
Per qualsiasi funzione destinata agli utenti, aggiungete una fase di reranking: prendete i primi 20–50 candidati della ricerca ibrida e riordinateli con un cross-encoder. Rerank 4 di Cohere, uscito a dicembre 2025, era l’opzione come servizio disponibile all’inizio di ottobre 2026, in due versioni: rerank-v4.0-pro per la qualità e rerank-v4.0-fast per la latenza e il throughput. Entrambe hanno una finestra di contesto di 32.768 token, contro i 4.096 del precedente rerank-v3.5, e supportano più di 100 lingue. La finestra più ampia conta quando i candidati sono lunghi: contratti, manuali, trascrizioni. Un modello ospitato sulla propria infrastruttura, come bge-reranker-v2-m3, è l’alternativa quando i documenti non possono uscire dalla vostra rete. In entrambi i casi, mettete in conto circa 100–300 ms di latenza aggiuntiva per query, ma misuratela sui vostri documenti anziché fidarvi di questo intervallo. Il reranking migliora sistematicamente l’NDCG rispetto alla sola ricerca di prima fase, perché i cross-encoder valutano congiuntamente la query e il documento invece di confrontare vettori precalcolati.
Regolazione del recall
Il recall si può regolare al momento della query, indipendentemente dall’indice costruito:
- HNSW: aumentate
ef_search(il valore predefinito varia a seconda del sistema) per ottenere più recall a costo di una maggiore latenza delle query; in Postgres si può impostare per sessione o per transazione tramiteSET hnsw.ef_search = 100;. - IVFFlat: aumentate
probes— partite da circalists / 10per corpus fino a ~1 milione di righe e da un valore più vicino asqrt(lists)per quelli più grandi, regolando il parametro tramiteSET ivfflat.probes = 10;.
Misurate il recall rispetto a un insieme di risultati di riferimento, non a sensazione: eseguite una ricerca esatta esaustiva (con un indice flat) su un campione di query, confrontatene i risultati con i primi k del vostro indice ANN e calcolate direttamente recall@k. Ricontrollatelo dopo ogni ricostruzione dell’indice, modifica del modello o modifica della quantizzazione: le regressioni del recall passano inosservate finché un utente non nota risultati scadenti.
Se il collo di bottiglia è lo spazio di archiviazione anziché la latenza, il tipo halfvec di pgvector (float a 16 bit invece che a 32 bit) dimezza lo spazio occupato con un impatto trascurabile sul recall. La quantizzazione binaria tramite il tipo bit riduce lo spazio occupato di circa 32 volte, ma comporta un’effettiva perdita di recall che una fase di reranking può recuperare in parte.
Acquisizione batch e in tempo reale
Esistono due modalità di acquisizione, spesso necessarie insieme:
- Batch: processi notturni o orari che generano o rigenerano gli embedding di un intero corpus o di un sottoinsieme modificato. Usateli per la prima elaborazione dei dati preesistenti e per le rielaborazioni periodiche (nuovo modello di embedding, modifica della strategia di suddivisione in blocchi). Le API batch (per esempio, l’endpoint batch di OpenAI) costano circa la metà per token rispetto alle chiamate sincrone, in cambio di tempi di completamento misurati in ore: il compromesso giusto per tutto ciò che non deve essere disponibile agli utenti in tempo reale.
- Tempo reale: un documento viene creato o modificato e deve diventare ricercabile entro pochi secondi — ticket di assistenza, messaggi di chat, articoli appena pubblicati. Generate l’embedding e inserite o aggiornate il documento in modo sincrono (o tramite una coda veloce) durante la scrittura, e assicuratevi che il tipo di indice tolleri inserimenti piccoli e frequenti: HNSW gestisce nativamente gli inserimenti incrementali; gli indici IVF addestrati su un campione ormai obsoleto peggiorano finché non vengono riaddestrati.
Per le fonti soggette a frequenti modifiche (come un database che alimenta il vostro archivio vettoriale), una pipeline CDC (per esempio, Debezium che legge il WAL di Postgres) verso una coda che attiva la rigenerazione degli embedding è più affidabile del controllo periodico di «updated_at > last_run», soprattutto quando occorre gestire anche le eliminazioni.
Checklist per la produzione
- Registrate
embedding_modele una versione per ogni riga; non mescolate mai silenziosamente modelli diversi nello stesso indice. - Fissate un obiettivo esplicito di recall (per esempio, recall@10 ≥ 0,9 rispetto alla ricerca esatta) e verificatelo dopo ogni modifica all’indice o al modello.
- Recuperate più risultati del necessario prima di applicare filtri o reranking (richiedete un numero di risultati pari a 3–5 volte il
kfinale), anziché presumere che l’indice ANN rispetti una clausolaWHEREselettiva restituendo esattamentekrisultati. - Aggiungete la ricerca ibrida (BM25 + vettori densi) prima di concludere che vi serva un modello di embedding più grande: la maggior parte delle lamentele per uno «scarso recall» riguarda corrispondenze esatte mancate, non corrispondenze semantiche.
- Monitorate separatamente il tempo di costruzione dell’indice e la latenza p99 delle query: peggiorano in modi diversi man mano che il corpus cresce e richiedono interventi diversi (cambiare tipo di indice oppure regolare
ef_search/probes).