الوحدة 6 — إعادة ترتيب المقاطع المسترجَعة
الوحدة السابقة أعطتنا استرجاعًا هجينًا بكفاءة معقولة. لكنّ أفضل عشرين مقطعًا حسب هذا الاسترجاع تحوي عادةً خمسة مفيدة و خمسة عشر ضجيجًا. إعادة الترتيب مرحلة صغيرة الكلفة قياسًا بالباقي، لكنّها تُحسِّن الجودة تحسينًا جوهريًّا، وتُصفّي القمّة قبل تمرير المقاطع إلى النموذج المولّد.
الفرق بين المشفِّر الثنائي والمشفِّر المتقاطع
في الوحدة 4 استعملنا نموذجًا يُنتج شعاعًا للسؤال منفصلًا عن شعاع ا لمقطع، ثم قسنا القرب بجيب التمام. هذا مشفِّر ثنائي (bi-encoder) أو قطبان (two-tower). ميزته: يمكن حساب شعاع كلّ مقطع مرّة وتخزينه، والبحث لاحقًا يكلّف تضمين السؤال فقط.
المشفِّر المتقاطع (cross-encoder) يعمل بمنطق مختلف: يأخذ السؤال والمقطع معًا في إدخال واحد ويُنتج درجة صلة مباشرة. هذا يمنحه دقّة أعلى بكثير لأنّه يلاحظ التفاعل بين السؤال والمقطع كلمة بكلمة. ثمنه: لا يمكن حساب شيء مسبقًا، فيجب تشغيله على كلّ زوج (سؤال، مقطع) وقت الاستفسار.
| المعيار | Bi-encoder (تضمين) | Cross-encoder (إعادة ترتيب) |
|---|---|---|
| كلفة الفهرسة | مرّة واحدة لكلّ مقطع | لا فهرسة ممكنة |
| كلفة الاستفسار | O(1) تضمين للسؤال | O(k) استدعاء نموذج |
| الدقّة | جيّدة | ممتازة |
| الاستعمال | استرجاع أوّليّ سريع من مئات الآلاف | ترتيب دقيق لعشرات المرشّحين |
المفتاح هنا: لا يعوض المشفِّرُ المتقاطع الأوّلَ ولا يُلغيه. يعمل الاثنان معًا في تسلسل: bi-encoder + BM25 يعطيان 20 – 50 مقطعًا (الاستدعاء)، ثم cross-encoder يعيد ترتيبها ويحتفظ بأفضل 3 – 5 (الدقّة).
نماذج إعادة ترتيب عملية
المرشّحون الأكثر استعمالًا اليوم:
BAAI/bge-reranker-v2-m3: مفتوح، متعدّد اللغات (يشمل العربية جيّدًا)، خفيف نسبيًّا (نصف مليار وسيط).mixedbread-ai/mxbai-rerank-large-v1: مفتوح، جودة عالية.cohere/rerank-3(مغلق): جودة ممتازة، متاح كخدمة.
للفيل الأحمر، bge-reranker-v2-m3 نقطة انطلاق سليمة لأنّه يعمل محلّيًا ويحفظ السرّية.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
# انطلاقًا من عشرين مرشّحًا رجعوا من الاسترجاع الهجين
candidates = fused[:20]
pairs = [(question, chunks[c.id].text) for c in candidates]
scores = reranker.predict(pairs)
# إعادة ترتيب حسب الدرجة، والاحتفاظ بأفضل 5
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
top_k = [c for c, _ in ranked[:5]]