5. RetrievalAvancé

Stratégies de Récupération Avancées pour le RAG

5 février 2025
13 min de lecture
Équipe de Recherche Ailog

Au-delà de la recherche de similarité basique : recherche hybride, expansion de requêtes, MMR et récupération multi-étapes pour de meilleures performances RAG.

TL;DR

  • Recherche hybride (sémantique + mots-clés) bat la recherche purement sémantique de 20-35%
  • Expansion de requêtes aide quand les requêtes sont vagues ou utilisent une terminologie différente
  • MMR réduit la redondance dans les résultats récupérés
  • Commencez simple : Pur sémantique → Ajoutez hybride → Optimisez avec reranking
  • Testez les stratégies de récupération côte à côte sur Ailog

Au-Delà de la Recherche de Similarité Simple

Le RAG de base utilise la similarité sémantique pour récupérer des documents. Bien qu'efficace, cette approche a des limitations :

  • Cécité aux mots-clés : Rate les correspondances de termes exacts (IDs de produit, noms propres)
  • Décalage requête-document : Questions formulées différemment des réponses
  • Redondance : Les chunks récupérés contiennent souvent des informations similaires
  • Insuffisance de contexte : Les k premiers chunks peuvent ne pas fournir un contexte complet

Les stratégies de récupération avancées abordent ces limitations.

Recherche Hybride

Combine la recherche sémantique (vectorielle) et lexicale (mots-clés).

BM25 + Recherche Vectorielle

BM25 (Best Matching 25) : Classement statistique par mots-clés

DEVELOPERpython
from rank_bm25 import BM25Okapi # Index documents tokenized_docs = [doc.split() for doc in documents] bm25 = BM25Okapi(tokenized_docs) # Keyword search keyword_scores = bm25.get_scores(query.split()) # Vector search vector_scores = cosine_similarity(query_embedding, doc_embeddings) # Combine scores (weighted average) alpha = 0.7 # Weight for vector search final_scores = alpha * vector_scores + (1 - alpha) * keyword_scores # Retrieve top-k top_k_indices = np.argsort(final_scores)[-k:][::-1]

Reciprocal Rank Fusion (RRF)

Combiner les classements de plusieurs récupérateurs.

DEVELOPERpython
def reciprocal_rank_fusion(rankings_list, k=60): """ rankings_list: List of ranked document IDs from different retrievers k: Constant (typically 60) """ scores = {} for ranking in rankings_list: for rank, doc_id in enumerate(ranking, start=1): if doc_id not in scores: scores[doc_id] = 0 scores[doc_id] += 1 / (k + rank) return sorted(scores.items(), key=lambda x: x[1], reverse=True) # Example usage vector_results = ["doc1", "doc3", "doc5", "doc2"] bm25_results = ["doc2", "doc1", "doc4", "doc3"] final_ranking = reciprocal_rank_fusion([vector_results, bm25_results]) # Result: [("doc1", score), ("doc2", score), ...]

Quand Utiliser la Recherche Hybride

Utilisez l'hybride quand :

  • Les requêtes contiennent des termes spécifiques (IDs, noms, termes techniques)
  • Mélange de correspondance sémantique et exacte nécessaire
  • Le domaine a un vocabulaire spécialisé

Utilisez le vecteur seul quand :

  • Requêtes en langage naturel
  • Gestion des synonymes critique
  • Recherche multilingue

Les benchmarks montrent :

  • L'hybride surpasse souvent les deux seuls de 10-20%
  • Particulièrement efficace pour les domaines techniques
  • Critique pour la recherche de produits, recherche de code

Expansion de Requêtes

Reformuler ou étendre les requêtes pour une meilleure récupération.

Génération Multi-Requêtes

Générer plusieurs variantes de requêtes.

DEVELOPERpython
def generate_query_variations(query, llm): prompt = f"""Étant donné la requête utilisateur, génère 3 variantes qui capturent différents aspects : Original: {query} Génère 3 variantes: 1. 2. 3. """ variations = llm.generate(prompt) all_queries = [query] + variations # Retrieve for each query all_results = [] for q in all_queries: results = retrieve(q, k=5) all_results.extend(results) # Deduplicate and rerank unique_results = deduplicate(all_results) return rerank(unique_results, query)

Avantages :

  • Capture plusieurs interprétations
  • Augmente le rappel
  • Gère les requêtes ambiguës

Coût :

  • Récupérations multiples (plus lent, plus cher)
  • Appel LLM pour la génération

HyDE (Hypothetical Document Embeddings)

Générer une réponse hypothétique, puis la rechercher.

DEVELOPERpython
def hyde_retrieval(query, llm, k=5): # Generate hypothetical answer prompt = f"""Écris un passage qui répondrait à cette question : Question: {query} Passage:""" hypothetical_answer = llm.generate(prompt) # Embed and search using the hypothetical answer answer_embedding = embed(hypothetical_answer) results = vector_search(answer_embedding, k=k) return results

Pourquoi ça marche :

  • Les réponses sont sémantiquement similaires aux réponses (pas aux questions)
  • Comble le fossé requête-document
  • Efficace quand les questions et réponses sont formulées différemment

Quand utiliser :

  • Systèmes de questions-réponses
  • Quand les requêtes sont des questions mais les documents sont des déclarations
  • Recherche académique/de recherche

Décomposition de Requêtes

Décomposer les requêtes complexes en sous-requêtes.

DEVELOPERpython
def decompose_query(complex_query, llm): prompt = f"""Décompose cette question complexe en sous-questions plus simples : Question: {complex_query} Sous-questions: 1. 2. 3. """ sub_questions = llm.generate(prompt) # Retrieve for each sub-question all_contexts = [] for sub_q in sub_questions: contexts = retrieve(sub_q, k=3) all_contexts.extend(contexts) # Generate final answer using all contexts final_answer = llm.generate( context=all_contexts, query=complex_query ) return final_answer

Cas d'usage :

  • Questions multi-sauts
  • Requêtes analytiques complexes
  • Quand une seule récupération est insuffisante

Maximal Marginal Relevance (MMR)

Réduire la redondance dans les résultats récupérés.

DEVELOPERpython
def mmr(query_embedding, doc_embeddings, documents, k=5, lambda_param=0.7): """ Maximize relevance while minimizing similarity to already-selected docs. lambda_param: Tradeoff between relevance (1.0) and diversity (0.0) """ selected = [] remaining = list(range(len(documents))) while len(selected) < k and remaining: mmr_scores = [] for i in remaining: # Relevance to query relevance = cosine_similarity( query_embedding, doc_embeddings[i] ) # Max similarity to already selected docs if selected: similarities = [ cosine_similarity(doc_embeddings[i], doc_embeddings[j]) for j in selected ] max_sim = max(similarities) else: max_sim = 0 # MMR score mmr_score = lambda_param * relevance - (1 - lambda_param) * max_sim mmr_scores.append((i, mmr_score)) # Select best MMR score best = max(mmr_scores, key=lambda x: x[1]) selected.append(best[0]) remaining.remove(best[0]) return [documents[i] for i in selected]

Paramètres :

  • lambda_param = 1.0 : Pertinence pure (pas de diversité)
  • lambda_param = 0.5 : Équilibre pertinence et diversité
  • lambda_param = 0.0 : Diversité maximale

Utiliser quand :

  • Les chunks récupérés sont très similaires
  • Besoin de perspectives diverses
  • Tâches de résumé

Récupération Parent-Enfant

Récupérer de petits chunks, retourner un contexte plus large.

DEVELOPERpython
class ParentChildRetriever: def __init__(self, documents): self.parents = [] # Original documents self.children = [] # Small chunks self.child_to_parent = {} # Mapping for doc_id, doc in enumerate(documents): # Split into small chunks for precise retrieval chunks = split_document(doc, chunk_size=256) for chunk_id, chunk in enumerate(chunks): self.children.append(chunk) self.child_to_parent[len(self.children) - 1] = doc_id self.parents.append(doc) # Embed children for retrieval self.child_embeddings = embed_batch(self.children) def retrieve(self, query, k=3): # Search small chunks for precision query_emb = embed(query) child_indices = vector_search(query_emb, self.child_embeddings, k=k) # Return parent documents for context parent_indices = [self.child_to_parent[i] for i in child_indices] unique_parents = list(set(parent_indices)) return [self.parents[i] for i in unique_parents]

Avantages :

  • Récupération précise (petits chunks)
  • Contexte riche (grands documents)
  • Le meilleur des deux mondes

Utiliser quand :

  • Besoin de contexte complet pour la génération
  • Les documents ont une hiérarchie naturelle (sections, paragraphes)
  • La fenêtre de contexte permet des chunks plus grands

Récupération d'Ensemble

Combiner plusieurs méthodes de récupération.

DEVELOPERpython
class EnsembleRetriever: def __init__(self, retrievers, weights=None): self.retrievers = retrievers self.weights = weights or [1.0] * len(retrievers) def retrieve(self, query, k=5): all_results = [] # Get results from each retriever for retriever, weight in zip(self.retrievers, self.weights): results = retriever.retrieve(query, k=k*2) # Overfetch # Weight scores for doc, score in results: all_results.append((doc, score * weight)) # Deduplicate and aggregate scores doc_scores = {} for doc, score in all_results: doc_id = doc['id'] if doc_id not in doc_scores: doc_scores[doc_id] = {'doc': doc, 'score': 0} doc_scores[doc_id]['score'] += score # Sort and return top-k ranked = sorted( doc_scores.values(), key=lambda x: x['score'], reverse=True ) return [item['doc'] for item in ranked[:k]]

Exemple d'ensemble :

DEVELOPERpython
ensemble = EnsembleRetriever( retrievers=[ VectorRetriever(embedding_model="openai"), BM25Retriever(), VectorRetriever(embedding_model="sentence-transformers") ], weights=[0.5, 0.3, 0.2] )

Récupération Auto-Requête

Extraire des filtres des requêtes en langage naturel.

DEVELOPERpython
def self_query_retrieval(query, llm, vector_db): # Extract structured query prompt = f"""Extrait les filtres de recherche de cette requête : Requête: {query} Extrait: - search_text: Texte de recherche sémantique - filters: Filtres de métadonnées (dict) Sortie (JSON):""" structured = llm.generate(prompt, format="json") # Example output: # { # "search_text": "meilleures pratiques support client", # "filters": {"department": "support", "date_range": "2024"} # } # Execute filtered search results = vector_db.query( text=structured['search_text'], filter=structured['filters'], k=5 ) return results

Avantages :

  • Exploite efficacement les métadonnées
  • Interface en langage naturel pour les filtres
  • Meilleure précision

Utiliser quand :

  • Métadonnées riches disponibles
  • Les requêtes contiennent des attributs filtrables
  • Filtrage basé sur le temps, la catégorie ou l'attribut nécessaire

Récupération Multi-Étapes

Pipeline de récupération du grossier au fin.

DEVELOPERpython
class MultiStageRetriever: def __init__(self, fast_retriever, accurate_reranker): self.retriever = fast_retriever self.reranker = accurate_reranker def retrieve(self, query, k=5): # Stage 1: Fast retrieval (overfetch) candidates = self.retriever.retrieve(query, k=k*10) # Stage 2: Accurate reranking reranked = self.reranker.rerank(query, candidates) # Return top-k return reranked[:k]

Étapes :

  1. Récupération (rapide, rappel élevé) : 100 candidats
  2. Reranking (précis, coûteux) : Top 10
  3. Optionnel : Affinement basé LLM : Top 3

Avantages :

  • Équilibre vitesse et précision
  • Rentable (modèles coûteux sur petit ensemble de candidats)
  • Résultats de meilleure qualité

Compression Contextuelle

Supprimer les parties non pertinentes des chunks récupérés.

DEVELOPERpython
def compress_context(query, chunks, llm): compressed = [] for chunk in chunks: prompt = f"""Extrait uniquement les parties pertinentes pour la requête : Requête: {query} Document: {chunk} Extrait pertinent:""" relevant_part = llm.generate(prompt, max_tokens=200) compressed.append(relevant_part) return compressed

Avantages :

  • Réduire l'utilisation de tokens
  • Adapter plus de chunks dans la fenêtre de contexte
  • Se concentrer sur les informations pertinentes

Coûts :

  • Appels LLM (coûteux)
  • Latence supplémentaire

Utiliser quand :

  • Le budget de tokens est serré
  • Les chunks récupérés sont longs et partiellement pertinents
  • Besoin d'adapter de nombreuses sources

Choisir une Stratégie de Récupération

Cadre de Décision

Commencez avec :

  • Recherche sémantique de base (similarité vectorielle)
  • k=3 à 5 chunks

Ajoutez la recherche hybride si :

  • Les requêtes contiennent des termes spécifiques
  • Le domaine a un vocabulaire spécialisé
  • Les performances s'améliorent lors de l'évaluation

Ajoutez l'expansion de requêtes si :

  • Les requêtes sont ambiguës
  • Le rappel est plus important que la précision
  • Prêt à accepter une latence/coût plus élevé

Ajoutez MMR si :

  • Les chunks récupérés sont redondants
  • Besoin de perspectives diverses
  • Tâches de résumé ou d'analyse

Ajoutez le reranking si :

  • Les résultats top-k ne sont pas systématiquement pertinents
  • Prêt à échanger latence contre qualité
  • Le budget le permet (le prochain guide couvre cela)

Impact sur les Performances

StratégieImpact LatenceImpact CoûtGain Qualité
Recherche hybride+20-50msFaible+10-20%
Multi-requêtes+3xÉlevé+15-25%
HyDE+appel LLMÉlevé+10-30%
MMR+10-50msFaible+5-15%
Parent-enfant+0-20msMoyen+10-20%
Reranking+50-200msMoyen+20-40%

Implémentation Pratique

Exemple LangChain

DEVELOPERpython
from langchain.retrievers import ( EnsembleRetriever, ContextualCompressionRetriever ) from langchain.retrievers.document_compressors import LLMChainExtractor # Ensemble: Vector + BM25 vector_retriever = vector_db.as_retriever(search_kwargs={"k": 5}) bm25_retriever = BM25Retriever.from_documents(documents) ensemble = EnsembleRetriever( retrievers=[vector_retriever, bm25_retriever], weights=[0.7, 0.3] ) # Add compression compressor = LLMChainExtractor.from_llm(llm) compressed_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble )

Exemple LlamaIndex

DEVELOPERpython
from llama_index import VectorStoreIndex, SimpleKeywordTableIndex from llama_index.retrievers import RouterRetriever # Create retrievers vector_retriever = VectorStoreIndex.from_documents(documents).as_retriever() keyword_retriever = SimpleKeywordTableIndex.from_documents(documents).as_retriever() # Router retriever (chooses based on query) router = RouterRetriever( selector=llm_selector, retriever_dict={ "vector": vector_retriever, "keyword": keyword_retriever } ) # Query-dependent routing results = router.retrieve("What are the system requirements?")

Surveillance de la Qualité de Récupération

Suivez ces métriques :

Métriques de Récupération :

  • Precision@k : Docs pertinents dans top-k
  • Recall@k : Docs pertinents récupérés / tous docs pertinents
  • MRR : Mean Reciprocal Rank du premier résultat pertinent

Métriques Utilisateur :

  • Notes de qualité de réponse
  • Taux de questions de suivi
  • Achèvement de tâche

Métriques Techniques :

  • Latence de récupération
  • Taux de succès du cache
  • Débit de requêtes

Conseil d'Expert d'Ailog : La recherche hybride est l'amélioration de récupération avec le meilleur ROI. Ajouter la recherche par mots-clés BM25 à côté de la recherche sémantique offre systématiquement 20-35% de meilleurs résultats dans tous les domaines. C'est plus facile à implémenter que l'expansion de requêtes et plus fiable que l'ajustement MMR. Si vous ne faites qu'une seule optimisation de récupération, faites la recherche hybride. Nous l'avons implémentée en production et avons vu des gains de qualité immédiats avec une complexité minimale.

Tester les Stratégies de Récupération sur Ailog

Comparez les méthodes de récupération avec vos documents :

Ailog vous permet de benchmarker :

  • Recherche sémantique pure vs recherche hybride
  • Différentes techniques d'expansion de requêtes
  • MMR vs récupération standard
  • Filtrage de métadonnées personnalisé

Voir les métriques réelles : Comparaisons Precision@k, MRR, latence

Commencez les tests → Accès gratuit à toutes les stratégies de récupération.

Prochaines Étapes

Les documents récupérés ont souvent besoin de reranking pour optimiser la requête spécifique. Le prochain guide couvre les stratégies de reranking et les modèles cross-encoder pour améliorer davantage la qualité du RAG.

Tags

récupérationrecherche-hybridequery expansionmmr

Articles connexes

Ailog Assistant

Ici pour vous aider

Salut ! Pose-moi des questions sur Ailog et comment intégrer votre RAG dans vos projets !