Vai al contenuto
AI.info

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

Guida all’implementazione della ricerca vettoriale

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).

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

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:

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.

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:

  1. 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.
  2. 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:

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:

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

Esplora

Altri articoli