Classificando e filtrando resultados de pesquisa
Pontuação de relevância, remoção de duplicatas e seleção dos melhores resultados para o contexto.
Classificando e filtrando resultados de pesquisa é uma aula grátis de AI Agents no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de AI Agents, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de AI Agents inclui 4 aulas no total.
Por que a classificação e a filtragem são importantes
Uma API de pesquisa retorna de 5 a 10 resultados, mas eles não são igualmente relevantes, confiáveis ou úteis para a tarefa do agente. Enviar resultados brutos diretamente ao LLM desperdiça tokens de contexto e pode introduzir ruído ou informações incorretas.
A classificação e a filtragem melhoram a relação sinal-ruído antes que os resultados cheguem ao LLM.
Pontuação de relevância com BM25
BM25 (Melhor correspondência 25) é um algoritmo clássico de classificação de textos que pontua documentos com base na sobreposição de palavras-chave com a consulta. Ele funciona bem para correspondência lexical — quando a consulta e o documento compartilham as mesmas palavras.
Instale com 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]Pontuação de relevância com incorporações
O BM25 só identifica correspondências exatas entre palavras. A similaridade entre incorporações captura o significado semântico — assim, “desenvolvimento web com Python” e “criação de sites com Django” apresentam alta similaridade mesmo usando palavras diferentes.
Use a similaridade de cosseno entre a incorporação da consulta e as incorporações dos 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)Classificação híbrida: BM25 + incorporações
O BM25 e a pontuação das incorporações capturam aspectos diferentes da relevância. A classificação híbrida combina as duas pontuações usando uma média ponderada, aproveitando o melhor da correspondência lexical e 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)Deduplicação por URL
Os resultados da pesquisa geralmente contêm quase duplicatas: o mesmo artigo publicado por várias fontes parceiras ou a mesma página com parâmetros de URL diferentes. A deduplicação remove esses resultados antes que sejam enviados ao 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'])
Pontuação de qualidade do domínio
Um resultado de docs.python.org é mais confiável do que um resultado de um blog desconhecido. Atribua multiplicadores de qualidade a níveis de domínio e inclua-os na classificação 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}")
Filtragem de resultados de baixa qualidade
Alguns resultados têm baixa qualidade estrutural, independentemente do domínio: são curtos demais para serem úteis, contêm principalmente texto de navegação ou vêm de páginas que exigem login. Filtre-os antes da classificação.
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')
Truncamento de resultados para o orçamento de contexto
Mesmo após a filtragem, você pode ter 5 resultados de alta qualidade com 600 caracteres cada — isso totaliza 3.000 caracteres. Decida quantos resultados cabem no orçamento de contexto do seu LLM e faça o truncamento de acordo.
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')Formatação de resultados para o prompt do LLM
Depois de classificar, deduplicar e truncar, formate os resultados como uma lista numerada no prompt do LLM. As fontes numeradas facilitam a citação delas pelo modelo na resposta.
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}'
)Armazenamento em cache dos resultados da pesquisa
A mesma consulta pode ser repetida em diferentes sessões ou ciclos do agente. Armazene os resultados da pesquisa em cache com um TTL curto, por exemplo, 1 hora, para reduzir os custos da API e melhorar o tempo de resposta de 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 resultsFluxo completo de filtragem e classificação
Encadeie todas as etapas em uma única função de fluxo: buscar → filtrar resultados de baixa qualidade → deduplicar → classificar → aplicar bônus de domínio → truncar para o contexto → formatar para o 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ção de conhecimento
Qual é a principal vantagem da classificação híbrida (BM25 + incorporações) em comparação com o uso apenas do BM25?
Recapitulação: classificação e filtragem de resultados da pesquisa
Os resultados brutos da pesquisa precisam ser processados antes de chegar ao LLM. O fluxo é: filtrar resultados de baixa qualidade (curtos demais, com acesso pago) → deduplicar por URL e conteúdo → classificar por BM25 e/ou similaridade de incorporações → aplicar bônus de qualidade do domínio → truncar para o orçamento de contexto → formatar como fontes numeradas.
Armazene os resultados da pesquisa em cache para reduzir os custos da API em consultas repetidas. As citações numeradas no prompt permitem que o LLM atribua as afirmações a fontes específicas.
Perguntas Frequentes
A aula “Classificando e filtrando resultados de pesquisa” é grátis?
Sim — o texto completo de “Classificando e filtrando resultados de pesquisa” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de AI Agents, atualize para CoddyKit PRO. O curso de AI Agents inclui 4 aulas no total.
O que vou aprender em “Classificando e filtrando resultados de pesquisa”?
Pontuação de relevância, remoção de duplicatas e seleção dos melhores resultados para o contexto. Você pratica AI Agents com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar AI Agents?
Nenhuma experiência prévia é necessária. AI Agents no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.
Quanto tempo leva a aula “Classificando e filtrando resultados de pesquisa”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de AI Agents?
Sim. Cada aula de AI Agents inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Tavily e SerpAPI para pesquisa com agentes
- Classificando e filtrando resultados de pesquisa
- Padrão de ciclo de pesquisa aprofundada
- Combinando pesquisa na web com RAG