0Pricing
AI Agents · درس

ترتيب نتائج البحث وتصفيتها

تقييم الصلة، وإزالة التكرارات، واختيار أفضل النتائج للسياق

ترتيب نتائج البحث وتصفيتها درس مجاني في AI Agents على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 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)، مثل ساعة واحدة، لتقليل تكاليف 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 إسناد الادعاءات إلى مصادر محددة.

الأسئلة الشائعة

هل درس «ترتيب نتائج البحث وتصفيتها» مجاني؟

نعم — نص درس «ترتيب نتائج البحث وتصفيتها» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Agents، انتقل إلى CoddyKit PRO. تتضمن دورة AI Agents 4 دروس في المجموع.

ماذا ستتعلم في «ترتيب نتائج البحث وتصفيتها»؟

تقييم الصلة، وإزالة التكرارات، واختيار أفضل النتائج للسياق تتمرن على AI Agents مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Agents؟

لا تُشترط خبرة سابقة. AI Agents على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «ترتيب نتائج البحث وتصفيتها»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Agents هذا؟

نعم. كل درس في AI Agents يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. Tavily وSerpAPI للبحث باستخدام الوكلاء
  2. ترتيب نتائج البحث وتصفيتها
  3. نمط حلقة البحث المتعمق
  4. دمج البحث على الويب مع RAG
← العودة إلى AI Agents