0Pricing
AI Agents · Pelajaran

Mengurutkan dan Menyaring Hasil Pencarian

Nilai relevansi, deduplikasi, dan pemilihan hasil terbaik untuk konteks.

Mengurutkan dan Menyaring Hasil Pencarian adalah pelajaran AI Agents gratis di CoddyKit. Ini adalah pelajaran 2 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar AI Agents, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus AI Agents mencakup 4 pelajaran total.

Mengapa Pemeringkatan dan Penyaringan Penting

API pencarian mengembalikan 5–10 hasil, tetapi tidak semuanya sama-sama relevan, dapat diandalkan, atau berguna untuk tugas agen. Memasukkan hasil mentah langsung ke LLM memboroskan token konteks dan dapat menimbulkan gangguan atau informasi yang salah.

Pemeringkatan dan penyaringan meningkatkan rasio sinyal terhadap derau sebelum hasil mencapai LLM.

Penilaian Relevansi dengan BM25

BM25 (Best Match 25) adalah algoritme klasik untuk memeringkat teks yang menilai dokumen berdasarkan kesamaan kata kunci dengan kueri. Algoritme ini bekerja baik untuk pencocokan leksikal — ketika kueri dan dokumen menggunakan kata yang sama.

Instal dengan 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]

Penilaian Relevansi dengan Vektor Semantik

BM25 hanya mencocokkan kata yang sama persis. Kemiripan representasi vektor menangkap makna semantik — sehingga 'pengembangan web dengan Python' dan 'membangun situs web dengan Django' memiliki kemiripan tinggi meskipun menggunakan kata yang berbeda.

Gunakan kemiripan kosinus antara representasi vektor kueri dan representasi vektor hasil.

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)

Pemeringkatan Hibrida: BM25 + Vektor Semantik

Penilaian BM25 dan representasi vektor menangkap aspek relevansi yang berbeda. Pemeringkatan hibrida menggabungkan kedua skor dengan rata-rata berbobot, sehingga memperoleh hasil terbaik dari pencocokan leksikal dan semantik.

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)

Deduplikasi berdasarkan URL

Hasil pencarian sering kali mengandung duplikat yang hampir sama: artikel yang sama dari beberapa sumber sindikasi, atau halaman yang sama dengan parameter URL berbeda. Deduplikasi menghapus hasil-hasil ini sebelum meneruskannya ke 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'])

Penilaian Kualitas Domain

Hasil dari docs.python.org lebih tepercaya daripada hasil dari blog acak. Tetapkan pengali kualitas untuk tingkatan domain dan sertakan nilainya dalam pemeringkatan akhir.

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

Memfilter Hasil Berkualitas Rendah

Beberapa hasil secara struktural berkualitas rendah terlepas dari domainnya: terlalu singkat untuk berguna, sebagian besar berisi teks navigasi, atau berasal dari halaman yang dibatasi login. Saring hasil-hasil ini sebelum melakukan pemeringkatan.

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

Pemotongan Hasil untuk Anggaran Konteks

Bahkan setelah pemfilteran, Anda mungkin memiliki 5 hasil berkualitas tinggi dengan masing-masing 600 karakter — totalnya 3.000 karakter. Tentukan berapa banyak hasil yang sesuai dengan anggaran konteks LLM Anda, lalu potong hasil tersebut sesuai kebutuhan.

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

Memformat Hasil untuk Perintah LLM

Setelah melakukan pemeringkatan, deduplikasi, dan pemotongan, format hasil sebagai daftar bernomor dalam perintah LLM. Sumber bernomor memudahkan model mengutipnya dalam jawabannya.

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

Menyimpan Hasil Pencarian dalam Cache

Kueri yang sama mungkin diulang dalam berbagai sesi atau perulangan agen. Simpan hasil pencarian dalam cache dengan TTL singkat (misalnya, 1 jam) untuk mengurangi biaya API dan mempercepat respons pada kueri yang diulang.

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

Alur Pemrosesan Lengkap untuk Pemfilteran dan Pemeringkatan

Rangkaikan semua langkah ke dalam satu fungsi alur pemrosesan: ambil → saring hasil berkualitas rendah → hapus duplikat → beri peringkat → terapkan peningkatan domain → potong untuk konteks → format untuk perintah.

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)

Pemeriksaan Pengetahuan

Apa keunggulan utama pemeringkatan hibrida (BM25 + representasi vektor) dibandingkan hanya menggunakan BM25?

Rangkuman: Pemeringkatan dan Pemfilteran Hasil Pencarian

Hasil pencarian mentah perlu diproses sebelum diteruskan ke LLM. Alurnya: saring hasil berkualitas rendah (terlalu singkat, berbayar) → hapus duplikat berdasarkan URL dan isi → beri peringkat berdasarkan BM25 dan/atau kemiripan representasi vektor → terapkan peningkatan kualitas domain → potong sesuai anggaran konteks → format sebagai sumber bernomor.

Simpan hasil pencarian dalam cache untuk mengurangi biaya API pada kueri yang diulang. Sitasi bernomor dalam perintah memungkinkan LLM mengaitkan klaim dengan sumber tertentu.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Mengurutkan dan Menyaring Hasil Pencarian” gratis?

Ya — teks lengkap “Mengurutkan dan Menyaring Hasil Pencarian” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus AI Agents, upgrade ke CoddyKit PRO. Kursus AI Agents mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Mengurutkan dan Menyaring Hasil Pencarian”?

Nilai relevansi, deduplikasi, dan pemilihan hasil terbaik untuk konteks. Kamu berlatih AI Agents dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai AI Agents?

Tidak diperlukan pengalaman sebelumnya. AI Agents di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 2 dari 4.

Berapa lama pelajaran “Mengurutkan dan Menyaring Hasil Pencarian” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran AI Agents ini?

Ya. Setiap pelajaran AI Agents menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Tavily dan SerpAPI untuk Pencarian Agen
  2. Mengurutkan dan Menyaring Hasil Pencarian
  3. Pola Putaran Riset Mendalam
  4. Menggabungkan Pencarian Web dengan RAG
← Kembali ke AI Agents