AI Agents · Leçon

Classer et filtrer les résultats de recherche

Évaluez la pertinence, éliminez les doublons et sélectionnez les meilleurs résultats pour le contexte.

Leçon 2 sur 413 étapes

Classer et filtrer les résultats de recherche est une leçon AI Agents gratuite sur CoddyKit. Ceci est la leçon 2 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 Agents, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours AI Agents comprend 4 leçons au total.

Pourquoi le classement et le filtrage sont importants

Une API de recherche renvoie 5 à 10 résultats, mais ils ne sont pas tous aussi pertinents, fiables ou utiles pour la tâche de l'agent. Transmettre directement les résultats bruts au LLM gaspille des jetons de contexte et peut introduire du bruit ou de la désinformation.

Le classement et le filtrage améliorent le rapport signal/bruit avant que les résultats n'atteignent le LLM.

Évaluation de la pertinence avec BM25

BM25 (Meilleure correspondance 25) est un algorithme classique de classement de textes qui évalue les documents selon le recoupement des mots-clés avec la requête. Il fonctionne bien pour la correspondance lexicale, lorsque la requête et le document contiennent les mêmes mots.

Installez-le avec pip install rank-bm25.

from rank_bm25 import BM25Okapi

def rank_with_bm25(query, results):
    # Tokenize: lowercase and split into words
    tokenized_results = [
        r['content'].lower().split()
        for r in results
    ]
    bm25 = BM25Okapi(tokenized_results)

    query_tokens = query.lower().split()
    scores = bm25.get_scores(query_tokens)

    # Sort results by score descending
    ranked = sorted(
        zip(scores, results),
        key=lambda x: x[0],
        reverse=True
    )
    return [(score, result) for score, result in ranked]

Évaluation de la pertinence avec des représentations vectorielles

BM25 ne fait correspondre que les mots exacts. La similarité des représentations vectorielles capture le sens sémantique : « développement web en Python » et « création de sites web avec Django » peuvent ainsi obtenir une forte similarité malgré des mots différents.

Utilisez la similarité cosinus entre la représentation vectorielle de la requête et celles des résultats.

import numpy as np
import openai
import os

client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

def embed(text):
    resp = client.embeddings.create(
        model='text-embedding-3-small',
        input=text[:8000]
    )
    return np.array(resp.data[0].embedding)

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def rank_by_embedding(query, results):
    q_emb = embed(query)
    scored = []
    for r in results:
        r_emb = embed(r['content'][:1000])
        score = cosine_similarity(q_emb, r_emb)
        scored.append((score, r))
    return sorted(scored, key=lambda x: x[0], reverse=True)

Classement hybride : BM25 + représentations vectorielles

BM25 et l'évaluation fondée sur les représentations vectorielles capturent des aspects différents de la pertinence. Le classement hybride combine ces deux évaluations au moyen d'une moyenne pondérée, afin de tirer le meilleur parti des correspondances lexicales et sémantiques.

def hybrid_rank(query, results, bm25_weight=0.4, embed_weight=0.6):
    # Get BM25 scores (normalized 0-1)
    bm25_scored = rank_with_bm25(query, results)
    max_bm25 = max(s for s, _ in bm25_scored) or 1
    bm25_norm = {r['url']: s / max_bm25 for s, r in bm25_scored}

    # Get embedding scores
    embed_scored = rank_by_embedding(query, results)
    embed_norm = {r['url']: s for s, r in embed_scored}

    # Combine
    combined = []
    for r in results:
        url = r['url']
        score = (bm25_weight * bm25_norm.get(url, 0) +
                 embed_weight * embed_norm.get(url, 0))
        combined.append((score, r))

    return sorted(combined, key=lambda x: x[0], reverse=True)

Déduplication par URL

Les résultats de recherche contiennent souvent des quasi-doublons : le même article provenant de plusieurs sources syndiquées, ou la même page avec des paramètres d'URL différents. La déduplication les supprime avant de transmettre les résultats au LLM.

from urllib.parse import urlparse, urlunparse

def normalize_url(url):
    parsed = urlparse(url)
    # Remove query params and fragment (tracking params, etc.)
    clean = parsed._replace(query='', fragment='')
    return urlunparse(clean).rstrip('/')

def deduplicate_results(results):
    seen_urls = set()
    unique = []
    for r in results:
        url = normalize_url(r.get('url', ''))
        if url not in seen_urls:
            seen_urls.add(url)
            unique.append(r)
    return unique

# Also deduplicate by content similarity (near-duplicate detection)
def deduplicate_by_content(results, min_unique_ratio=0.7):
    unique = [results[0]] if results else []
    for candidate in results[1:]:
        cand_words = set(candidate['content'].lower().split())
        is_duplicate = False
        for kept in unique:
            kept_words = set(kept['content'].lower().split())
            overlap = len(cand_words & kept_words) / max(len(cand_words), 1)
            if overlap > (1 - min_unique_ratio):
                is_duplicate = True
                break
        if not is_duplicate:
            unique.append(candidate)
    return unique

if __name__ == '__main__':
    demo_results = [
        {'url': 'https://example.com/a?utm_source=x'},
        {'url': 'https://example.com/a'},
        {'url': 'https://example.com/b'},
    ]
    unique = deduplicate_results(demo_results)
    print(f'{len(demo_results)} results -> {len(unique)} unique')
    for r in unique:
        print(' -', r['url'])

Évaluation de la qualité du domaine

Un résultat provenant de docs.python.org est plus fiable qu'un résultat provenant d'un blog quelconque. Attribuez des coefficients de qualité aux différents niveaux de domaine et intégrez-les au classement final.

DOMAIN_QUALITY = {
    # Tier 1 — authoritative (1.3x boost)
    'docs.python.org': 1.3,
    'developer.mozilla.org': 1.3,
    'arxiv.org': 1.3,
    'github.com': 1.2,
    'stackoverflow.com': 1.2,
    # Tier 2 — good (1.0x, no change)
    # Tier 3 — low quality (penalty)
    'pinterest.com': 0.3,
    'quora.com': 0.5,
    'wikihow.com': 0.6
}

def get_domain_multiplier(url):
    from urllib.parse import urlparse
    domain = urlparse(url).netloc.lower().replace('www.', '')
    return DOMAIN_QUALITY.get(domain, 1.0)  # default: no change

def apply_domain_boost(scored_results):
    boosted = []
    for score, r in scored_results:
        multiplier = get_domain_multiplier(r.get('url', ''))
        boosted.append((score * multiplier, r))
    return sorted(boosted, key=lambda x: x[0], reverse=True)

if __name__ == '__main__':
    scored = [(1.0, {'url': 'https://pinterest.com/x'}), (1.0, {'url': 'https://docs.python.org/x'})]
    for score, r in apply_domain_boost(scored):
        print(f"{r['url']}: boosted score {score:.2f}")

Filtrage des résultats de faible qualité

Certains résultats sont structurellement de faible qualité, quel que soit leur domaine : ils sont trop courts pour être utiles, contiennent principalement du texte de navigation ou proviennent de pages protégées par une connexion. Filtrez-les avant le classement.

MIN_CONTENT_LENGTH = 200  # characters

LOW_QUALITY_SIGNALS = [
    'sign in to view',
    'please log in',
    'subscribe to read',
    '404 not found',
    'access denied',
    'this content is for members only'
]

def is_quality_result(result):
    content = result.get('content', '')

    # Too short
    if len(content) < MIN_CONTENT_LENGTH:
        return False

    # Paywall / access barrier detected
    content_lower = content.lower()
    for signal in LOW_QUALITY_SIGNALS:
        if signal in content_lower:
            return False

    return True

def filter_results(results):
    return [r for r in results if is_quality_result(r)]

if __name__ == '__main__':
    demo_results = [
        {'content': 'Please log in to view this article which has plenty of extra padding text here.'},
        {'content': 'A' * 250},
    ]
    kept = filter_results(demo_results)
    print(f'{len(demo_results)} results -> {len(kept)} passed quality filter')

Troncature des résultats pour respecter le budget de contexte

Même après le filtrage, vous pouvez avoir 5 résultats de haute qualité de 600 caractères chacun, soit 3 000 caractères au total. Déterminez combien de résultats tiennent dans le budget de contexte de votre LLM et tronquez-les en conséquence.

MAX_CONTEXT_CHARS = 4000
MAX_SNIPPET_CHARS = 600

def truncate_for_context(ranked_results, budget=MAX_CONTEXT_CHARS):
    selected = []
    used_chars = 0

    for score, result in ranked_results:
        content = result.get('content', '')[:MAX_SNIPPET_CHARS]
        entry = f"Source: {result['title']}\nURL: {result['url']}\nContent: {content}"
        entry_len = len(entry)

        if used_chars + entry_len > budget:
            break

        selected.append(result)
        used_chars += entry_len

    return selected

ranked_results = [
    (0.9, {'title': 'Doc A', 'url': 'http://a', 'content': 'x' * 800}),
    (0.7, {'title': 'Doc B', 'url': 'http://b', 'content': 'y' * 800}),
]
budget = 1000
selected = truncate_for_context(ranked_results, budget=budget)
print(f'Selected {len(selected)} results within {budget}-char budget')

Mise en forme des résultats pour la consigne du LLM

Après le classement, la déduplication et la troncature, mettez les résultats en forme sous la forme d'une liste numérotée dans la consigne du LLM. Les sources numérotées permettent au modèle de les citer facilement dans sa réponse.

def format_results_for_prompt(results):
    lines = ['Here are relevant search results:\n']
    for i, r in enumerate(results, 1):
        lines.append(f'[{i}] {r["title"]}')
        lines.append(f'    URL: {r["url"]}')
        lines.append(f'    {r.get("content", "")[:400]}')
        lines.append('')
    lines.append('Use these sources to answer the question. Cite sources as [1], [2], etc.')
    return '\n'.join(lines)

# Usage in agent prompt
formatted = format_results_for_prompt(selected_results)
response = llm_call(
    system='You are a research assistant.',
    user=f'{formatted}\n\nQuestion: {user_question}'
)

Mise en cache des résultats de recherche

La même requête peut être répétée au cours de plusieurs sessions ou boucles d'agent. Mettez les résultats de recherche en cache avec un TTL court (par ex., 1 heure) afin de réduire les coûts d'API et d'améliorer le temps de réponse des requêtes répétées.

import hashlib
import time

search_cache = {}  # In production: use Redis or disk cache
CACHE_TTL = 3600  # 1 hour

def cached_search(query, **kwargs):
    cache_key = hashlib.md5(query.encode()).hexdigest()
    entry = search_cache.get(cache_key)

    if entry and (time.time() - entry['ts']) < CACHE_TTL:
        print('Search cache hit')
        return entry['results']

    results = client.search(query=query, **kwargs)
    search_cache[cache_key] = {
        'results': results,
        'ts': time.time()
    }
    return results

Chaîne complète de filtrage et de classement

Enchaînez toutes les étapes dans une seule fonction de chaîne de traitement : récupérer → filtrer les résultats de faible qualité → dédupliquer → classer → appliquer le renforcement du domaine → tronquer pour respecter le contexte → mettre en forme pour la consigne.

def search_and_rank(query, max_context_chars=4000):
    # 1. Fetch
    raw = cached_search(query, max_results=8)
    results = raw.get('results', [])

    # 2. Filter quality
    results = filter_results(results)

    # 3. Deduplicate
    results = deduplicate_results(results)

    # 4. Rank (BM25 fast path — save embedding costs)
    scored = rank_with_bm25(query, results)

    # 5. Domain boost
    scored = apply_domain_boost(scored)

    # 6. Truncate to context budget
    selected = truncate_for_context(scored, budget=max_context_chars)

    # 7. Format
    return format_results_for_prompt(selected)

Vérification des connaissances

Quel est le principal avantage du classement hybride (BM25 + représentations vectorielles) par rapport à l'utilisation de BM25 seul ?

Récapitulatif : classement et filtrage des résultats de recherche

Les résultats de recherche bruts doivent être traités avant d'atteindre le LLM. La chaîne de traitement consiste à : filtrer les résultats de faible qualité (trop courts, protégés par un péage) → dédupliquer par URL et par contenu → classer avec BM25 et/ou la similarité des représentations vectorielles → appliquer des renforcements selon la qualité du domaine → tronquer pour respecter le budget de contexte → mettre en forme sous forme de sources numérotées.

Mettez les résultats de recherche en cache afin de réduire les coûts d'API pour les requêtes répétées. Les citations numérotées dans la consigne permettent au LLM d'attribuer les affirmations à des sources précises.

Gratuit pour commencer

Apprends AI Agents 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
60
Leçons
239

Questions Fréquemment Posées

La leçon « Classer et filtrer les résultats de recherche » est-elle gratuite ?

Oui — le texte complet de « Classer et filtrer les résultats de recherche » 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 Agents, passe à CoddyKit PRO. Le cours AI Agents comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Classer et filtrer les résultats de recherche » ?

Évaluez la pertinence, éliminez les doublons et sélectionnez les meilleurs résultats pour le contexte. Tu pratiques AI Agents 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 Agents ?

Aucune expérience préalable n'est requise. AI Agents 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 2 sur 4.

Combien de temps prend la leçon « Classer et filtrer les résultats de recherche » ?

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 Agents ?

Oui. Chaque leçon AI Agents 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

  1. Tavily et SerpAPI pour la recherche des agents
  2. Classer et filtrer les résultats de recherche
  3. Schéma de boucle de recherche approfondie
  4. Combiner recherche Web et RAG
← Retour à AI Agents