Finora fare RAG su documenti visivi — PDF scansionati, screenshot, slide, immagini di prodotto — richiedeva pipeline complesse con OCR, pre-processing e modelli separati per testo e immagini. Sentence Transformers v5.4 cambia questo: con una singola API puoi creare embedding multimodali su testo, immagini e video, e costruire sistemi di ricerca semantica che funzionano cross-modale. Requisito minimo: Python 3.9+, GPU con 8GB VRAM per i modelli 2B.
Cosa ottieni alla fine
Un sistema RAG che risponde a query di testo cercando tra documenti visivi — PDF come immagini, screenshot di interfacce, cataloghi prodotto fotografati. Stessa API che già conosci da Sentence Transformers, zero cambio di paradigma. Il modello principale è Qwen3-VL-Embedding-2B: 2 miliardi di parametri, input testo + immagini + video, 8GB VRAM.
Hardware: cosa serve per girare i modelli
| GPU | VRAM | Modello consigliato | Velocità embedding | Note |
|---|---|---|---|---|
| RTX 4060 / 4060 Ti | 8 GB / 16 GB | Qwen3-VL-Embedding-2B | ~12 img/s | Ideale per 2B — 16GB per batch grandi |
| RTX 4070 / 4070 Super | 12 GB | Qwen3-VL-Embedding-2B | ~18 img/s | Buon equilibrio velocità/costo |
| RTX 4070 Ti Super / 4080 | 16 GB | Qwen3-VL-Embedding-2B/8B | ~25 img/s (2B) | 8B funziona ma lento senza offload |
| RTX 4090 / RTX 5080 | 24 GB | Qwen3-VL-Embedding-8B | ~20 img/s | 8B fluido — qualità massima |
| Apple M3 Pro / M4 Pro | 18-48 GB unified | Qwen3-VL-Embedding-2B/8B | ~8 img/s (2B) | MPS backend, velocità inferiore alla GPU dedicata |
| CPU only | RAM sistema | Qwen3-VL-Embedding-2B | <1 img/s | Estremamente lento — solo per test |
Il principio è identico alla VRAM per gli LLM: il modello deve stare interamente in memoria GPU per lavorare a velocità utile. Con Qwen3-VL-Embedding-2B pesano circa 6GB in bfloat16 — lasci margine per il batch.
Modelli disponibili: tabella completa
| Modello | Parametri | VRAM | Modalità | Caso d’uso |
|---|---|---|---|---|
| Qwen3-VL-Embedding-2B | 2B | ~8 GB | Testo, Immagine, Video | Uso generale, RAG su documenti |
| Qwen3-VL-Embedding-8B | 8B | ~20 GB | Testo, Immagine, Video | Alta qualità, corpus grandi |
| nvidia/llama-nemotron-embed-vl-1b-v2 | 1.7B | ~6 GB | Testo, Immagine | Hardware limitato |
| Qwen3-VL-Reranker-2B | 2B | ~8 GB | Testo, Immagine, Video | Reranking dopo retrieval |
| nvidia/llama-nemotron-rerank-vl-1b-v2 | 1.7B | ~6 GB | Testo, Immagine | Reranking leggero |
Step 1 — Installazione
# Installa Sentence Transformers con supporto immagini
pip install -U "sentence-transformers[image]"
# Per supporto audio (opzionale)
pip install -U "sentence-transformers"
# Per video (opzionale, peso extra)
pip install -U "sentence-transformers"
# Verifica versione installata (deve essere >= 5.4)
python -c "import sentence_transformers; print(sentence_transformers.__version__)"
Step 2 — Carica il modello e crea embedding
from sentence_transformers import SentenceTransformer
# Carica Qwen3-VL-Embedding-2B
# La revision è obbligatoria — il modello è in PR su HuggingFace
model = SentenceTransformer(
"Qwen/Qwen3-VL-Embedding-2B",
revision="refs/pr/23",
model_kwargs={
"torch_dtype": "bfloat16", # Dimezza la VRAM rispetto a float32
"attn_implementation": "flash_attention_2", # Richiede GPU Ampere+
}
)
# Verifica modalità supportate
print(model.modalities)
# Output atteso: ['text', 'image', 'video', 'message']
# Embedding di testo
testo_embeddings = model.encode([
"fattura Q1 2026 con errore IVA",
"grafico vendite anno corrente",
])
# Embedding di immagini (URL, path o PIL Image)
img_embeddings = model.encode([
"./screenshot_fattura.png",
"https://example.com/grafico.jpg",
])
# Similarità cross-modale testo → immagini
similarita = model.similarity(testo_embeddings, img_embeddings)
print(similarita)
# tensor([[0.82, 0.11], # testo 0 molto simile a img 0
# [0.09, 0.79]]) # testo 1 molto simile a img 1
Step 3 — Pipeline RAG completa con retrieval e reranking
from sentence_transformers import SentenceTransformer, CrossEncoder
import os
import numpy as np
# --- FASE 1: Indicizzazione del corpus ---
embedder = SentenceTransformer(
"Qwen/Qwen3-VL-Embedding-2B",
revision="refs/pr/23",
model_kwargs={"torch_dtype": "bfloat16"}
)
# Lista dei tuoi documenti (screenshot, PDF come immagini, foto)
corpus_docs = [
"./docs/fattura_2026_01.png",
"./docs/grafico_vendite_q1.jpg",
"./docs/contratto_fornitore.png",
# ... aggiungi tutti i tuoi documenti
]
# Crea embedding del corpus (fallo una volta e salva)
corpus_embeddings = embedder.encode_document(
corpus_docs,
batch_size=8,
show_progress_bar=True
)
# Salva per non ricalcolare ogni volta
np.save("corpus_embeddings.npy", corpus_embeddings)
# --- FASE 2: Retrieval con query testuale ---
query = "mostrami le fatture con IVA errata del 2026"
query_embedding = embedder.encode_query(query)
# Calcola similarità e prendi top-10
similarita = embedder.similarity(query_embedding, corpus_embeddings)[0]
top10_indices = similarita.argsort(descending=True)[:10].tolist()
top10_docs = [corpus_docs[i] for i in top10_indices]
# --- FASE 3: Reranking dei top-10 ---
reranker = CrossEncoder(
"Qwen/Qwen3-VL-Reranker-2B",
revision="refs/pr/11",
trust_remote_code=True,
)
rankings = reranker.rank(query, top10_docs)
for rank in rankings[:3]: # Stampa i top-3 dopo reranking
doc_path = top10_docs[rank["corpus_id"]]
print(f"Score: {rank['score']:.4f} — {doc_path}")
Performance attese nella pratica
- Indicizzazione corpus (100 immagini): ~8 secondi su RTX 4070, ~15 secondi su M3 Pro
- Query retrieval (corpus 1000 doc): <100ms — la similarità coseno è velocissima
- Reranking top-10: 2-4 secondi su RTX 4070
- Pipeline completa query → risposta: 3-5 secondi su RTX 4070 Super 12GB
- CPU only (2B, corpus 100 doc): ~120 secondi indicizzazione — non consigliato in produzione
Setup avanzato: ottimizzazione per corpus grandi
# Per corpus da migliaia di documenti, usa FAISS per il retrieval
# pip install faiss-gpu # oppure faiss-cpu
import faiss
import numpy as np
# Carica embedding pre-calcolati
corpus_embeddings = np.load("corpus_embeddings.npy").astype("float32")
# Crea indice FAISS
dimension = corpus_embeddings.shape[1] # 2048 per Qwen3-VL-2B
index = faiss.IndexFlatIP(dimension) # Inner product = cosine similarity su vettori normalizzati
faiss.normalize_L2(corpus_embeddings)
index.add(corpus_embeddings)
# Salva indice
faiss.write_index(index, "corpus.faiss")
# Retrieval ultra-veloce su corpus da 100k documenti
query_vec = np.array([query_embedding]).astype("float32")
faiss.normalize_L2(query_vec)
scores, indices = index.search(query_vec, k=10) # Top-10 in millisecondi
Risoluzione problemi comuni
- CUDA out of memory durante encode_document: riduci il batch_size da 8 a 2-4. Con 8GB VRAM, batch_size=4 per immagini ad alta risoluzione è il limite sicuro.
- ImportError: sentence_transformers ha bisogno di versione >= 5.4: esegui
pip install -U sentence-transformers[image]— l’upgrade non è automatico conpip install sentence-transformers. - revision=”refs/pr/23″ non trovata: il modello è ancora in PR. Se la PR viene mergiata, rimuovi il parametro revision. Controlla lo stato su huggingface.co/Qwen/Qwen3-VL-Embedding-2B.
- Flash Attention 2 non disponibile: rimuovi
attn_implementation: flash_attention_2— richiede GPU Ampere (RTX 3000+) o superiore. Su GPU più vecchie usa il default. - Similarità sempre vicina a 0 tra testo e immagini: stai usando
model.encode()invece dimodel.encode_query()emodel.encode_document(). I modelli asymmetrici come Qwen3-VL-Embedding usano prompt diversi per query e documento — le funzioni distinte li gestiscono correttamente. - Immagine non riconosciuta / errore PIL: installa Pillow aggiornato (
pip install -U Pillow) e verifica che il file non sia corrotto. Per URL remoti, aggiungi timeout nella richiesta HTTP prima di passare a encode.
