0Pricing
AI Agents · บทเรียน

การจัดอันดับและกรองผลการค้นหา

ให้คะแนนความเกี่ยวข้อง ลบข้อมูลซ้ำ และเลือกผลลัพธ์ที่ดีที่สุดสำหรับบริบท

การจัดอันดับและกรองผลการค้นหา เป็นบทเรียน AI Agents ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Agents และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Agents มีบทเรียนทั้งหมด 4 บทเรียน

เหตุใดการจัดอันดับและการกรองจึงสำคัญ

API การค้นหาส่งคืนผลลัพธ์ 5–10 รายการ แต่ผลลัพธ์เหล่านั้นไม่ได้เกี่ยวข้อง น่าเชื่อถือ หรือมีประโยชน์ต่องานของตัวแทนเท่ากันทั้งหมด การป้อนผลลัพธ์ดิบให้ LLM โดยตรงทำให้โทเค็นบริบทสูญเปล่า และอาจนำเสียงรบกวนหรือข้อมูลผิดมา

การจัดอันดับและการกรองช่วยปรับปรุง อัตราส่วนสัญญาณต่อสัญญาณรบกวน ก่อนที่ผลลัพธ์จะไปถึง LLM

การให้คะแนนความเกี่ยวข้องด้วย BM25

BM25 (การจับคู่ที่ดีที่สุด 25) เป็นอัลกอริทึมจัดอันดับข้อความแบบดั้งเดิมที่ให้คะแนนเอกสารจากการทับซ้อนของคำสำคัญกับคำค้น วิธีนี้ทำงานได้ดีสำหรับการจับคู่ตามคำ กล่าวคือเมื่อคำค้นและเอกสารมีคำเดียวกัน

ติดตั้งด้วย 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]

การให้คะแนนความเกี่ยวข้องด้วยเวกเตอร์ฝังตัว

BM25 จับคู่ได้เฉพาะคำที่ตรงกันทุกประการ ส่วน ความคล้ายคลึงของเวกเตอร์ฝังตัว จะสะท้อนความหมายของเนื้อหา ดังนั้น “การพัฒนาเว็บด้วย Python” และ “การสร้างเว็บไซต์ด้วย Django” จึงอาจได้คะแนนความคล้ายคลึงสูง แม้จะใช้คำต่างกัน

ใช้ความคล้ายคลึงแบบโคไซน์ระหว่างเวกเตอร์ฝังตัวของคำค้นกับเวกเตอร์ฝังตัวของผลลัพธ์

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)

การจัดอันดับแบบผสม: BM25 + เวกเตอร์ฝังตัว

การให้คะแนนด้วย BM25 และเวกเตอร์ฝังตัวสะท้อนความเกี่ยวข้องคนละด้าน การจัดอันดับแบบผสม จะรวมคะแนนทั้งสองด้วยค่าเฉลี่ยถ่วงน้ำหนัก ทำให้ได้ประโยชน์สูงสุดจากทั้งการจับคู่ตามคำและการจับคู่ตามความหมาย

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)

การลบรายการซ้ำตาม URL

ผลการค้นหามักมีรายการที่เกือบซ้ำกัน เช่น บทความเดียวกันจากแหล่งเผยแพร่ต่อหลายแห่ง หรือหน้าเดียวกันที่มีพารามิเตอร์ URL ต่างกัน ควรลบรายการซ้ำเหล่านี้ก่อนส่งผลลัพธ์ให้ 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'])

การให้คะแนนคุณภาพของโดเมน

ผลลัพธ์จาก docs.python.org น่าเชื่อถือกว่าผลลัพธ์จากบล็อกทั่วไป ควรกำหนดตัวคูณคุณภาพให้กับระดับของโดเมน แล้วนำตัวคูณเหล่านี้ไปคำนวณในการจัดอันดับขั้นสุดท้าย

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

การกรองผลลัพธ์คุณภาพต่ำ

ผลลัพธ์บางรายการมีคุณภาพต่ำในเชิงโครงสร้าง ไม่ว่าโดเมนจะเป็นอะไร เช่น สั้นเกินกว่าจะมีประโยชน์ มีข้อความเมนูนำทางเป็นส่วนใหญ่ หรือมาจากหน้าที่ต้องเข้าสู่ระบบ ควรกรองผลลัพธ์เหล่านี้ออกก่อนจัดอันดับ

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

การตัดทอนผลลัพธ์ตามงบประมาณบริบท

แม้กรองแล้ว คุณอาจมีผลลัพธ์คุณภาพสูง 5 รายการ รายการละ 600 อักขระ รวมเป็น 3,000 อักขระ ควรกำหนดว่ามีผลลัพธ์กี่รายการที่พอดีกับงบประมาณบริบทของ LLM แล้วตัดทอนให้เหมาะสม

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

การจัดรูปแบบผลลัพธ์สำหรับคำสั่งของ LLM

หลังจากจัดอันดับ ลบรายการซ้ำ และตัดทอนแล้ว ให้จัดรูปแบบผลลัพธ์เป็นรายการลำดับเลขในคำสั่งของ LLM แหล่งข้อมูลที่มีหมายเลขช่วยให้โมเดลอ้างอิงแหล่งที่มาในคำตอบได้ง่าย

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

การแคชผลการค้นหา

คำค้นเดียวกันอาจถูกใช้ซ้ำในหลายเซสชันหรือหลายรอบการทำงานของเอเจนต์ ควรแคชผลการค้นหาโดยกำหนด TTL ให้สั้น เช่น 1 ชั่วโมง เพื่อลดค่าใช้จ่ายของ API และเพิ่มความเร็วในการตอบคำค้นที่ซ้ำกัน

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

กระบวนการกรองและจัดอันดับแบบครบถ้วน

เชื่อมทุกขั้นตอนเข้าด้วยกันเป็นฟังก์ชันกระบวนการเดียว: ดึงข้อมูล → กรองคุณภาพต่ำ → ลบรายการซ้ำ → จัดอันดับ → เพิ่มคะแนนตามโดเมน → ตัดทอนสำหรับบริบท → จัดรูปแบบสำหรับคำสั่ง

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)

ตรวจสอบความรู้

ข้อได้เปรียบหลักของการจัดอันดับแบบผสม (BM25 + เวกเตอร์ฝังตัว) เมื่อเทียบกับการใช้ BM25 เพียงอย่างเดียวคืออะไร

ทบทวน: การจัดอันดับและกรองผลการค้นหา

ผลการค้นหาดิบต้องผ่านการประมวลผลก่อนส่งต่อไปยัง LLM กระบวนการคือ กรองผลลัพธ์คุณภาพต่ำ (สั้นเกินไปหรือมีการจำกัดการเข้าถึง) → ลบรายการซ้ำตาม URL และเนื้อหา → จัดอันดับด้วย BM25 และ/หรือความคล้ายคลึงของเวกเตอร์ฝังตัว → เพิ่มคะแนนคุณภาพของโดเมน → ตัดทอนให้พอดีกับงบประมาณบริบท → จัดรูปแบบเป็นแหล่งข้อมูลแบบมีหมายเลข

แคชผลการค้นหาเพื่อลดค่าใช้จ่ายของ API สำหรับคำค้นที่ใช้ซ้ำ การอ้างอิงแบบมีหมายเลขในคำสั่งช่วยให้ LLM ระบุได้ว่าข้อกล่าวอ้างมาจากแหล่งข้อมูลใด

คำถามที่พบบ่อย

บทเรียน “การจัดอันดับและกรองผลการค้นหา” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การจัดอันดับและกรองผลการค้นหา” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Agents ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Agents มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การจัดอันดับและกรองผลการค้นหา”

ให้คะแนนความเกี่ยวข้อง ลบข้อมูลซ้ำ และเลือกผลลัพธ์ที่ดีที่สุดสำหรับบริบท คุณปฏิบัติ AI Agents ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Agents หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Agents บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “การจัดอันดับและกรองผลการค้นหา” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน AI Agents นี้ได้ไหม

ได้ บทเรียน AI Agents ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. Tavily และ SerpAPI สำหรับการค้นหาของตัวแทน
  2. การจัดอันดับและกรองผลการค้นหา
  3. รูปแบบลูปการวิจัยเชิงลึก
  4. การผสานการค้นหาเว็บกับ RAG
← กลับไปที่ AI Agents