RAG Multimodale: Oltre il Testo con Qdrant
I miei primi RAG erano solo testuali. Funzionavano, ma ignoravano metà dell'informazione: le immagini.
I miei primi RAG erano solo testuali. Funzionavano, ma ignoravano metà dell'informazione: le immagini.
Ho costruito sistemi di Retrieval-Augmented Generation per analizzare documenti interni. Report, presentazioni, analisi di mercato. Il testo veniva estratto, diviso in chunk, trasformato in vettori e salvato su Qdrant. Un approccio standard che funziona bene. Fino a quando non ti accorgi che un grafico vale più di mille parole, ma il tuo sistema non può vederlo.
Il limite del solo testo
Un modello di embedding come all-MiniLM-L6-v2 è eccellente per il testo. Comprende la semantica, le relazioni tra le parole. Ma per lui un'immagine è un buco nero. Se un report contiene un grafico cruciale con una didascalia generica come "Figura 1", il suo contenuto è perso. La ricerca vettoriale restituirà il testo circostante, ma l'informazione visiva resta inaccessibile.
Il problema è che le informazioni di valore non sono solo nel testo. Sono nei diagrammi di flusso, nelle mappe di calore, negli schemi di architettura. Ignorarli significa lavorare con dati incompleti. Ho dovuto trovare un modo per far "vedere" i dati al mio sistema di retrieval.
Un singolo spazio vettoriale
La soluzione è usare modelli di embedding multimodali. Modelli come CLIP di OpenAI sono addestrati per mappare sia immagini che testo nello stesso spazio vettoriale latente. Questo significa che la rappresentazione vettoriale della frase "un gatto seduto su un tappeto" sarà molto vicina alla rappresentazione vettoriale di una foto che mostra esattamente quella scena.
Questo cambia completamente il gioco. Non sto più cercando corrispondenze testuali. Sto cercando concetti. Posso usare una query testuale per trovare un'immagine. Posso chiedere "mostrami il grafico a torta sulla distribuzione del budget" e il sistema può restituire l'immagine di quel grafico, anche se nessuna parola esatta corrisponde nella didascalia.
Qdrant per il multimodale
L'implementazione pratica è sorprendentemente diretta. A Qdrant non interessa da dove provenga il vettore. Gli importa solo la sua dimensionalità e la metrica di distanza. La logica si sposta dal database al processo di embedding.
Il mio flusso di lavoro è diventato questo:
1. Estraggo testo e immagini da un documento (es. un PDF).
2. Uso un modello come clip-ViT-B-32 per generare gli embedding.
3. Per il testo, passo le stringhe al modello. Per le immagini, passo gli oggetti immagine.
4. Salvo ogni vettore in una singola collection Qdrant, usando i metadati per distinguere la fonte.
Questo è un esempio di come inserire i dati in Qdrant.
from sentence_transformers import SentenceTransformer
from qdrant_client import QdrantClient, models
from PIL import Image
# Carico un modello multimodale
model = SentenceTransformer('clip-ViT-B-32')
client = QdrantClient(host="localhost", port=6333)
collection_name = "documenti_multimodali"
client.recreate_collection(
collection_name=collection_name,
vectors_config=models.VectorParams(size=model.get_sentence_embedding_dimension(), distance=models.Distance.COSINE),
)
# Genero l'embedding per un'immagine
img_path = "grafico_vendite.png"
img_embedding = model.encode(Image.open(img_path)).tolist()
# Genero l'embedding per un testo
text_chunk = "Report Q3. Le vendite sono aumentate del 20% rispetto al trimestre precedente."
text_embedding = model.encode(text_chunk).tolist()
# Salvo entrambi nella stessa collection
client.upsert(
collection_name=collection_name,
points=[
models.PointStruct(id=1, vector=img_embedding, payload={"source_file": img_path, "type": "image"}),
models.PointStruct(id=2, vector=text_embedding, payload={"source_file": "report.pdf", "type": "text"}),
],
wait=True,
)
Nel payload conservo il tipo di contenuto (image o text) e il riferimento al file originale. Questo è fondamentale per ricostruire il contesto quando si recuperano i risultati.
La query che vede
La vera differenza si nota durante l'interrogazione. Posso formulare una domanda in linguaggio naturale e ottenere come risultato l'immagine più pertinente. Non una descrizione dell'immagine, ma l'immagine stessa.
# Query testuale per trovare un'immagine
query_text = "grafico che mostra la crescita delle vendite"
query_vector = model.encode(query_text).tolist()
hits = client.search(
collection_name=collection_name,
query_vector=query_vector,
limit=3,
query_filter=models.Filter(
must=[models.FieldCondition(key="type", match=models.MatchValue(value="image"))]
)
)
# Il risultato più probabile è il punto con id=1
# che corrisponde a 'grafico_vendite.png'
for hit in hits:
print(f"Trovato: {hit.payload['source_file']}, Score: {hit.score}")
Ho aggiunto un filtro per cercare solo tra i contenuti di tipo image. La query è testo, ma la ricerca avviene nello spazio semantico condiviso. Il sistema non sta cercando le parole "crescita" o "vendite" nella didascalia. Sta cercando un'immagine che rappresenta concettualmente quel significato.
Recuperare informazioni non è più una questione di parole.