Pourquoi la recherche en deux étapes fonctionne
Comprenez le compromis entre rappel et précision dans une recherche en une seule étape, et comment un système de recherche rapide et approximatif suivi d’un reclassement lent mais précis permet de tirer le meilleur des deux approches.
Pourquoi la recherche en deux étapes fonctionne est une leçon AI Engineering Academy gratuite sur CoddyKit. Ceci est la leçon 1 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage AI Engineering Academy, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours AI Engineering Academy comprend 4 leçons au total.
Le compromis rappel-précision dans la recherche
Tout système de recherche doit faire face à un compromis fondamental : le rappel mesure le nombre de documents pertinents que vous trouvez (en avez-vous oublié ?), tandis que la précision mesure l'exactitude des premiers résultats (combien de documents récupérés sont réellement pertinents ?). Maximiser les deux simultanément est coûteux en calcul. Les retrievers rapides sacrifient la précision au profit du rappel ; les classeurs précis sacrifient la vitesse au profit de l'exactitude.
Bi-encodeur ou cross-encodeur : la distinction fondamentale
Les deux types de modèles au cœur de la recherche en deux étapes diffèrent par la manière dont ils examinent la requête et le document. Un bi-encodeur encode séparément la requête et chaque document, puis mesure la similarité entre leurs vecteurs : il est rapide, mais limité par cet encodage indépendant. Un cross-encodeur voit la requête et le document concaténés comme une seule entrée, ce qui permet une interaction approfondie entre eux : il est très précis, mais sa complexité est en O(n) par rapport à l'ensemble des candidats.
# Bi-encoder: compute query embedding ONCE, compare to all doc embeddings
# O(1) query encoding + O(n) dot products via ANN index = fast
query_vec = embed(query) # done once
results = vector_index.search(query_vec, top_k=100) # fast ANN search
# Cross-encoder: re-scores (query, doc) pairs jointly
# O(k) forward passes for k candidate documents = slow but accurate
for doc in results[:100]:
score = cross_encoder.score(query, doc.text) # joint scoringÉtape 1 : recherche grossière rapide
La première étape consiste en un retriever rapide, généralement un bi-encodeur associé à un index de plus proches voisins approximatifs ou à un index BM25, qui récupère un vaste ensemble de candidats (50 à 200 documents) avec un rappel élevé, mais une précision modérée. L'objectif n'est pas d'être exact, mais de ne manquer aucun document pertinent. Nous ratissons large et acceptons quelques faux positifs, sachant que la deuxième étape les éliminera.
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
# Stage 1: retrieve 100 candidates (high recall, modest precision)
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
coarse_retriever = vectorstore.as_retriever(
search_kwargs={'k': 100} # large candidate set
)
candidates = coarse_retriever.invoke(query)
print(f'Stage 1: retrieved {len(candidates)} candidate documents')Étape 2 : reclassement précis par cross-encodeur
La deuxième étape prend l'ensemble de candidats de l'étape 1 et réévalue chaque paire (requête, document) à l'aide d'un cross-encodeur qui lit les deux éléments ensemble. Comme il ne traite que 50 à 200 candidats, et non l'ensemble du corpus, il peut se permettre cet encodage conjoint coûteux. L'attention approfondie du cross-encodeur sur l'entrée concaténée le rend bien plus précis qu'un bi-encodeur pour estimer la pertinence réelle.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def rerank(query: str, candidates: list[str], top_k: int = 5) -> list[str]:
# Score each (query, document) pair jointly
pairs = [[query, doc] for doc in candidates]
scores = reranker.predict(pairs)
# Sort by score descending
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [doc for doc, _ in ranked[:top_k]]
candidate_texts = [doc.page_content for doc in candidates]
final_docs = rerank(query, candidate_texts, top_k=5)
print(f'Stage 2: selected top {len(final_docs)} documents after re-ranking')Pourquoi cette combinaison fonctionne
La conception en deux étapes exploite une asymétrie essentielle : la recherche ANN rapide de la première étape s'étend à des millions de documents en quelques millisecondes, tandis que le cross-encodeur précis de la seconde étape ne fonctionne que sur le petit ensemble de candidats. Vous obtenez la capacité de mise à l'échelle de la recherche approximative et la précision du calcul conjoint exact. Le pipeline global est à la fois rapide et très précis, ce qu'aucune des deux étapes ne permet d'obtenir seule.
Profil de latence de la recherche en deux étapes
Dans un pipeline en deux étapes typique, l'étape 1 (recherche ANN vectorielle sur 1 million de documents) prend 5 à 20 ms ; l'étape 2 (cross-encodeur sur 100 candidats) prend 100 à 500 ms selon la longueur des documents et le matériel. Le budget total de latence est de 150 à 600 ms, ce qui convient à la plupart des applications. L'accélération par GPU lors de l'étape 2 peut réduire le reclassement à moins de 30 ms pour les documents courts, rendant le pipeline compétitif en latence avec une recherche en une seule étape dans les applications sensibles à la latence.
import time
def two_stage_search(query, coarse_retriever, reranker, top_k=5):
t0 = time.perf_counter()
candidates = coarse_retriever.invoke(query) # stage 1
t1 = time.perf_counter()
candidate_texts = [c.page_content for c in candidates]
final_docs = rerank(query, candidate_texts, top_k) # stage 2
t2 = time.perf_counter()
print(f'Stage 1 (retrieval): {(t1-t0)*1000:.1f}ms')
print(f'Stage 2 (re-ranking): {(t2-t1)*1000:.1f}ms')
print(f'Total: {(t2-t0)*1000:.1f}ms')
return final_docsChoisir la taille appropriée de l'ensemble de candidats
La taille de l'ensemble de candidats de la première étape est un hyperparamètre essentiel. Si elle est trop petite (10, par exemple), des documents pertinents peuvent être manqués avant même le début du reclassement. Si elle est trop grande (500, par exemple), la latence de l'étape 2 explose. La courbe du rappel à N — le nombre de documents pertinents capturés pour différentes valeurs de N — vous aide à faire ce choix. Le meilleur compromis se situe généralement entre 50 et 150 candidats, là où le rappel est presque maximal tout en conservant une latence raisonnable.
def recall_at_n(coarse_retriever, test_queries, golden_relevant, n_values):
for n in n_values:
recalls = []
for query, relevant in zip(test_queries, golden_relevant):
# Temporarily set k to n
coarse_retriever.search_kwargs['k'] = n
results = coarse_retriever.invoke(query)
retrieved_ids = {r.metadata.get('id') for r in results}
relevant_found = len(set(relevant) & retrieved_ids)
recalls.append(relevant_found / len(relevant))
avg = sum(recalls) / len(recalls)
print(f'N={n}: recall={avg:.3f}')Première étape hybride et deuxième étape par cross-encodeur
La configuration en deux étapes la plus performante associe un retriever hybride (dense + BM25) pour la première étape et un cross-encodeur pour la deuxième. La recherche hybride maximise le rappel de la première étape en combinant la correspondance sémantique et celle des mots-clés, puis le cross-encodeur sélectionne avec précision les documents les plus pertinents dans l'ensemble de candidats combiné. Cette configuration atteint régulièrement une qualité de recherche de pointe sur les bancs d'essai.
from langchain.retrievers import EnsembleRetriever
# Stage 1: hybrid retrieval for maximum recall
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
)
# Stage 2: cross-encoder re-ranking for high precision
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
cross_encoder_model = HuggingFaceCrossEncoder(model_name='cross-encoder/ms-marco-MiniLM-L-6-v2')
compressor = CrossEncoderReranker(model=cross_encoder_model, top_n=5)
from langchain.retrievers import ContextualCompressionRetriever
two_stage = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=hybrid_retriever,
)API commerciales de reclassement
Si vous souhaitez bénéficier de la précision d'un cross-encodeur sans gérer votre propre modèle, Cohere Rerank et Jina AI Reranker proposent tous deux des API de reclassement hébergées dans le cloud. Vous envoyez une requête et une liste de textes de documents, puis recevez les scores de pertinence. Ces API utilisent de grands modèles cross-encodeurs (souvent de plus de 500 millions de paramètres), plus performants que les petits cross-encodeurs hébergés en interne, au prix d'une latence d'API supplémentaire (50 à 300 ms) et d'une facturation par document reclassé.
import cohere
co = cohere.Client('YOUR_API_KEY')
def cohere_rerank(query: str, documents: list[str], top_k: int = 5):
response = co.rerank(
model='rerank-english-v3.0',
query=query,
documents=documents,
top_n=top_k,
)
return [
{'text': documents[r.index], 'score': r.relevance_score}
for r in response.results
]
final = cohere_rerank(query, candidate_texts, top_k=5)
for doc in final:
print(f'Score {doc["score"]:.3f}: {doc["text"][:80]}')Quand la recherche en deux étapes est excessive
La recherche en deux étapes ajoute de la complexité et de la latence par rapport à une recherche en une seule étape. Elle n'est pas toujours nécessaire. Pour les petits corpus de moins de 10 000 documents, un seul cross-encodeur appliqué à l'ensemble peut être suffisamment rapide. Pour les applications où une latence inférieure à 100 ms est essentielle et où les gains de précision sont modestes, une recherche dense en une seule étape peut être préférable. Utilisez deux étapes lorsque vous disposez d'un corpus volumineux, que vos exigences de précision sont élevées et que vous pouvez accepter une latence de recherche de 200 à 500 ms.
Recherche en trois étapes à très grande échelle
Pour des corpus de dizaines de millions de documents, on utilise parfois une chaîne de traitement en trois étapes : la première étape récupère 10 000 candidats avec ANN, la deuxième les reclasse pour n’en retenir que 100 à l’aide d’un cross-encodeur rapide de petite taille, et la troisième les reclasse pour n’en retenir que 5 à l’aide d’un cross-encodeur puissant de grande taille. Chaque étape applique un modèle plus coûteux et plus précis à un ensemble plus restreint. Cette architecture est utilisée par les moteurs de recherche et les systèmes de questions-réponses sur documents à grande échelle.
Vérification rapide
Vérifiez votre compréhension des raisons pour lesquelles la récupération en deux étapes fonctionne dans cette leçon.
Récapitulatif de la leçon
Dans cette leçon, vous avez appris que les bi-encodeurs sont rapides, mais limités par l’encodage indépendant de la requête et du document, que les cross-encodeurs sont précis grâce à l’encodage conjoint, mais trop lents pour effectuer une recherche dans l’ensemble du corpus, et que la récupération en deux étapes combine les deux : une première étape rapide offrant un rappel élevé, suivie d’une deuxième étape précise offrant une haute précision. La première étape récupère beaucoup plus de candidats que nécessaire afin d’éviter de manquer des documents pertinents. Nous allons maintenant mettre en œuvre le reclassement avec un cross-encodeur à l’aide de Cohere et de BGE.
Apprends Python avec un tuteur IA — gratuit
Écris et exécute du vrai code dans ton navigateur, obtiens de l'aide instantanée d'un tuteur IA disponible 24h/24, et reprends là où tu t'es arrêté sur le web ou dans l'app.
- Cours
- 30
- Leçons
- 120
Questions Fréquemment Posées
La leçon « Pourquoi la recherche en deux étapes fonctionne » est-elle gratuite ?
Oui — le texte complet de « Pourquoi la recherche en deux étapes fonctionne » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours AI Engineering Academy, passe à CoddyKit PRO. Le cours AI Engineering Academy comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Pourquoi la recherche en deux étapes fonctionne » ?
Comprenez le compromis entre rappel et précision dans une recherche en une seule étape, et comment un système de recherche rapide et approximatif suivi d’un reclassement lent mais précis permet de ti… Tu pratiques AI Engineering Academy avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer AI Engineering Academy ?
Aucune expérience préalable n'est requise. AI Engineering Academy sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 1 sur 4.
Combien de temps prend la leçon « Pourquoi la recherche en deux étapes fonctionne » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon AI Engineering Academy ?
Oui. Chaque leçon AI Engineering Academy inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Pourquoi la recherche en deux étapes fonctionne
- Reclassement par encodeur croisé avec Cohere et BGE
- Compression contextuelle et filtrage par pertinence
- Mesurer l’impact du reclassement