Ranking i filtrowanie wyników wyszukiwania
Ocenianie trafności, usuwanie duplikatów i wybieranie najlepszych wyników do kontekstu.
Ranking i filtrowanie wyników wyszukiwania to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Agents, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Agents zawiera 4 lekcji w sumie.
Dlaczego ranking i filtrowanie mają znaczenie
Interfejs API wyszukiwania zwraca od 5 do 10 wyników, ale nie wszystkie są równie trafne, wiarygodne lub przydatne dla zadania agenta. Przekazywanie surowych wyników bezpośrednio do LLM marnuje tokeny kontekstu i może wprowadzać szum lub dezinformację.
Ranking i filtrowanie poprawiają stosunek sygnału do szumu, zanim wyniki trafią do LLM.
Ocena trafności za pomocą BM25
BM25 (Best Match 25) to klasyczny algorytm rankingowania tekstu, który ocenia dokumenty na podstawie pokrycia słów kluczowych z zapytaniem. Sprawdza się dobrze w dopasowaniu leksykalnym — gdy zapytanie i dokument zawierają te same słowa.
Należy zainstalować za pomocą 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]Ocena trafności za pomocą embeddingów
BM25 dopasowuje tylko dokładne słowa. Podobieństwo embeddingów uwzględnia znaczenie semantyczne — dlatego „Python web development” i „building websites with Django” mogą uzyskać wysokie podobieństwo mimo użycia różnych słów.
Należy użyć podobieństwa cosinusowego między embeddingiem zapytania a embeddingami wyników.
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 hybrydowy: BM25 + embeddingi
BM25 i ocena na podstawie embeddingów uwzględniają różne aspekty trafności. Ranking hybrydowy łączy obie oceny za pomocą średniej ważonej, wykorzystując zalety zarówno dopasowania leksykalnego, jak i semantycznego.
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)Deduplikacja według adresu URL
Wyniki wyszukiwania często zawierają niemal identyczne elementy: ten sam artykuł z wielu serwisów przedrukowujących treści albo tę samą stronę z różnymi parametrami URL. Deduplikacja usuwa je przed przekazaniem wyników do 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'])
Ocena jakości domeny
Wynik z docs.python.org jest bardziej wiarygodny niż wynik z przypadkowego bloga. Należy przypisać mnożniki jakości poszczególnym poziomom domen i uwzględnić je w końcowym rankingu.
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}")
Filtrowanie wyników niskiej jakości
Niektóre wyniki mają niską jakość ze względów strukturalnych, niezależnie od domeny: są zbyt krótkie, aby były użyteczne, zawierają głównie tekst nawigacyjny albo pochodzą ze stron wymagających logowania. Należy odfiltrować je przed rankingowaniem.
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')
Skracanie wyników w ramach budżetu kontekstu
Nawet po odfiltrowaniu może pozostać 5 wysokiej jakości wyników po 600 znaków każdy — łącznie daje to 3000 znaków. Należy ustalić, ile wyników mieści się w budżecie kontekstu LLM-a, i odpowiednio je skrócić.
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')Formatowanie wyników w prompcie LLM-a
Po ustaleniu rankingu, usunięciu duplikatów i skróceniu wyników należy sformatować je jako numerowaną listę w prompcie LLM-a. Numerowane źródła ułatwiają modelowi cytowanie ich w odpowiedzi.
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}'
)Buforowanie wyników wyszukiwania
To samo zapytanie może być powtarzane w różnych sesjach lub pętlach agenta. Należy buforować wyniki wyszukiwania z krótkim TTL-em (np. 1 godzinę), aby ograniczyć koszty API i przyspieszyć odpowiedzi na powtarzające się zapytania.
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 resultsPełny potok filtrowania i rankingowania
Wszystkie kroki należy połączyć w jednej funkcji potoku: pobieranie → filtrowanie wyników niskiej jakości → deduplikacja → ranking → zastosowanie wzmocnienia domeny → skrócenie w ramach kontekstu → formatowanie na potrzeby promptu.
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)Sprawdzenie wiedzy
Jaka jest główna zaleta rankingu hybrydowego (BM25 + embeddingi) w porównaniu z użyciem samego BM25?
Podsumowanie: ranking i filtrowanie wyników wyszukiwania
Surowe wyniki wyszukiwania wymagają przetworzenia, zanim trafią do LLM-a. Potok obejmuje: filtrowanie wyników niskiej jakości (zbyt krótkich lub objętych płatnym dostępem) → deduplikację według adresu URL i treści → ranking według BM25 i/lub podobieństwa embeddingów → zastosowanie wzmocnień jakości domen → skrócenie do budżetu kontekstu → sformatowanie jako numerowanych źródeł.
Buforowanie wyników wyszukiwania ogranicza koszty API w przypadku powtarzających się zapytań. Numerowane cytowania w prompcie umożliwiają LLM-owi przypisywanie twierdzeń do konkretnych źródeł.
Ucz się AI Agents dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 60
- Lekcje
- 239
Często zadawane pytania
Czy lekcja „Ranking i filtrowanie wyników wyszukiwania” jest bezpłatna?
Tak — pełny tekst „Ranking i filtrowanie wyników wyszukiwania” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Agents, przejdź na CoddyKit PRO. Kurs AI Agents zawiera 4 lekcji w sumie.
Co nauczysz się w „Ranking i filtrowanie wyników wyszukiwania”?
Ocenianie trafności, usuwanie duplikatów i wybieranie najlepszych wyników do kontekstu. Ćwiczysz AI Agents z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć AI Agents?
Nie wymagamy żadnego doświadczenia. AI Agents w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.
Ile czasu zajmuje lekcja „Ranking i filtrowanie wyników wyszukiwania”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji AI Agents?
Tak. Każda lekcja AI Agents zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Tavily i SerpAPI do wyszukiwania przez agenta
- Ranking i filtrowanie wyników wyszukiwania
- Wzorzec pętli pogłębionego researchu
- Łączenie wyszukiwania internetowego z RAG