Hybrid search: BM25 + dense embeddings
La ricerca moderna non è più una scelta binaria tra ricerca lessicale e semantica. La ricerca ibrida combina il meglio di entrambi i mondi: la precisione di BM25 per le corrispondenze esatte e la rilevanza semantica dei dense embeddings per comprendere il significato. In questo articolo scoprirai perché la ricerca ibrida è diventata lo standard nei sistemi di recupero dell'informazione moderni, come implementarla e quali sono i trade-off pratici.
Cosa è BM25 e come funziona
BM25 (Best Matching 25) è l'algoritmo di ranking più diffuso nel recupero dell'informazione moderno. È una formula probabilistica sviluppata negli anni '90 che calcola la rilevanza di un documento rispetto a una query in base alla frequenza dei termini.
L'algoritmo prende in considerazione tre fattori principali:
- Frequenza del termine (TF): quante volte il termine di ricerca appare nel documento
- Frequenza inversa del documento (IDF): quanti documenti contengono il termine (i termini rari sono più rilevanti)
- Lunghezza del documento: BM25 normalizza i risultati per evitare che i documenti lunghi ottengano sempre score più alti
La formula di base è:
score(D, Q) = Σ IDF(qi) * (f(qi, D) * (k1 + 1)) / (f(qi, D) + k1 * (1 - b + b * |D| / avgdl))
Dove k1 e b sono parametri di tuning (tipicamente k1=2.0, b=0.75).
BM25 è estremamente efficace per query con termini specifici e parole chiave ben definite. Se cerchi "macchina fotografica reflex Canon", BM25 troverà documenti che contengono esattamente questi termini. È rapido, interpretabile e non richiede addestramento.
Dense embeddings e ricerca semantica
I dense embeddings sono rappresentazioni numeriche di testo che catturano il significato semantico. Un modello di embedding converte una frase (o una parola, o un documento) in un vettore di numeri in uno spazio vettoriale ad alta dimensione.
A differenza di BM25 che lavora con token discreti, gli embeddings catturano relazioni concettuali. Per esempio:
- "automobile" e "macchina" avranno embeddings molto simili (distanza coseno bassa)
- "gatto" avrà un embedding più vicino a "animale" che a "frigorifero"
- "Roma" e "Capitale" avranno una distanza coseno ridotta nel spazio semantico
La ricerca con embeddings funziona così:
- Converte la query in un embedding usando lo stesso modello
- Calcola la similarità (di solito coseno) tra l'embedding della query e gli embeddings dei documenti indicizzati
- Restituisce i documenti con la similarità più alta
Modelli comuni per gli embeddings includono Sentence Transformers (all-MiniLM-L6-v2, all-mpnet-base-v2), OpenSearch Neural Search, e LLM embedding proprietari. Nel contesto di sistemi on-premises come Ollama, puoi usare modelli come nomic-embed-text o sentence-transformers.
Perché combinare BM25 e dense embeddings
Nessuno dei due approcci è perfetto isolatamente. BM25 eccelle nel trovare corrispondenze exact match, ma fallisce quando il linguaggio è diverso. Dense embeddings comprendono il significato, ma possono essere imprecisi con query specifiche e richiedono più risorse computazionali.
La ricerca ibrida sfrutta i punti di forza di entrambi:
- BM25 per catch esatti e termini specifici (username, codici prodotto, numeri)
- Dense embeddings per comprendere il significato e trovare documenti semanticamente simili
- Combinazione ponderata per ottenere il meglio dai due mondi
Uno studio su diverse workload di recupero ha dimostrato che la ricerca ibrida in media supera sia BM25 puro che embeddings puri di circa il 15-30% in termini di NDCG@10 (Normalized Discounted Cumulative Gain).
Strategie di combinazione
Ci sono diverse tecniche per fondere i risultati di BM25 e dense embeddings:
1. Ranking fusion semplice
Esegui entrambe le ricerche (BM25 e embedding) e combina i risultati con una media ponderata. Per ogni documento, calcola un score finale:
final_score = w1 * normalized_bm25_score + w2 * normalized_embedding_score
dove w1 e w2 sono pesi (tipicamente 0.5 + 0.5, ma puoi tunarli per il tuo use case).
2. Reciprocal Rank Fusion (RRF)
Se BM25 ritorna il documento con rank 1 e embeddings ritorna lo stesso documento con rank 3, il RRF score sarà:
RRF_score = 1/60 + 1/63 ≈ 0.0317
RRF è robusto e non richiede normalizzazione esplicita dei score eterogenei. È particolarmente utile quando i due sistemi hanno scale di score molto diverse.
3. Re-ranking a due stadi
Usa BM25 come primo filtro veloce (recupera top 100 documenti), poi riranking con embeddings. Questo riduce la computazione degli embeddings.
Implementazione con RediSearch e Ollama
Se usi Redis come vector store (RediSearch 2.4+), la ricerca ibrida è nativa:
import redis
import numpy as np
from sentence_transformers import SentenceTransformer
# Connetti a Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Modello embedding on-premise (es. Ollama con nomic-embed-text)
model = SentenceTransformer('model-name')
# Indice RediSearch ibrido
schema = [
{"name": "title", "type": "TEXT"},
{"name": "content", "type": "TEXT"},
{"name": "embedding", "type": "VECTOR", "algorithm": "HNSW", "dim": 768, "distance_metric": "cosine"}
]
# Query ibrida
query_text = "miglior configurazione per RAG"
query_embedding = model.encode(query_text)
# BM25 part
bm25_results = r.execute_command(
'FT.SEARCH', 'idx:hybrid',
f'(@title|@content):("{query_text}")',
'RETURN', '2', 'title', 'content',
'LIMIT', '0', '10'
)
# Vector search part
vector_results = r.execute_command(
'FT.SEARCH', 'idx:hybrid',
f'*=>[KNN 10 @embedding $vec]',
'PARAMS', '2', 'vec', query_embedding.tobytes(),
'RETURN', '2', 'title', 'content'
)
# Fusion: combina i risultati
combined = merge_results(bm25_results, vector_results, weights=(0.4, 0.6))
La chiave è usare la sintassi `*=>[KNN ...]` in RediSearch per il vector search combinato con query FT.SEARCH per BM25.
Trade-off e considerazioni pratiche
Latenza
La ricerca ibrida è più lenta della sola BM25 (BM25 ≈ 1-5ms, embeddings ≈ 10-50ms, ibrida ≈ 15-60ms). Se la latenza è critica (es. ricerca in tempo reale), considera:
- Caching degli embedding delle query frequenti
- Re-ranking: usa BM25 per filtrare velocemente, poi embeddings solo sui top N risultati
- Parallelizzazione delle richieste BM25 e vector search
Dimensione dell'indice
Gli embeddings richiedono storage significativo. Un modello 768-dim con 1 milione di documenti richiede circa 3GB di RAM in Redis (768 float32 * 1M * 4 byte). Se lo storage è un vincolo, considera embeddings più piccoli (384-dim) o compressione quantizzata.
Freshness
BM25 è istantaneo dopo l'indexing. Gli embeddings hanno un lag se l'indicizzazione batch è asincrona. Per update real-time, indicizza sia BM25 che embeddings in transazione.
Tuning dei pesi
I pesi w1 e w2 dipendono dal tuo use case. Usa un validation set e valuta NDCG, MRR (Mean Reciprocal Rank) o altre metriche di ranking. Tipicamente:
- Dataset con query molto specifiche (codici, numeri): w1=0.7, w2=0.3
- Dataset conversazionale: w1=0.3, w2=0.7
- Bilanciato: w1=0.5, w2=0.5
Casi d'uso reali
RAG (Retrieval-Augmented Generation)
La ricerca ibrida è ormai standard nei sistemi RAG. Per una query in linguaggio naturale, gli embeddings catturano l'intenzione, mentre BM25 trova documenti con termini specifici (nomi entità, dati tecnici). Il modello generativo riceve il contesto più rilevante.
E-commerce
Gli utenti cercano prodotti con query vaghe ("giacca blu comoda") e esatte (SKU, marca). BM25 gestisce SKU e brand, embeddings capiscono "blu" vs "azzurro". Il risultato: meno product-not-found.
Knowledge base interna
FAQ, documentazione tecnica, procedure. BM25 trova il numero di issue o il titolo esatto, embeddings trovano contenuto semanticamente simile anche se scritto diversamente.
Question Answering
Un sistema Q&A che combina BM25 e embeddings riduce le "no matching answer" e migliora l'accuracy del ranking dei candidati.
Strumenti e librerie
- RediSearch 2.4+: Supporto nativo per ricerca ibrida con `FT.SEARCH` + vector search
- Elasticsearch 8.x: Plugin `dense_vector` per embeddings + BM25 nativi, fusion con `rank fusion`
- Milvus 2.x: Vector database con HNSW, integrazione BM25 tramite sparse vector + dense vector
- Weaviate: Supporto per BM25 e vector search ibridi nativo
- Qdrant: Vector database moderno con hybrid search via fusion strategy
- Sentence Transformers: Libreria Python per generare embeddings (Apache 2.0)
- PyMilvus, Redis-py, Elasticsearch-py: Client Python per gli indici
Metriche di valutazione
Per valutare la qualità della ricerca ibrida, usa:
- NDCG@k: Normalized Discounted Cumulative Gain. Penalizza i risultati rilevanti che compaiono in basso. Target: > 0.7 per dataset ben bilanciato.
- MRR: Mean Reciprocal Rank. Media del reciproco del rank del primo risultato rilevante. Target: > 0.6 per query challenging.
- Precision@k: % di risultati rilevanti nei top k. Target: > 0.8@10.
- Recall@k: % di documenti rilevanti totali recuperati nei top k. Target dipende dal contesto (precision/recall trade-off).
Crea un validation set manuale (100-500 query con giudizi di rilevanza) e usa questi strumenti per tuning incrementale dei pesi.
Errori comuni e come evitarli
- Dimenticare la normalizzazione dei score: BM25 score spesso è 0-100+, embedding coseno è 0-1. Normalizza prima di combinare.
- Pesi fissi per tutti gli use case: Valuta sempre i pesi su dati reali. 0.5/0.5 non funziona ovunque.
- Ignorare il costo computazionale: Gli embeddings scalano male con corpus grandi. Dimensiona il tuo vector store correttamente.
- Non riscalare gli embeddings dopo aggiornamenti del modello: Se cambi modello embedding, ricrea gli indici. Embeddings da modelli diversi non sono compatibili.
- Assumere che la ricerca ibrida sia sempre migliore: Per dataset piccoli (< 10k doc) o molto specifici, BM25 puro può bastare. Misura sempre.
FAQ sulla ricerca ibrida
D: Devo mettere in indice sia BM25 che embeddings?
R: Sì, per la ricerca ibrida vera. BM25 richiede il testo full-text indicizzato, embeddings richiedono il vettore pre-calcolato. La maggior parte dei vector database moderni supporta entrambi nativamente.
D: Quale dimensione di embedding scegliere?
R: 768-dim è lo standard (all-MiniLM-L6-v2). Per dataset molto grandi (> 10M doc), considera 384-dim. Per precision massima, 1024-dim. Trade-off: più grande = più accurato ma più storage.
D: E se la query è molto lunga (intero paragrafo)?
R: Alcuni modelli di embedding hanno limite di token (512 per molti Sentence Transformers). Trancia il testo, embedda i chunks, media i vettori risultanti. O usa modelli recenti che supportano contesti più lunghi (es. BGE-M3).
D: È meglio un unico embedding o multi-embedding (chunking)?
R: Multi-embedding (un embedding per chunk di testo) permette recupero più granulare e spesso è più accurato. Aggiungi complessità in indexing, ma il retrieval rimane veloce.
D: Posso usare BM25 + embeddings senza un vector database specializzato?
R: Sì, ma è lento. PostgreSQL + pgvector + tsquery (full-text) è un'opzione DIY. Per produzione, usa RediSearch, Elasticsearch, Milvus o Qdrant.
Conclusioni
La ricerca ibrida BM25 + dense embeddings è ormai la norma nei sistemi di recupero dell'informazione moderni. Combina la precisione lessicale e la semantica in un unico framework robusto. Implementarla richiede:
- Un vector database che supporta la fusion (RediSearch, Elasticsearch, Milvus)
- Un modello di embedding (Sentence Transformers, Ollama)
- Un validation set per tuning dei pesi
- Attenzione ai trade-off di latenza, storage e costo
Se stai costruendo un sistema RAG, una knowledge base o un motore di ricerca moderno, la ricerca ibrida dovrebbe essere nel tuo toolkit dal primo giorno. Nel prossimo articolo di questa serie esploreremo strategie avanzate di re-ranking e fine-tuning dei modelli di embedding per use case specifici.

Commenti
Caricamento commenti...
Accedi per commentare