المستوى المطلوب: متوسط — تحتاج فهم أساسي لـ RAG والـ embeddings والـ vector search.
Reranking في RAG: ليه نتائج بحثك متلخبطة حتى مع Embeddings ممتازة
لو بنيت RAG pipeline على pgvector أو Pinecone وفجأة لقيت إن أهم وثيقة بتيجي في المركز التاسع بدل الأول، المشكلة مش embeddings بتاعتك. المشكلة إنك واقف عند خطوة retrieval وما ضفتش طبقة reranking. هتتعلم هنا ليه bi-encoder بيلاقي وثائق قريبة دلاليًا لكن مش بالظبط الإجابة، وإزاي cross-encoder بيرتّب الـ top-50 صح في أقل من 100ms.
المشكلة باختصار
عندك مكتبة بـ 500 ألف وثيقة. المستخدم يسأل: "إزاي أعمل rate limiting على Stripe webhooks؟". الـ vector search بيرجّع 10 نتايج، الإجابة الصح فيهم لكن في رقم 7. الـ LLM بياخد أول 5 (limit الـ context) فبيكتب إجابة عامة من غير الجواب الفعلي. المستخدم بيفتكر النموذج غبي، الحقيقة إن الترتيب غلط مش المحتوى.
مثال للمبتدئ قبل التعريف العلمي
تخيل إنك دخلت مكتبة عامة تدور على كتاب عن "تربية النحل في المدن". أمين المكتبة بصّ في الفهرس وجابلك 50 كتاب فيهم كلمة "نحل" أو "مدينة" — 95% منهم مش هما اللي عايزه. بعد كده باحث متخصص في علم الحشرات قعد يقلّب الـ 50 كتاب واحد واحد، وأعطاك 5 بترتيب الأهمية الفعلية. أمين المكتبة هو الـ bi-encoder، والباحث المتخصص هو الـ cross-encoder reranker. الأول سريع لكن سطحي، التاني بطيء لكن بيقرا فعلاً.
التعريف العلمي: bi-encoder مقابل cross-encoder
الـ bi-encoder (زي text-embedding-3-small من OpenAI أو voyage-2 من Voyage AI) بيحوّل الـ query والـ document لـ vectors بشكل منفصل، وبيقيس التشابه بـ cosine similarity. ميزته إنه سريع جدًا، وتقدر تحسب embeddings الوثائق مرة واحدة وتخزّنها في pgvector. عيبه إن مفيش تفاعل مباشر بين الـ query والـ document أثناء الحساب، فبيفوّت العلاقات الدقيقة على مستوى الكلمة.
الـ cross-encoder (زي BAAI/bge-reranker-v2-m3 أو Cohere Rerank-3) بياخد (query, document) معاً كمدخل واحد ويرجّع score بين 0 و1 يدل على الصلة الفعلية. أبطأ بكتير لأن مفيش تخزين مسبق ممكن، لكن دقته أعلى لأنه بيشوف الـ token-level interaction بين السؤال والوثيقة. الحل العملي اللي بيشتغل في production: استخدم bi-encoder لجلب top-100 سريع من قاعدة البيانات، بعدين cross-encoder يرتّب أحسن 5 منهم بدقة عالية.
الكود الفعلي: pgvector + sentence-transformers cross-encoder
from sentence_transformers import CrossEncoder
import psycopg
from openai import OpenAI
client = OpenAI()
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)
def search(query: str, top_k: int = 5):
# 1) bi-encoder retrieval (سريع: ~30ms)
q_embed = client.embeddings.create(
model="text-embedding-3-small", input=query
).data[0].embedding
with psycopg.connect("postgresql://...") as conn:
rows = conn.execute(
"SELECT id, content FROM docs "
"ORDER BY embedding <=> %s::vector LIMIT 50",
(q_embed,)
).fetchall()
# 2) cross-encoder reranking (أبطأ لكن أدق: ~80ms)
pairs = [(query, content) for _id, content in rows]
scores = reranker.predict(pairs)
ranked = sorted(zip(rows, scores), key=lambda x: x[1], reverse=True)
return [(doc_id, content) for (doc_id, content), _s in ranked[:top_k]]