0Pricing
AI Agents · Lección

Clasificación y filtrado de resultados de búsqueda

Puntúe la relevancia, elimine duplicados y seleccione los mejores resultados para el contexto.

Clasificación y filtrado de resultados de búsqueda es una lección gratuita de AI Agents en CoddyKit. Esta es la lección 2 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de AI Agents, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de AI Agents incluye 4 lecciones en total.

Por qué son importantes la clasificación y el filtrado

Una API de búsqueda devuelve entre 5 y 10 resultados, pero no todos son igual de relevantes, fiables o útiles para la tarea del agente. Enviar los resultados sin procesar directamente al LLM desperdicia tokens de contexto y puede introducir ruido o información errónea.

La clasificación y el filtrado mejoran la relación entre señal y ruido antes de que los resultados lleguen al LLM.

Puntuación de relevancia con BM25

BM25 (Best Match 25) es un algoritmo clásico de clasificación de texto que asigna una puntuación a los documentos según la coincidencia de palabras clave con la consulta. Funciona bien para la coincidencia léxica, es decir, cuando la consulta y el documento comparten las mismas palabras.

Instálelo 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]

Puntuación de relevancia con embeddings

BM25 solo encuentra coincidencias entre palabras exactas. La similitud entre embeddings captura el significado semántico, por lo que «desarrollo web con Python» y «crear sitios web con Django» obtienen una similitud alta aunque usen palabras diferentes.

Utilice la similitud coseno entre el embedding de la consulta y los embeddings de los resultados.

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)

Clasificación híbrida: BM25 + embeddings

BM25 y la puntuación basada en embeddings capturan distintos aspectos de la relevancia. La clasificación híbrida combina ambas puntuaciones mediante un promedio ponderado para aprovechar al máximo la coincidencia léxica y la semántica.

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)

Eliminación de duplicados por URL

Los resultados de búsqueda suelen contener casi duplicados: el mismo artículo procedente de varias fuentes que lo han sindicado, o la misma página con distintos parámetros de URL. La eliminación de duplicados los quita antes de enviar los resultados al 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'])

Puntuación de calidad del dominio

Un resultado de docs.python.org es más fiable que uno de un blog cualquiera. Asigne multiplicadores de calidad a los distintos niveles de dominio e incorpórelos a la clasificación 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}")

Filtrado de resultados de baja calidad

Algunos resultados tienen una calidad estructuralmente baja independientemente de su dominio: son demasiado cortos para resultar útiles, contienen principalmente texto de navegación o proceden de páginas que requieren iniciar sesión. Fíltrelos antes de clasificarlos.

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

Truncamiento de resultados para el presupuesto de contexto

Incluso después del filtrado, puede tener 5 resultados de alta calidad con 600 caracteres cada uno: 3.000 caracteres en total. Determine cuántos resultados caben en el presupuesto de contexto de su LLM y trúnquelos según corresponda.

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

Formato de los resultados para el prompt del LLM

Después de clasificar, eliminar duplicados y truncar, dé formato a los resultados como una lista numerada en el prompt del LLM. Las fuentes numeradas facilitan que el modelo las cite en su respuesta.

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

Almacenamiento en caché de los resultados de búsqueda

La misma consulta puede repetirse en distintas sesiones o ciclos del agente. Almacene los resultados de búsqueda en caché con un TTL corto (por ejemplo, 1 hora) para reducir los costes de la API y mejorar el tiempo de respuesta de las consultas repetidas.

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 completo de filtrado y clasificación

Encadene todos los pasos en una única función de pipeline: obtener → filtrar los resultados de baja calidad → eliminar duplicados → clasificar → aplicar el impulso del dominio → truncar para el contexto → dar formato para el 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)

Comprobación de conocimientos

¿Cuál es la principal ventaja de la clasificación híbrida (BM25 + embeddings) frente a utilizar solo BM25?

Repaso: clasificación y filtrado de resultados de búsqueda

Los resultados de búsqueda sin procesar necesitan un tratamiento antes de llegar al LLM. El pipeline es: filtrar los resultados de baja calidad (demasiado cortos o protegidos por un muro de pago) → eliminar duplicados por URL y contenido → clasificar mediante BM25 o similitud entre embeddings, o ambos → aplicar mejoras por calidad del dominio → truncar según el presupuesto de contexto → dar formato como fuentes numeradas.

Almacene los resultados de búsqueda en caché para reducir los costes de la API en las consultas repetidas. Las citas numeradas del prompt permiten que el LLM atribuya las afirmaciones a fuentes específicas.

Preguntas frecuentes

¿La lección «Clasificación y filtrado de resultados de búsqueda» es gratis?

Sí — el texto completo de «Clasificación y filtrado de resultados de búsqueda» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de AI Agents, actualiza a CoddyKit PRO. El curso de AI Agents incluye 4 lecciones en total.

¿Qué aprenderé en «Clasificación y filtrado de resultados de búsqueda»?

Puntúe la relevancia, elimine duplicados y seleccione los mejores resultados para el contexto. Practicas AI Agents con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar AI Agents?

No se requiere experiencia previa. AI Agents en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 2 de 4.

¿Cuánto tiempo toma la lección «Clasificación y filtrado de resultados de búsqueda»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de AI Agents?

Sí. Cada lección de AI Agents incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Tavily y SerpAPI para búsquedas de agentes
  2. Clasificación y filtrado de resultados de búsqueda
  3. Patrón de ciclo de investigación profunda
  4. Combinación de búsqueda web con RAG
← Volver a AI Agents