La Génération Augmentée par Récupération (RAG) a révolutionné la façon dont les grands modèles de langage (LLM) interagissent avec les données privées. Cependant, la recherche sémantique pure (basée sur les vecteurs) présente un défaut critique : elle a souvent du mal avec les identifiants spécifiques, les noms propres ou les termes techniques exacts. Si vous demandez à un système RAG « Code d'erreur 40412 », une base de données vectorielle pourrait récupérer des pages sur les erreurs 404 génériques ou des erreurs « de niveau 400 » non liées, mais rarement la définition exacte dont vous avez besoin.
La solution ? La recherche hybride.
La recherche hybride combine la compréhension contextuelle des plongements vectoriels avec la précision de l'appariement traditionnel par mots-clés (recherche creuse). En fusionnant ces deux méthodologies, vous améliorez considérablement l'exhaustivité et la précision, garantissant que votre LLM récupère les documents les plus pertinents à chaque fois.
Pourquoi la recherche vectorielle pure est insuffisante
Les plongements vectoriels capturent le sens du texte. Ils sont excellents pour comprendre que « vols bon marché » et « billets d'avion à bas prix » sont similaires. Cependant, ils opèrent dans un espace de haute dimension où l'appariement exact de chaînes de caractères n'existe pas.
- Manque de spécificité : Les vecteurs ne peuvent pas distinguer entre « Pièce A » et « Pièce B » s'ils apparaissent dans des contextes similaires.
- Problèmes de tokenisation : Les ID uniques, les codes produits ou les noms propres rares peuvent avoir de mauvaises représentations de plongement car ils manquent de fréquence contextuelle dans les données d'entraînement.
- Lacunes d'exhaustivité : Si une requête contient un mot-clé qui est sémantiquement éloigné du document cible mais lexicalement identique, la recherche vectorielle le manquera.
À l'inverse, la recherche traditionnelle par mots-clés (comme BM25) est excellente pour trouver des correspondances exactes, mais échoue à comprendre l'intention. Elle ne peut pas dire que « comment réparer un tuyau qui fuit » et « réparation de plomberie pour les robinets » sont liés, à moins que les mots ne se chevauchent.
L'architecture de la recherche hybride
Un pipeline de recherche hybride typique implique trois étapes principales :
- Récupération dense : Plonger la requête et récupérer les K meilleurs résultats basés sur la similarité cosinus.
- Récupération creuse : Utiliser un moteur de recherche traditionnel (par exemple, Elasticsearch, recherche en texte intégral PostgreSQL) pour récupérer les K meilleurs résultats basés sur le chevauchement lexical (BM25).
- Fusion : Combiner les deux ensembles de résultats en utilisant un algorithme de classement comme la Reciprocal Rank Fusion (RRF).
Exemple d'implémentation
Ci-dessous se trouve un exemple en Python utilisant LangChain et Chroma (pour les vecteurs) avec une implémentation simple de BM25. En production, vous pourriez utiliser Elasticsearch ou OpenSearch pour la composante creuse.
import numpy as np
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.retrievers import ParentDocumentRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1. Configuration du magasin vectoriel (Recherche dense)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(texts, embeddings=embeddings)
vector_retriever = vectorstore.as_retriever(k=5)
# 2. Configuration de la recherche creuse (BM25)
# Note : Dans une application réelle, vous utiliseriez Elasticsearch ou un index TF-IDF/BM25
def bm25_search(query, documents, top_k=5):
# Pseudo-code pour le classement BM25
# Retourne une liste de (document_id, score)
pass
def reciprocal_rank_fusion(rank_lists, k=60):
"""
Fusionne plusieurs listes classées en une seule liste classée.
k est une constante (généralement 60) pour atténuer l'impact des éléments de rang inférieur.
"""
scores = {}
for i, rank_list in enumerate(rank_lists):
for rank, item in enumerate(rank_list, start=1):
if item not in scores:
scores[item] = 0
scores[item] += 1.0 / (k + rank)
# Trier par score décroissant
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
# 3. Fonction de récupération hybride
def hybrid_search(query, documents):
# Obtenir les résultats de la recherche vectorielle
vector_results = vector_retriever.get_relevant_documents(query)
vector_ids = [doc.metadata['id'] for doc in vector_results]
# Obtenir les résultats de BM25
bm25_results = bm25_search(query, documents, top_k=5)
bm25_ids = [res[0] for res in bm25_results]
# Fusionner les résultats
fused_ids, fused_scores = reciprocal_rank_fusion([vector_ids, bm25_ids])
# Retourner les 3 premiers documents
return fused_ids[:3]
Astuces pratiques pour l'ajustement
- Pondération : Vous pouvez appliquer des poids à la formule RRF si une méthode de recherche est historiquement plus fiable pour votre domaine. Par exemple, si les documents techniques s'appuient fortement sur les numéros de pièce, accordez un poids plus élevé à la recherche creuse.
- Modèles de plongement : Utilisez des modèles de plongement plus récents et performants (par exemple,
text-embedding-3-large) pour maximiser la qualité de la composante de recherche dense. - Indexation : Assurez-vous que votre index creux est optimisé. Utilisez des n-grammes ou des listes de synonymes dans la configuration de votre moteur de recherche pour traiter efficacement les abréviations et les pluriels.
Conclusion
La recherche hybride n'est plus une amélioration optionnelle pour les systèmes RAG ; c'est une bonne pratique pour les applications de niveau production. En comblant le fossé entre la compréhension sémantique et la précision lexicale, vous créez une couche de récupération à la fois robuste et précise. Commencez par une fusion RRF simple de vos recherches vectorielles et par mots-clés existantes, mesurez votre amélioration de la qualité des réponses, et itérez à partir de là. Le résultat sera un système qui comprend véritablement vos données—que ce soit en demandant le sens d'un concept ou l'ID exact d'un composant.