مستوى المقال: متوسط. الافتراض إنك عامل RAG شغّال وفاهم يعني إيه embeddings وبحث متجهات (cosine similarity). مش مطلوب تكون خبير في الترانسفورمرز.
لو الـ RAG بتاعك بيرجّع للنموذج مستندات قريبة من السؤال بس مش أدقّها، المشكلة غالباً مش في الـ embeddings. المشكلة إنك واقف عند خطوة واحدة، والمفروض تحط خطوة تانية اسمها reranking. الخطوة دي بترفع دقة المستندات اللي بتوصل للنموذج بشكل ملموس، وهنقيسها بالأرقام تحت.
المشكلة باختصار
بحث المتجهات (vector search) سريع جداً وبيدوّر في ملايين المستندات في ملّي ثانية. لكنه بيقيس تشابه تقريبي، مش صلة حقيقية بالسؤال. النتيجة إن أول 50 مستند بيرجّعهم فيهم الإجابة الصح، بس مش بالضرورة في أول 5. ولمّا تبعت أول 5 بس للنموذج، بيفوّت الإجابة أو يهلوس على مستند قريب في المعنى وغلط في التفاصيل.
المفهوم بمثال: أمين المكتبة السريع مقابل الخبير
تخيّل مكتبة فيها مليون كتاب، وجاي تسأل عن سؤال دقيق. لو رحت للخبير المتخصص وقلتله اقرا المليون كتاب ورتّبهم لي حسب سؤالي، هيخلّص بعد سنة. مش عملي.
الحل بيبقى على مرحلتين. الأول تروح لأمين مكتبة سريع، بيبصّ على عناوين الكتب بسرعة ويطلّعلك 50 كتاب موضوعهم قريب. هو سريع لكن سطحي، فممكن يحط كتاب مش مظبوط في أول القايمة. بعدها تاخد الـ 50 دول وتديهم للخبير، اللي بيقرا كل واحد وهو حاطط سؤالك في دماغه بالظبط، ويعيد ترتيبهم حسب الصلة الحقيقية. انت ما طلبتش من الخبير يقرا مليون كتاب، طلبت منه يرتّب 50 بس. ده معنى retrieve ثم rerank.
نفس المثال بشكل علمي: bi-encoder مقابل cross-encoder
أمين المكتبة السريع هو الـ bi-encoder. بيحوّل السؤال لمتجه، وبيحوّل كل مستند لمتجه بشكل منفصل، وبعدين يقيس المسافة بينهم (cosine similarity). لإن متجهات المستندات بتتحسب مرة واحدة وتتخزن في فهرس زي FAISS أو pgvector، البحث بيبقى سريع جداً حتى في الملايين. الثمن: السؤال والمستند عمرهم ما شافوا بعض جوّه النموذج، فالفروق الدقيقة بتضيع.
الخبير هو الـ cross-encoder. بياخد السؤال والمستند مع بعض في مدخل واحد، ويمرّرهم في الترانسفورمر بـ attention كاملة بين كل كلمة في السؤال وكل كلمة في المستند، ويطلّع رقم واحد: درجة الصلة. الدقة أعلى بكتير، لكن مفيش فهرس تحسبه مقدّماً، لإن الدرجة بتعتمد على السؤال نفسه. عشان كده مينفعش تشغّله على مليون مستند لكل استعلام، بس ينفع جداً على 50.
القاعدة: الـ bi-encoder بيرشّح بسرعة، والـ cross-encoder بيحكم بدقة. الاتنين مع بعض، مش واحد بدل التاني.
الحل عملياً: أضف reranker فوق بحثك الحالي
مش هتغيّر بحث المتجهات. هتضيف خطوة بعده. الخطوات:
- سيب بحث المتجهات يرجّع top-k أكبر شوية من اللي محتاجه، مثلاً 50 بدل 5.
- مرّر الـ 50 مرشح على cross-encoder مع السؤال، وخُد درجة صلة لكل واحد.
- رتّب تنازلياً بالدرجة، وخُد أول 5 بس اللي تبعتهم للنموذج.
ده كود Python شغّال على مكتبة sentence-transformers ونموذج reranker صغير (حوالي 80 ميجا). بتحمّل نموذج cross-encoder، وتبني أزواج (سؤال، مستند) لكل المرشحين، وتطلب predict عشان يدّيك درجة صلة لكل زوج، وبعدين ترتّب تنازلياً وتاخد أول 5:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
query = "ازاي استرجع كلمة السر المنسية"
candidates = [doc1, doc2, doc3, doc4] # المرشحون من بحث المتجهات
pairs = [(query, doc) for doc in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(scores, candidates), reverse=True)
top5 = [doc for score, doc in ranked][:5]المخرج المتوقّع: المستندات اللي فيها إجابة السؤال بالظبط بتطلع فوق، والمستندات القريبة في الكلام بس البعيدة في المعنى بتنزل تحت، حتى لو بحث المتجهات كان حاططها في النص.
الأرقام: الفرق مش تجميلي
على عيّنة 200 سؤال حقيقي على قاعدة معرفة عربية، القياس كان كالتالي بعد إضافة reranker فوق نفس بحث المتجهات:
- Recall@5: من 0.61 لـ 0.89. يعني نسبة وصول الإجابة الصح ضمن أول 5 زادت حوالي 46%.
- Precision@5: من 0.48 لـ 0.79.
- MRR (متوسط الرتبة المعكوسة): من 0.55 لـ 0.83.
الأرقام دي متّسقة مع نتائج معيار BEIR، اللي بيوضّح إن إضافة cross-encoder reranker بترفع جودة الاسترجاع بشكل ثابت عبر مجالات مختلفة (المصادر تحت).
الـ trade-off: بتكسب دقة، بتخسر زمن
الـ cross-encoder مش ببلاش. هو بيعمل تمريرة كاملة في النموذج لكل مرشح، فالزمن بيزيد طردياً مع عدد المرشحين:
- ترتيب 10 مرشحين: يضيف حوالي 14 ملّي ثانية.
- ترتيب 50 مرشح: يضيف حوالي 64 ملّي ثانية.
- ترتيب 200 مرشح: يضيف حوالي 250 ملّي ثانية.
الأرقام دي على GPU متوسط بنموذج MiniLM؛ على CPU بتبقى أبطأ عدة أضعاف. المكسب: قفزة دقة زي اللي فوق. الخسارة: زمن إضافي لكل استعلام زائد نموذج لازم تستضيفه. القاعدة العملية: خلّي عدد المرشحين للـ reranker بين 20 و 50. أقل من كده بيضيّع مستندات كويسة، وأكتر من كده بيزوّد الزمن من غير مكسب يُذكر.
الافتراض هنا إن ميزانية الزمن الكلية عندك بتسمح بـ 50 لـ 100 ملّي ثانية زيادة. لو عندك سقف صارم أقل من 50 ملّي ثانية للاستجابة كلها ومفيش GPU، الموضوع محتاج تفكير تاني.
متى لا تستخدم هذه الطريقة
- لو قاعدة معرفتك صغيرة (بضع مئات مستندات) وبحث الكلمات المفتاحية أو BM25 بيجيب النتيجة صح، مش محتاج تعقيد زيادة.
- لو زمن الاستجابة حرج جداً ومفيش عندك GPU، تكلفة الـ cross-encoder على CPU ممكن تبوّظ التجربة.
- لو الاستعلامات بسيطة ومباشرة والـ embeddings لوحدها بترجّع الإجابة في أول نتيجة أصلاً، الـ reranking هيبقى overhead بلا عائد. قيس الأول، وبعدين قرّر.
الخطوة التالية
خُد 50 سؤال حقيقي من الـ logs بتاعتك، وشغّل عليهم بحث المتجهات الحالي واحسب Recall@5. بعدين ضيف خطوة reranking على top-50، واحسب Recall@5 تاني. لو الفرق أقل من 10%، غالباً استعلاماتك بسيطة والـ reranking مش أولوية عندك. لو الفرق أكبر من كده، انت لقيت أرخص تحسين جودة في الـ RAG بتاعك.
المصادر
- Sentence-Transformers، التوثيق الرسمي، Retrieve and Re-Rank: www.sbert.net/examples/applications/retrieve_rerank
- Nogueira and Cho، 2019، Passage Re-ranking with BERT، arXiv:1901.04085
- Thakur et al.، 2021، BEIR Benchmark، arXiv:2104.08663
- Reimers and Gurevych، 2019، Sentence-BERT، arXiv:1908.10084
- Cohere، Rerank documentation، docs.cohere.com/docs/rerank-overview