0Pricing
AI Agents · Lezione

Classificazione e filtraggio dei risultati di ricerca

Assegnazione di punteggi di pertinenza, deduplicazione e selezione dei risultati migliori per il contesto

Classificazione e filtraggio dei risultati di ricerca è una lezione AI Agents gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Agents, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Agents include 4 lezioni in totale.

Perché il ranking e il filtraggio sono importanti

Un'API di ricerca restituisce 5-10 risultati, ma non tutti sono ugualmente pertinenti, affidabili o utili per l'attività dell'agente. Inviare i risultati grezzi direttamente all'LLM spreca token di contesto e può introdurre rumore o disinformazione.

Il ranking e il filtraggio migliorano il rapporto segnale/rumore prima che i risultati raggiungano l'LLM.

Valutazione della rilevanza con BM25

BM25 (Best Match 25) è un algoritmo classico di ranking del testo che assegna un punteggio ai documenti in base alla sovrapposizione delle parole chiave con la query. Funziona bene per il matching lessicale — quando query e documento condividono le stesse parole.

Installi con 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]

Valutazione della rilevanza con gli embedding

BM25 esegue il matching solo delle parole esatte. La similarità tra embedding cattura il significato semantico — quindi «Python web development» e «building websites with Django» ottengono una similarità elevata anche se usano parole diverse.

Utilizzi la similarità coseno tra l'embedding della query e gli embedding dei risultati.

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)

Ranking ibrido: BM25 + embedding

BM25 e il punteggio basato sugli embedding catturano aspetti diversi della rilevanza. Il ranking ibrido combina entrambi i punteggi con una media ponderata, ottenendo il meglio del matching lessicale e di quello semantico.

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)

Deduplicazione per URL

I risultati di ricerca contengono spesso quasi-duplicati: lo stesso articolo proveniente da più fonti che lo ripubblicano oppure la stessa pagina con parametri URL diversi. La deduplicazione rimuove questi duplicati prima di passare i risultati all'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'])

Valutazione della qualità del dominio

Un risultato proveniente da docs.python.org è più affidabile di uno proveniente da un blog qualsiasi. Assegni moltiplicatori di qualità ai diversi livelli di dominio e li includa nel ranking finale.

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}")

Filtraggio dei risultati di bassa qualità

Alcuni risultati sono strutturalmente di bassa qualità, indipendentemente dal dominio: sono troppo brevi per essere utili, contengono soprattutto testo di navigazione oppure provengono da pagine che richiedono l'accesso. Li filtri prima del ranking.

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')

Troncamento dei risultati per il budget di contesto

Anche dopo il filtraggio, potrebbe avere 5 risultati di alta qualità da 600 caratteri ciascuno: in totale sono 3.000 caratteri. Decida quanti risultati rientrano nel budget di contesto dell'LLM e li tronchi di conseguenza.

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')

Formattazione dei risultati per il prompt dell'LLM

Dopo il ranking, la deduplicazione e il troncamento, formatti i risultati come elenco numerato nel prompt dell'LLM. Le fonti numerate permettono al modello di citarle facilmente nella risposta.

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}'
)

Memorizzazione nella cache dei risultati di ricerca

La stessa query può essere ripetuta in sessioni diverse o durante i cicli dell'agente. Memorizzi i risultati di ricerca nella cache con un TTL breve (ad es. 1 ora) per ridurre i costi delle API e migliorare i tempi di risposta delle query ripetute.

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

Pipeline completa di filtraggio e ranking

Concateni tutti i passaggi in un'unica funzione di pipeline: recuperare → filtrare la bassa qualità → deduplicare → eseguire il ranking → applicare il boost del dominio → troncare per il contesto → formattare per il prompt.

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)

Verifica delle conoscenze

Qual è il principale vantaggio del ranking ibrido (BM25 + embedding) rispetto all'uso del solo BM25?

Riepilogo: ranking e filtraggio dei risultati di ricerca

I risultati di ricerca grezzi devono essere elaborati prima di arrivare all'LLM. La pipeline è: filtrare i risultati di bassa qualità (troppo brevi, con accesso a pagamento) → deduplicare per URL e contenuto → eseguire il ranking con BM25 e/o la similarità tra embedding → applicare i boost basati sulla qualità del dominio → troncare in base al budget di contesto → formattare come fonti numerate.

Memorizzi nella cache i risultati di ricerca per ridurre i costi delle API in caso di query ripetute. Le citazioni numerate nel prompt consentono all'LLM di attribuire le affermazioni a fonti specifiche.

Domande Frequenti

La lezione «Classificazione e filtraggio dei risultati di ricerca» è gratuita?

Sì — il testo completo di «Classificazione e filtraggio dei risultati di ricerca» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Agents, passa a CoddyKit PRO. Il corso AI Agents include 4 lezioni in totale.

Cosa imparerò in «Classificazione e filtraggio dei risultati di ricerca»?

Assegnazione di punteggi di pertinenza, deduplicazione e selezione dei risultati migliori per il contesto Eserciti AI Agents con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare AI Agents?

Non è richiesta alcuna esperienza precedente. AI Agents su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.

Quanto tempo richiede la lezione «Classificazione e filtraggio dei risultati di ricerca»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione AI Agents?

Sì. Ogni lezione AI Agents include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Tavily e SerpAPI per la ricerca degli agenti
  2. Classificazione e filtraggio dei risultati di ricerca
  3. Pattern del ciclo di ricerca approfondita
  4. Combinazione della ricerca web con RAG
← Torna a AI Agents