Cerca “cucciolo” in un sistema che confronta solo le parole chiave e un documento sull’addestramento dei cani non comparirà, anche se chiunque legga entrambi li definirebbe strettamente collegati. La ricerca per parole chiave confronta stringhe di testo. Non sa che “cucciolo” e “cane” significano quasi la stessa cosa. Gli embedding risolvono proprio questo. Trasformano un testo in una lista di numeri, un vettore, posizionato in modo che testi con significato simile finiscano vicini ad altri testi con significato simile, indipendentemente dalle parole usate.
Come un modello di embedding trasforma il testo in numeri
Un modello di embedding è una rete neurale addestrata a mappare il testo in un punto di uno spazio ad alta dimensionalità: 384, 768 o 1536 dimensioni sono valori tipici, a seconda del modello. Non consulta un dizionario. Da enormi quantità di testo ha imparato quali parole e frasi compaiono in contesti simili, e lo codifica nella posizione verso cui punta il vettore.
La distanza in quello spazio segue la similarità di significato. “Re” e “regina” finiscono vicini. “Re” e “golden retriever” finiscono lontani. Nessuno ha detto al modello di confrontare quelle parole; la geometria emerge dall’addestramento, e il confronto è una conseguenza di dove finisce ciascuna parola.
Puoi vedere la stessa forma con numeri scelti a mano, prima che entri in gioco un modello vero. Assegna a ogni parola un vettore di 3 numeri (un modello reale ne usa centinaia):
| |
king e queen concordano strettamente sui primi due numeri. dog no. Per trasformare “concordano strettamente” in un punteggio unico, serve una metrica di similarità.
Cosine similarity, dot product ed Euclidean distance a confronto
Tre metriche ricorrono nel lavoro con gli embedding, e ciascuna risponde a una domanda leggermente diversa:
| Metrica | Cosa misura | Quando usarla |
|---|---|---|
| Cosine similarity | L’angolo tra due vettori, ignorando la loro lunghezza | Default per gli embedding di testo, perché la maggior parte dei modelli è addestrata per questo |
| Dot product | Angolo e lunghezza insieme | Il modello è stato addestrato esplicitamente per il ranking con dot product (alcuni modelli di retrieval lo sono) |
| Euclidean distance | Distanza in linea retta tra i due punti | Dati spaziali o basati su conteggi; meno comune per il testo |
La cosine similarity è quella a cui ricorrerai quasi sempre. Molti modelli di text embedding normalizzano il proprio output a lunghezza 1, quindi la direzione è l’unica cosa che varia, e su vettori normalizzati cosine e dot product producono lo stesso ranking. Il punteggio va da -1 (opposto) a 1 (identico), con 0 che significa non correlato. Ecco il calcolo sui vettori scelti a mano:
| |
0.801 contro 0.375. king è molto più vicino a queen che a dog. Un modello reale produce lo stesso tipo di scarto, distribuito su centinaia di dimensioni che ha imparato dai dati invece che tre scelte a mano.
Come generare embedding in Python
sentence-transformers è la libreria open-source standard per questo compito. Funziona in locale senza bisogno di un’API key:
| |
| |
all-MiniLM-L6-v2 trasforma ogni frase in un vettore di 384 numeri, e model.similarity calcola la cosine similarity per ogni coppia. Le due frasi sul tempo soleggiato non condividono quasi nessuna parola, eppure ottengono un punteggio alto tra loro e basso rispetto a quella sullo stadio. Stesso schema dell’esempio giocattolo, solo che ora i numeri arrivano da un modello che ha imparato il linguaggio.
Le API hosted restituiscono la stessa cosa. Gli endpoint text-embedding-3-small di OpenAI e embed di Cohere danno entrambi una lista di numeri decimali per ogni testo. Se usi già modelli in locale, anche Ollama serve embedding: ollama pull nomic-embed-text, poi chiama l’endpoint /api/embed (la configurazione completa è in Come eseguire un LLM in locale con Ollama). La libreria cambia. Il vettore, e cosa ne fai, no.
Come la ricerca per similarità vettoriale trova la corrispondenza più vicina
Calcolare l’embedding di un singolo testo è la parte facile. Il lavoro vero è quasi sempre: dato un vettore di query, trovare le corrispondenze più vicine tra migliaia o milioni di vettori memorizzati. Questa è la ricerca per similarità vettoriale, e la versione diretta è la forza bruta: confronta la query con ogni vettore memorizzato e tieni i risultati migliori.
| |
Su una macchina ordinaria, questo stampa qualcosa vicino a:
| |
Il tempo cresce linearmente con il numero di vettori, O(n) nei termini della notazione Big O, ma NumPy rende ogni passaggio talmente economico che un milione di vettori richiede ben meno di 100 ms. Questo smette di bastare con traffico concorrente pesante, decine di milioni di vettori, o entrambi. A quel punto serve un indice approximate nearest neighbor (ANN), che fa per i vettori quello che un B-tree fa per una clausola WHERE in un database (vedi Come Funzionano gli Indici Database e Quando Usarli). Due tipi ricorrono ovunque:
- HNSW (Hierarchical Navigable Small World) costruisce un grafo multilivello che collega ogni vettore ai suoi vicini, così una ricerca percorre il grafo verso la risposta invece di toccare ogni vettore. Offre il miglior recall per millisecondo tra le opzioni comuni, e lo paga con più memoria e build dell’indice più lente.
- IVFFlat raggruppa i vettori in cluster attorno a centroidi, poi cerca solo nei cluster più vicini alla query. Si costruisce più in fretta e usa meno memoria di HNSW, ma richiede dati rappresentativi già caricati prima di crearlo. Costruito su una tabella vuota, produce cluster scadenti e recall scadente.
In Postgres con l’estensione pgvector, ciascuno è un’unica istruzione:
| |
Entrambi scambiano esattezza con velocità. Possono non trovare il vettore davvero più vicino, il che va bene per ricerca e raccomandazioni ed è un problema per qualsiasi caso che richiede la risposta esatta ogni volta.
Errori comuni nella ricerca con gli embedding
- Interrogare con un modello e salvare con un altro. I vettori di due modelli diversi vivono in spazi incompatibili. Se cambi modello, ricalcola l’embedding di ogni documento memorizzato con quello nuovo. Un mix di vettori vecchi e nuovi restituisce corrispondenze inutili senza generare alcun errore.
- Ranking con dot product su vettori mai normalizzati. Il dot product include la lunghezza, quindi un vettore lungo può superare in ranking uno più corto e più rilevante. Normalizza al momento della scrittura, oppure usa l’operatore cosine, che ignora la lunghezza.
- Trattare la vicinanza come correttezza. Un punteggio cosine alto significa “compare in contesti simili”, non “è la risposta giusta”. Una query sugli effetti collaterali di un farmaco può restituire un chunk sul dosaggio dello stesso farmaco: correlato, e sbagliato. Questo è il principale punto debole nelle pipeline RAG. La similarità restringe i candidati. Non li verifica.
- Aggiungere un indice ANN prima che la forza bruta sia davvero troppo lenta. Il benchmark sopra gestisce un milione di vettori in meno di 100 ms. HNSW o IVFFlat aggiungono tempo di build, parametri da calibrare e risultati approssimati. Conviene solo oltre le decine di milioni di vettori o sotto carico concorrente pesante, non di default.
- Dimenticare di ricalcolare l’embedding quando la fonte cambia. Un embedding è una fotografia del testo al momento in cui l’hai calcolato. Modifica il documento senza ricalcolarlo e la ricerca continuerà a restituire la vecchia versione con piena sicurezza.
Serve davvero un vector database?
Non per iniziare. Calcola l’embedding di qualche centinaio dei tuoi documenti reali con sentence-transformers, tieni i vettori in un array NumPy e cercali con il ciclo a forza bruta visto sopra. Nessun database, nessun indice. Passa a pgvector o a un vector database gestito quando la forza bruta diventa misurabilmente troppo lenta per la tua quantità di dati e il tuo volume di query. Aggiungi un indice ANN sopra solo quando la scansione della tabella è il collo di bottiglia che hai effettivamente misurato.