Suchergebnisse bewerten und filtern
Relevanz bewerten, Duplikate entfernen und die besten Ergebnisse für den Kontext auswählen.
Suchergebnisse bewerten und filtern ist eine kostenlose AI Agents-Lektion auf CoddyKit. Dies ist Lektion 2 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des AI Agents-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der AI Agents-Kurs umfasst insgesamt 4 Lektionen.
Warum Ranking und Filterung wichtig sind
Eine Such-API liefert 5–10 Ergebnisse, aber nicht alle sind für die Aufgabe des Agenten gleichermaßen relevant, zuverlässig oder nützlich. Ungefilterte Ergebnisse, die direkt an das LLM übergeben werden, verbrauchen Kontext-Token und können Rauschen oder Fehlinformationen einführen.
Ranking und Filterung verbessern das Signal-Rausch-Verhältnis, bevor die Ergebnisse das LLM erreichen.
Relevanzbewertung mit BM25
BM25 (Best Match 25) ist ein klassischer Algorithmus zur Textbewertung, der Dokumente anhand der Übereinstimmung von Schlüsselwörtern mit der Suchanfrage bewertet. Er eignet sich gut für den lexikalischen Abgleich – also wenn Suchanfrage und Dokument dieselben Wörter enthalten.
Installieren Sie ihn mit 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]Relevanzbewertung mit Embeddings
BM25 findet nur exakte Wortübereinstimmungen. Die Ähnlichkeit von Embeddings erfasst die semantische Bedeutung – daher erzielen „Python web development“ und „building websites with Django“ eine hohe Ähnlichkeit, obwohl sie unterschiedliche Wörter verwenden.
Verwenden Sie die Kosinusähnlichkeit zwischen dem Embedding der Suchanfrage und den Embeddings der Ergebnisse.
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)Hybrides Ranking: BM25 + Embeddings
BM25 und die Bewertung anhand von Embeddings erfassen unterschiedliche Aspekte der Relevanz. Das hybride Ranking kombiniert beide Bewertungen mit einem gewichteten Mittelwert und vereint so die Vorteile des lexikalischen und des semantischen Abgleichs.
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)Deduplizierung anhand der URL
Suchergebnisse enthalten häufig nahezu identische Einträge: denselben Artikel aus mehreren syndizierten Quellen oder dieselbe Seite mit unterschiedlichen URL-Parametern. Durch die Deduplizierung werden diese Einträge entfernt, bevor die Ergebnisse an das LLM übergeben werden.
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'])
Bewertung der Domainqualität
Ein Ergebnis von docs.python.org ist vertrauenswürdiger als eines aus einem beliebigen Blog. Weisen Sie Domain-Kategorien Qualitätsfaktoren zu und berücksichtigen Sie diese beim finalen Ranking.
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}")
Ergebnisse mit niedriger Qualität filtern
Einige Ergebnisse sind unabhängig von ihrer Domain strukturell von geringer Qualität: Sie sind zu kurz, enthalten hauptsächlich Navigationstext oder stammen von Seiten, die eine Anmeldung erfordern. Filtern Sie diese Ergebnisse vor dem Ranking heraus.
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')
Ergebnisse an das Kontextbudget anpassen
Selbst nach dem Filtern können 5 hochwertige Ergebnisse mit jeweils 600 Zeichen übrig bleiben – insgesamt also 3.000 Zeichen. Legen Sie fest, wie viele Ergebnisse in das Kontextbudget Ihres LLM passen, und kürzen Sie sie entsprechend.
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')Ergebnisse für den LLM-Prompt formatieren
Formatieren Sie die Ergebnisse nach dem Ranking, der Deduplizierung und dem Kürzen als nummerierte Liste im LLM-Prompt. Nummerierte Quellen erleichtern es dem Modell, sie in seiner Antwort zu zitieren.
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}'
)Suchergebnisse zwischenspeichern
Dieselbe Suchanfrage kann in mehreren Sitzungen oder Agentenschleifen wiederholt werden. Speichern Sie Suchergebnisse mit einer kurzen TTL (z. B. 1 Stunde) zwischen, um API-Kosten zu senken und die Antwortzeit bei wiederholten Suchanfragen zu verbessern.
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 resultsVollständige Pipeline zum Filtern und Ranken
Verketten Sie alle Schritte in einer einzigen Pipeline-Funktion: abrufen → Ergebnisse mit niedriger Qualität filtern → deduplizieren → ranken → Domain-Boost anwenden → an das Kontextbudget anpassen → für den Prompt formatieren.
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)Wissenscheck
Was ist der wichtigste Vorteil des hybriden Rankings (BM25 + Embeddings) gegenüber der alleinigen Verwendung von BM25?
Zusammenfassung: Suchergebnisse ranken und filtern
Rohe Suchergebnisse müssen verarbeitet werden, bevor sie das LLM erreichen. Die Pipeline: Ergebnisse mit niedriger Qualität filtern (zu kurz, durch eine Bezahlschranke geschützt) → anhand von URL und Inhalt deduplizieren → nach BM25 und/oder Embedding-Ähnlichkeit ranken → Domain-Qualitäts-Boosts anwenden → an das Kontextbudget anpassen → als nummerierte Quellen formatieren.
Speichern Sie Suchergebnisse zwischen, um bei wiederholten Suchanfragen API-Kosten zu senken. Nummerierte Zitate im Prompt ermöglichen es dem LLM, Aussagen bestimmten Quellen zuzuordnen.
Häufig gestellte Fragen
Ist die Lektion „Suchergebnisse bewerten und filtern“ kostenlos?
Ja — der vollständige Text von „Suchergebnisse bewerten und filtern“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des AI Agents-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der AI Agents-Kurs umfasst insgesamt 4 Lektionen.
Was lerne ich in „Suchergebnisse bewerten und filtern“?
Relevanz bewerten, Duplikate entfernen und die besten Ergebnisse für den Kontext auswählen. Du übst AI Agents mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.
Brauche ich Erfahrung, um AI Agents zu starten?
Keine Vorkenntnisse erforderlich. AI Agents auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 2 von 4.
Wie lange dauert die Lektion „Suchergebnisse bewerten und filtern“?
Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.
Kann ich in dieser AI Agents-Lektion Code schreiben und ausführen?
Ja. Jede AI Agents-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.
Alle Lektionen in diesem Kurs
- Tavily und SerpAPI für die Agentensuche
- Suchergebnisse bewerten und filtern
- Muster für Deep-Research-Schleifen
- Websuche mit RAG kombinieren