Zoekresultaten rangschikken en filteren
Relevantie scoren, duplicaten verwijderen en de beste resultaten voor de context selecteren.
Zoekresultaten rangschikken en filteren is een gratis AI-agenten-les op CoddyKit. Dit is les 2 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject AI-agenten. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus AI-agenten bevat in totaal 4 lessen.
Waarom rangschikken en filteren belangrijk zijn
Een zoek-API retourneert 5-10 resultaten, maar niet alle resultaten zijn even relevant, betrouwbaar of nuttig voor de taak van de agent. Rechtstreeks aan de LLM gevoerde onbewerkte resultaten verspillen contexttokens en kunnen ruis of desinformatie introduceren.
Rangschikken en filteren verbetert de signaal-ruisverhouding voordat de resultaten de LLM bereiken.
Relevantie bepalen met BM25
BM25 (Best Match 25) is een klassiek algoritme voor tekstrangschikking dat documenten beoordeelt op basis van de overlap van trefwoorden met de zoekopdracht. Het werkt goed voor lexicaal zoeken — wanneer de zoekopdracht en het document dezelfde woorden bevatten.
Installeer het met 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]Relevantie bepalen met embeddings
BM25 koppelt alleen exacte woorden. Overeenkomst tussen embeddings legt de semantische betekenis vast — daardoor krijgen 'Python-webontwikkeling' en 'websites bouwen met Django' een hoge overeenkomstsscore, ook al gebruiken ze verschillende woorden.
Gebruik de cosinusovereenkomst tussen de embedding van de zoekopdracht en de embeddings van de resultaten.
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)Hybride rangschikking: BM25 + embeddings
BM25 en scores op basis van embeddings leggen verschillende aspecten van relevantie vast. Hybride rangschikking combineert beide scores met een gewogen gemiddelde, zodat je het beste van lexicaal en semantisch zoeken krijgt.
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)Ontdubbelen op basis van URL
Zoekresultaten bevatten vaak bijna-duplicaten: hetzelfde artikel van meerdere gesyndiceerde bronnen of dezelfde pagina met verschillende URL-parameters. Door deze resultaten te ontdubbelen, verwijder je ze voordat je de resultaten aan de LLM doorgeeft.
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'])
Kwaliteitsscore van domeinen
Een resultaat van docs.python.org is betrouwbaarder dan een resultaat van een willekeurig blog. Wijs kwaliteitsfactoren toe aan domeinniveaus en verwerk die in de uiteindelijke rangschikking.
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}")
Resultaten van lage kwaliteit filteren
Sommige resultaten zijn structureel van lage kwaliteit, ongeacht hun domein: ze zijn te kort om nuttig te zijn, bevatten voornamelijk navigatietekst of komen van pagina's waarvoor je moet inloggen. Filter deze resultaten voordat je ze rangschikt.
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')
Resultaten afkappen voor het contextbudget
Zelfs na het filteren heb je misschien vijf resultaten van hoge kwaliteit met elk 600 tekens — dat zijn in totaal 3.000 tekens. Bepaal hoeveel resultaten binnen het contextbudget van je LLM passen en kap de resultaten dienovereenkomstig af.
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')Resultaten opmaken voor de LLM-prompt
Maak de resultaten na het rangschikken, ontdubbelen en afkappen op als een genummerde lijst in de LLM-prompt. Dankzij genummerde bronnen kan het model er gemakkelijk naar verwijzen in zijn antwoord.
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}'
)Zoekresultaten cachen
Dezelfde zoekopdracht kan in meerdere sessies of agentlussen worden herhaald. Sla zoekresultaten op in de cache met een korte TTL (bijvoorbeeld één uur) om API-kosten te verlagen en de reactietijd bij herhaalde zoekopdrachten te verbeteren.
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 resultsVolledige verwerkingsketen voor filteren en rangschikken
Verbind alle stappen in één functie voor de verwerkingsketen: ophalen → resultaten van lage kwaliteit filteren → ontdubbelen → rangschikken → domeinversterking toepassen → afkappen voor de context → opmaken voor de 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)Kennistoets
Wat is het belangrijkste voordeel van hybride rangschikking (BM25 + embeddings) ten opzichte van alleen BM25 gebruiken?
Samenvatting: zoekresultaten rangschikken en filteren
Onbewerkte zoekresultaten moeten worden verwerkt voordat ze de LLM bereiken. De verwerkingsketen: resultaten van lage kwaliteit filteren (te kort, achter een betaalmuur) → ontdubbelen op basis van URL en inhoud → rangschikken op basis van BM25 en/of embedding-similariteit → kwaliteitsversterking voor domeinen toepassen → afkappen tot het contextbudget → opmaken als genummerde bronnen.
Cache zoekresultaten om API-kosten bij herhaalde zoekopdrachten te verlagen. Dankzij genummerde bronverwijzingen in de prompt kan de LLM beweringen aan specifieke bronnen toeschrijven.
Leer AI-agenten met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 60
- Lessen
- 239
Veelgestelde vragen
Is de les “Zoekresultaten rangschikken en filteren” gratis?
Ja — de volledige tekst van “Zoekresultaten rangschikken en filteren” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus AI-agenten wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus AI-agenten bevat in totaal 4 lessen.
Wat leer ik in “Zoekresultaten rangschikken en filteren”?
Relevantie scoren, duplicaten verwijderen en de beste resultaten voor de context selecteren. Je oefent met AI-agenten door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met AI-agenten te beginnen?
Ervaring vooraf is niet nodig. AI-agenten op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 2 van 4.
Hoe lang duurt de les “Zoekresultaten rangschikken en filteren”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over AI-agenten?
Ja. Elke les over AI-agenten bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Tavily en SerpAPI voor zoeken door agents
- Zoekresultaten rangschikken en filteren
- Patroon voor de deep-researchlus
- Webzoekopdrachten combineren met RAG