البحث الهجين في RAG للغة الفارسية: دليل عملي

البحث الهجين في RAG للفارسية: لماذا تفشل الوصفات الإنجليزية، وما ينقص PostgreSQL، وكيف تدمج رتب البحث الكثيف والمعجمي عبر RRF، وكيف تختبر ذلك.

البحث الهجين (hybrid search) في RAG للغة الفارسية يعني تشغيل مُسترجِعين جنبًا إلى جنب، أحدهما بحث بالتضمينات الكثيفة (dense embeddings) والآخر بحث معجمي (lexical)، ثم دمج ترتيبَيهما بطريقة دمج الرتب المتبادل (reciprocal rank fusion أو RRF). تحتاج إلى الاثنين لأن الفارسية تكسر افتراضات ضمنية في الوصفات المصممة للإنجليزية: فالكلمة نفسها قد تُخزَّن بحروف عربية أو فارسية، ومحرف عدم الوصل صفري العرض (ZWNJ) يقع داخل الكلمات، ولا يوفّر Snowball ولا PostgreSQL مُجذِّعًا (stemmer) للفارسية. والخلاصة: طبِّع النص عند الكتابة، وأبقِ الجانب المعجمي بلا تجذيع، وادمج الرتب لا الدرجات، ولا تُصفِّ حسب اللغة أبدًا، وجزِّئ المستند وفق بنيته هو، واختبر ذلك كله على مجموعة أسئلة ثنائية اللغة قبل أن تثق بأي جزء منه.

يستند هذا الدليل إلى نظامين. الأول هو المساعد في هذا الموقع، وقد بنيته وأتولى صيانته. والثاني هو Kartez Contract، حيث عملت مهندسَ ذكاء اصطناعي؛ وكان على مساعده (copilot) أن يجد البنود الصحيحة داخل عقود فارسية مرقّمة بالمادة والبند والملاحظة. أما تصميم الاسترجاع الموصوف هنا (كثيف مع معجمي، ودمج الرتب، وpgvector) فهو تصميم هذا الموقع؛ ومن Kartez لا آخذ إلا ما تذكره دراسة الحالة. ولا يتضمن هذا الدليل قياسًا معياريًا (benchmark) لأيٍّ من النظامين.

الاسترجاع الهجين للفارسية: تُطبَّع الأجزاء وتُحوَّل إلى تضمينات عند الكتابة؛ وتُدمج القائمتان الكثيفة والمعجمية عبر RRF، واللغة لا تكسر إلا التعادل التام؛ ويُفحص الحدّ الأدنى للتشابه قبل استدعاء نموذج اللغة: فدونه يُعطى السؤال الذي يتعلق بي سجلات الموقع، ويُرفض أي سؤال آخر.

لماذا تفشل وصفات RAG الإنجليزية مع النص الفارسي؟

الإخفاق المعتاد ليس إجابة خاطئة بل غياب الإجابة: لا يجد البحث كلمةً يراها المستخدم بعينه في المستند، لأن سلسلتين تبدوان متطابقتين تختلفان في المحارف التي تتألفان منها.

كلمة واحدة وسلسلتان مختلفتان: الياء والكاف في العربية والفارسية

تشترك الفارسية والعربية في الخط، لا في كل نقاط الترميز (code points). فالياء الفارسية «ی» هي U+06CC ARABIC LETTER FARSI YEH، والياء العربية «ي» هي U+064A ARABIC LETTER YEH: محرفان مختلفان. والأمر نفسه في الكاف الفارسية «ک»، وهي U+06A9 ARABIC LETTER KEHEH، والكاف العربية «ك»، وهي U+0643 ARABIC LETTER KAF. وتسمّي وثيقة متطلبات تخطيط النص العربي والفارسي الصادرة عن W3C زوج الياء صراحةً: فالحرف «يُمثَّل بمحرفين مختلفين». والمقطع المخزَّن بأحد الشكلين لا يطابق سؤالًا كُتب بالشكل الآخر؛ وخطوة التطبيع في المخطط أعلاه تُظهر الزوجين كليهما.

ومن بين ما يفعله هذا الموقع قبل أن يدخل أي مقطع إلى فهرس الاسترجاع أنه يحوّل الياء العربية والألف المقصورة والكاف العربية إلى أشكالها الفارسية، ويحذف علامات الاتجاه غير المرئية (bidi marks) من تلك النسخة، ويطبّق التحويل نفسه على كل سؤال.

محرف عدم الوصل صفري العرض (ZWNJ) جزء من الكلمة

«می‌شود» و«میشود» و«می شود» ثلاث سلاسل مختلفة. في الأولى يفصل محرف عدم الوصل صفري العرض (ZWNJ، ذو نقطة الترميز U+200C) البادئةَ عن الفعل، فيمنع اتصال الحروف دون أن يضيف مسافة. ويصف معيار Unicode هذا الوصل غير الافتراضي بأنه «مطلوب أحيانًا في كتابة الفارسية بالخط العربي»، وتنص متطلبات W3C على أن محارف التحكم «ينبغي الحفاظ عليها عند تخزين النصوص وعرضها».

احذف ZWNJ، كما قد يفعل خط معالجة (pipeline) مصمَّم للإنجليزية، فتصير «می‌شود» «میشود»؛ واستبدل به مسافة، فتصبح الكلمة الواحدة رمزين (tokens). والمُطبِّع في hazm، وهي حزمة أدوات لمعالجة اللغة الفارسية، يتضمن «تصحيح ZWNJ» (ZWNJ correction)، فيحوّل «کتاب های» إلى «کتاب‌های». وفي هذا الموقع يحتفظ النص المخزَّن بمحرف ZWNJ، ولا يعامله معاملة المسافة إلا التوحيدُ المستخدم داخل المقارنات.

ثلاث مجموعات من الأرقام

تكتب الفارسية ۱۲۳ بالأرقام الهندية العربية الشرقية (Eastern Arabic-Indic، من U+06F0 إلى U+06F9)، وتكتب العربية العدد نفسه ١٢٣ بالأرقام الهندية العربية (Arabic-Indic، من U+0660 إلى U+0669)، وكثيرًا ما يكتبه النص التقني 123.

وحِّد الأرقام للمقارنة فقط، لا في النص المخزَّن أبدًا؛ ففي هذا الموقع، من شأن إعادة كتابة الأرقام الفارسية المخزَّنة أن تغيّر طريقة عرض السيرة الذاتية الفارسية. لكن التوحيد يجب أن يجري على الطرفين: فإن وحّدت أرقام السؤال وحدها، لم يعد السؤال المكتوب بأرقام فارسية يطابق المقطع الذي نُسخ منه. وتطبّق مقالة حواجز حماية LLM أثناء البث التوحيد نفسه على الأرقام التي يُخرجها النموذج.

التشكيل نادر، فعلامة شاردة واحدة تُفسد المطابقة التامة

تقولها متطلبات W3C بوضوح: «من غير المعتاد في النصوص المكتوبة بالخط العربي استخدام علامات التشكيل للدلالة على الحركات وعلى تضعيف الصوامت». لذلك فإن المقطع الوحيد الذي فيه كسرة، أو السؤال الوحيد الذي لُصق وفيه شدّة، يخفق في كل مطابقة تامة مع الكتابة غير المشكولة. وفي بحث تحويل النص الفارسي إلى كلام الذي عملت عليه لصالح MCI، كانت الخاصية نفسها هي الجزء الصعب من جهة توليد الكلام: فالحركات القصيرة والإضافة (ezāfe)، وهي الحركة التي تربط الاسم بما يصفه، لا تُكتب. أما في الاسترجاع، فاحذف التشكيل داخل المقارنات وأبقِه في النص المخزَّن.

المستخدم يسأل بالعامية والمستند مكتوب بلغة رسمية

يكتب Tajalli وShamsfard وKalantari في تقديم مدوّنة نصية متوازية من ٥٠٬٠٠٠ زوج من الجمل: «الفارسية واحدة من اللغات التي تكثر فيها الفروق بين أسلوبَي الكتابة الرسمي وغير الرسمي». فالمستخدم يكتب «می‌تونم» حيث يكتب النص الرسمي «می‌توانم». وقراءتي للنتيجة: لا يرى المطابِق المعجمي تداخلًا يُذكر بين سؤال بالعامية وبند مكتوب بلغة رسمية، فتبقى الفرصة للمُسترجِع الكثيف، ومجموعة اختبار من أسئلة محكمة الصياغة ستُخفي هذه الفجوة.

تطبيع آمن للنص الفارسي

  • حوِّل الياء والكاف العربيتين إلى «ی» و«ک» الفارسيتين عند الكتابة، وفي المحتوى الفارسي وحده.

  • أبقِ ZWNJ في النص المخزَّن.

  • احذف علامات الاتجاه (bidi) من النسخة التي تفهرسها وتقارن بها، لا من النص المعروض.

  • وحِّد الأرقام واحذف التشكيل داخل المقارنات فقط، وعلى الطرفين.

  • طبِّق على الأسئلة الدالةَ نفسها تمامًا التي تطبّقها على المقاطع.

  • لا تطبّق التوحيد الفارسي على النص العربي أبدًا، فالياء (U+064A) والكاف (U+0643) العربيتان هما الحرفان الصحيحان فيه.

ولا يلتزم مُسترجِع هذا الموقع بهذه القائمة بعد: فهو يطبّق توحيد الحروف الفارسي في كل اللغات، بما فيها العربية؛ وقد تفوت قائمتَه المعجمية مقاطعُ مكتوبة بأرقام فارسية؛ ويُحذف التشكيل من مصطلحات السؤال ومن كل مقارنة تجريها بوابة الجمل، لكنه لا يُحذف من النص المخزَّن الذي يفحصه البحث بالكلمات المفتاحية. فاقرأ القائمة على أنها الهدف.

البحث في النص الكامل بالفارسية في PostgreSQL: لا يوجد إعداد مدمج

البحث في النص الكامل (full-text search) هو الخيار البديهي للنصف المعجمي من البحث الهجين، وتقول وثائق PostgreSQL إنه «يأتي بإعدادات مُعرَّفة مسبقًا للعديد من اللغات». والفارسية ليست منها.

لدى Snowball مُجذِّع عربي لا فارسي

تضم قائمة خوارزميات التجذيع في Snowball العربية، ولا تحوي أي مدخل ل Persian أو Farsi. ويبني PostgreSQL قواميس التجذيع لديه من Snowball: فقائمة اللغات في الشيفرة المصدرية ل PostgreSQL 18 تتضمن arabic ولا تتضمن persian، وقائمة القواميس في الوثائق تُظهر arabic_stem وsimple دون أي قاموس فارسي. والمُجذِّع العربي يشارك الفارسية خطَّها، لا قواعدها. وخارج قاعدة البيانات، توفّر حزمة hazm مُجذِّعًا (stemmer) وأداةَ ردٍّ إلى الصيغة المعجمية (lemmatizer) للفارسية؛ أما داخلها فلا شيء.

ما الذي يقدّمه إعداد simple وما الذي لا يقدّمه

يعمل قالب القاموس simple، بتعبير وثائق القواميس في PostgreSQL، عبر «تحويل الرمز المُدخَل إلى أحرف صغيرة ومقارنته بملف من كلمات التوقف». والقاموس المدمج simple بلا ملف لكلمات التوقف، لذا يُحتفظ بكل رمز، ولا يوجد تجذيع: فتبقى «قرارداد» و«قراردادها» وحدتين معجميتين (lexemes) منفصلتين، وهي خسارة للاستدعاء (recall) ومكسب لقابلية التنبؤ.

وقبل أن يعمل أي قاموس، يقرّر محلِّل النص (parser) ما الذي يُعدّ كلمة، ومفهومه للحرف «يحدده إعداد locale في قاعدة البيانات، وتحديدًا lc_ctype». فلا تفترض كيف تقسّم قاعدة بياناتك النص الفارسي، ولا سيما الكلمات الموصولة بمحرف ZWNJ. بل اسألها عبر الدالة ts_debug الواردة في وثائق اختبار البحث النصي وتصحيح أخطائه:

-- How does this database tokenise Persian? Check; do not assume.
SELECT alias, token, lexemes
FROM ts_debug('simple', 'قراردادها را می‌توان فسخ کرد');

ويستخدم البحث بالكلمات المفتاحية في هذا الموقع simple للصفوف الفارسية وenglish للصفوف الإنجليزية. ويشغّله عرض البحث الدلالي مقابل البحث بالكلمات المفتاحية إلى جانب المُسترجِع الهجين الخاص بالمساعد، على محتوى هذا الموقع.

تشابه الثلاثيات (trigram) لأخطاء الكتابة

يقارن الامتداد pg_trgm تسلسلات من ثلاثة محارف، فتظل الكلمة المكتوبة بخطأ إملائي تشترك في ثلاثيات مع صيغتها الصحيحة. وهو «يتجاهل المحارف التي لا تنتمي إلى الكلمات (أي غير الحروف والأرقام) عند استخراج الثلاثيات»، لذا تحقّق قبل الاعتماد عليه: ينبغي أن يُعيد SELECT show_trgm('قرارداد'); ثلاثيات مؤلفة من حروف فارسية. ويضيف البحث بالكلمات المفتاحية في الموقع فرعًا يعتمد على word_similarity، وهي تطابق استعلامًا قصيرًا مع أفضل امتداد متصل داخل مقطع طويل.

تغطية الكلمات الكاملة: القائمة المعجمية داخل مُسترجِع هذا الموقع

لا يستخدم مُسترجِع المساعد البحثَ في النص الكامل لقائمته المعجمية، إذ لا يوجد إعداد فارسي. بل يحسب عدد كلمات المحتوى في السؤال التي تظهر في المقطع كلماتٍ كاملة، مستعينًا بحدود الكلمات في التعابير النمطية (regular expressions). ويحذف كلمات التوقف في كل لغة من لغات الموقع، ويُسقط المصطلحات القصيرة جدًا، ويُبقي علامات الصوائت الهندية داخل الكلمة التي تنتمي إليها، ولا يطبّق أي تجذيع. والنتيجة نسبة تغطية بين ٠ و١.

حدود الكلمات مهمة: فمطابقة السلاسل الجزئية ل is تصيب his وthis وIsfahan، فيبدو كل مقطع كأنه دليل. ومهمٌّ كذلك أن النسبة محصورة بين طرفين، لأن الحدّ الأدنى للتشابه يمكن أن يُقارَن بها. ولا تُحتسب النسبة في حدّ الرفض إلا إذا تطابقت كلمتان مختلفتان على الأقل من كلمات المحتوى.

المتجهات المتفرقة المُتعلَّمة: الخيار الموثَّق

يطابق الاسترجاع المتفرق المُتعلَّم (learned sparse retrieval) المصطلحاتِ كما يفعل البحث المعجمي، لكن بأوزان يتعلّمها نموذج. وفي BGE-M3 تكون المصطلحات قطعًا من إنتاج المُقطِّع (tokenizer): فبطاقة نموذجه تعطي أوزانًا ل De وfin وation. والمتجه «حجمه مساوٍ لحجم المفردات»، بتعبير الورقة التي قدّمته، أي ٢٥٠٬٠٠٢ في إعدادات النموذج. ولا يستخدمها مُسترجِع هذا الموقع في بيئة الإنتاج: فهو يشغّل البحث الكثيف مع المعجمي، وواجهة API للتضمينات التي يستدعيها لا تُعيد أي أوزان متفرقة. وإن أضفتها، فاقرأ أولًا قسم خطأ الإزاحة بواحد أدناه.

البحث الهجين مع دمج الرتب المتبادل: ادمج الرتب لا الدرجات

يُعيد المُسترجِع الكثيف والمُسترجِع المعجمي قوائم مرتَّبة تعني درجاتُها أشياء مختلفة. ويتجاهل دمج الرتب المتبادل الدرجاتِ ويجمع المواضع.

صيغة RRF، ومن أين جاءت k = 60

يعرّف Cormack وClarke وBüttcher الدرجة المدمجة لمستند ما بأنها مجموع 1/(k + r(d)) على كل الترتيبات، حيث r(d) رتبته في كل قائمة. والثابت أقل حساسية مما يبدو: «ثُبِّتت k = 60 خلال دراسة استطلاعية ولم تُغيَّر خلال التحقق اللاحق»، وأشارت الدراسة الاستطلاعية إلى «أن k = 60 قريبة من المثلى، لكن الاختيار لم يكن حاسمًا».

ويستخدم مُسترجِع هذا الموقع k = 60، وتُسهم كل قائمة بمرشحين أكثر مما ستستخدمه الإجابة، فيظل بإمكان مقطع في منتصف قائمة وقريب من رأس الأخرى أن يظهر في النتائج. وهذه خطوطه العريضة:

K = 60  # Cormack, Clarke and Büttcher

def fuse(lists, k=K):
    """lists: (weight, passage ids best-first) for each retriever."""
    scores = {}
    for weight, ranked in lists:
        for rank, passage in enumerate(ranked, start=1):
            scores[passage] = scores.get(passage, 0.0) + weight / (k + rank)
    return scores

def order(scores, locales_of, query_locale):
    # The fused score decides. Locale only separates exactly equal
    # scores, and the id keeps the order identical on every run.
    return sorted(scores, key=lambda p: (-scores[p], query_locale not in locales_of[p], str(p)))

fused = fuse([(1.0, dense_ids), (0.5, lexical_ids)])
passages = order(fused, locales_of, query_locale="fa")[:top_k]

لماذا يحتاج جمع الدرجات إلى أوزان عليك ضبطها

البديل هو جمع الدرجات، وهذا ما تفعله الورقة التي يقوم عليها BGE-M3 بالصيغة s_rank ← w1·s_dense + w2·s_lex + w3·s_mul، «حيث تعتمد قيم w1 وw2 وw3 على سيناريو الاستخدام اللاحق (downstream)». وللجمع بين الكثيف والمتفرق على MIRACL جعل المؤلفون قيمة w1 مساوية ١ وقيمة w2 مساوية ٠٫٣؛ وعلى MLDR، وهو معيار لاسترجاع المستندات الطويلة، جعلوا w1 مساوية ٠٫٢ وw2 مساوية ٠٫٨.

فتشابه جيب التمام (cosine)، والجداء الداخلي للمتجهات المتفرقة، ونسبة التغطية تقع على مقاييس مختلفة، وسعر الصرف بينها يتغيّر بتغيّر المدوّنة (corpus). ودمج الرتب يتجاوز هذه المعايرة. لكنه مقايضة لا مكسب مجاني: فالورقة تُبلغ عن نتائج قوية بالمجاميع الموزونة، وRRF لا يميّز الفوز الساحق من الفوز بفارق ضئيل.

لماذا تحصل القائمة المعجمية على نصف الوزن

في هذا الموقع، وُجدت القائمة المعجمية لترفع المقاطع التي تحوي سلاسل دقيقة قد يطمسها النموذج الكثيف: اسم نطاق مثل booqai.app، أو اختصار، أو اسم برنامج مثل SAMPAD. وهذا هو السبب المعتاد للاحتفاظ بقائمة معجمية. ويشير مقال Anthropic عن Contextual Retrieval إلى الأمر نفسه: فنموذج التضمين «قد يجد محتوى عن رموز الأخطاء عمومًا، لكنه قد يفوّت المطابقة الدقيقة ل TS-999».

وتُسهم القائمة المعجمية بنصف الوزن، فترفع مقطعًا أعادته القائمة الكثيفة أيضًا دون أن تدع تداخل الكلمات المفتاحية يتغلّب على ترتيب القائمة الكثيفة. والثمن حقيقي: مع k = 60 وأعداد المرشحين في هذا الموقع، يقع أي مقطع لا تجده إلا القائمة المعجمية دون كل مرشحي القائمة الكثيفة، فلا يستطيع الدمج أن يستعيد مقطعًا فات النموذجَ الكثيف تمامًا. والاستثناء الوحيد هو المقطع صاحب أعلى تشابه، إذ يُعاد دائمًا، كما يشرح القسم التالي. وإذا فشل استدعاء التضمين، يعود الاسترجاع إلى القائمة المعجمية وحدها، ويظل الحدّ الأدنى للتشابه ساريًا.

درجة RRF ليست مقياسًا للثقة

تُبنى درجة RRF من المواضع وحدها، لذا فحتى مدوّنة من الضجيج الخالص تُنتج مقطعًا في المرتبة الأولى بأفضل درجة مدمجة متاحة. ولهذا يُقارَن حدّ الرفض في هذا الموقع بتشابه حقيقي لكل مقطع، ولا يُقارَن أبدًا بالدرجة المدمجة: هو الأعلى من تشابه جيب التمام بين المقطع والسؤال كما طُرح (أو، في سؤال المتابعة، السؤال موصولًا بالسؤال الذي سبقه، ومعه العنصر الذي يشير إليه إذا صيغ بوصفه موضعًا في قائمة، أو مطلع الإجابة السابقة إذا تعذّرت قراءة ذلك العنصر) ومن تغطية الكلمات الكاملة، التي لا تُحتسب إلا إذا تطابقت كلمتان على الأقل من كلمات المحتوى. ويجري ذلك قبل استدعاء نموذج اللغة.

والمقطع صاحب أعلى تشابه بين كل المرشحين يكون دائمًا بين المقاطع المُعادة، حتى لو وضعه الدمج في مرتبة أدنى من أن يُختار. وتحت الحدّ، يُرفض السؤال الذي لا يتعلق بي دون استدعاء النموذج، أما السؤال الذي يتعلق بي فيُعطى سجلات الموقع بدلًا من ذلك؛ وتتناول مقالة حواجز الحماية من هلوسة RAG الأمرين كليهما.

لا تُصفِّ الاسترجاع حسب اللغة أبدًا

يبدو مرشِّح اللغة من حُسن التنظيم: الأسئلة الفارسية تبحث في المقاطع الفارسية. لكنه في مدوّنة ثنائية اللغة يحذف الأدلة.

قد يكون أفضل دليل لديك باللغة الأخرى

في هذا الموقع، تقع المقاطع بكل اللغات جنبًا إلى جنب في جدول واحد، ويبحث السؤال فيها جميعًا. فالسيرة الذاتية الفارسية هي المستند الأصلي والإنجليزية مقتبسة عنها، لذا قد يكون أفضل دليل لسؤال إنجليزي عن منصب ما موجودًا بالفارسية. وكان ل Kartez Contract الشكل نفسه في الاتجاه المعاكس: فكان على معالج الصياغة فيه أن ينقل متطلبات مكتوبة بالفارسية إلى مكتبة من النصوص الإنجليزية لصياغة العقود.

اللغة لكسر التعادل التام فقط

ما زال للغة استخدام واحد في الترتيب: بين مقطعين لهما الدرجة المدمجة نفسها تمامًا، يتقدّم المقطع المتاح بلغة السؤال، ثم يحسم معرّف ثابت، كما في مفتاح الفرز أعلاه. ومع قائمة كثيفة واحدة ونصف الوزن للقائمة المعجمية، يكاد التعادل التام لا يقع. لكنه قد يقع حين يُبحث عن السؤال بأكثر من صيغة، كما يرد أدناه، فينال مقطعان الرتب نفسها من القوائم الكثيفة لتلك الصيغ، وهي متساوية الوزن؛ وحتى عندئذ لا تحسم اللغة إلا إذا كان أحد المقطعين وحده متاحًا بلغة السؤال. وهذا مقصود: يجب ألا تتقدم اللغة على الدرجة أبدًا. وللغة مهمة أخرى: اختيار الترجمة التي يراها القارئ من المقطع.

أما المكافأة الصغيرة التي تُضاف إلى درجات المقاطع المكتوبة بلغة السؤال، فليست كسرًا للتعادل. فالفارق بين إسهامين متجاورين في قائمة واحدة كاملة الوزن هو 1/((k + r)(k + r + 1))، وهو فارق يصغر كلما نزلنا في القائمة، وقد تتقارب المجاميع الآتية من قائمتين أكثر من ذلك. والمكافأة الثابتة آمنة ما دامت دون كل فارق يمكن أن تنتجه أعماق قوائمك وأوزانها، وهو حدّ يتغيّر كلما تغيّر أيٌّ منهما. وما إن تتجاوز المكافأةُ ذلك الحدّ حتى تتبادل نتائجُ لم تكن متعادلة مواضعَها في منتصف القائمة، وبين المرشحين أنفسهم الذين وُجد الدمج ليرفعهم.

مقطع واحد، عشر ترجمات

تُنشر صفحات هذا الموقع بما يصل إلى عشر لغات، وكانت نسخ المقطع الواحد بلغات مختلفة قد تشغل عدة مواضع من المواضع الثمانية التي تُسلَّم إلى النموذج. أما الآن فيعامل الاسترجاع ترجمات المقطع الواحد مرشحًا واحدًا: يُقيَّم بأفضل ترجماته، ويُرتَّب مرة واحدة، ويُعرض بلغة القارئ متى وُجدت تلك الترجمة. وتُجمَّع المقالات حسب القسم لا حسب الجزء، لأن القسم الطويل ينقسم إلى عدد مختلف من الأجزاء في كل لغة؛ أما الترجمة التي لا تتطابق أقسامها مع أقسام النسخة الإنجليزية فتبقى منفصلة بدل أن تُجمَّع خطأً. ولغة القارئ تحدد الترجمة المعروضة؛ وفيما عدا التعادل التام، لا تحدد أبدًا موضع المقطع في الترتيب.

أسئلة فارسية على مستندات إنجليزية

الجانب الكثيف وحده يستطيع عبور اللغات: فالسؤال الفارسي والمقطع الإنجليزي لا يكادان يشتركان في كلمة، باستثناء المصطلحات اللاتينية مثل RAG. وتقييم BGE-M3 نفسه لا يحسم المسألة للفارسية: فمعياره العابر للغات، MKQA، يسترجع مقاطع من ويكيبيديا الإنجليزية لاستعلامات في ٢٥ لغة غير الإنجليزية، والعربية من بينها، أما الفارسية فلا. فاختبر أزواجك الفارسية–الإنجليزية بنفسك.

أسئلة بلا كلمات مفتاحية

سؤال زائرٍ مثل «ماذا فعل مؤخرًا؟» لا يحمل كلمة تستطيع المدوّنة مطابقتها، و«وقبل ذلك؟» لا يحمل شيئًا على الإطلاق. فحين يتعلق السؤال بي، يُبحث عنه أيضًا مع إلحاق اسمي به، ويُبحث عن سؤال المتابعة أيضًا موصولًا بالسؤال الذي سبقه، وبينهما العنصر الذي يشير إليه إذا صيغ بوصفه موضعًا في قائمة، مثل «الثاني»، أو مطلع الإجابة السابقة إذا تعذّرت قراءة ذلك العنصر. وتُحوَّل كل هذه الصيغ إلى متجهات في استدعاء واحد، وتُدمج حسب الرتبة. والصيغة التي أُلحق بها اسمي ترتّب المقاطع لكنها لا تُحتسب أبدًا في حدّ الرفض، لأن اسمي وحده قد يجعل سؤالًا عني لا صلة له بعملي يبدو قريبًا من سيرتي الذاتية. أما الأسئلة عن عملي في مجمله فلا تعتمد على الاسترجاع: بل يُجاب عنها من سجلات مؤرَّخة يبنيها الموقع من سيرتي الذاتية ودراسات الحالة والمقالات والخدمات، كما تشرح مقالة حواجز الحماية من هلوسة RAG.

تجزئة العقود الفارسية حسب المادة والبند والملاحظة

للعقود الفارسية بنيتها الخاصة من المواد (ماده) والبنود (بند) والملاحظات (تبصره). وفي مساعد Kartez Contract، كان عقد المستخدم نفسه يُقسَّم وفق هذه البنية، ولا يُستخدم للإجابة عن السؤال إلا الأقسام المتصلة به؛ فالإجابات «لا تستند إلا إلى البنود المختارة». وتتناول دراسة حالة Kartez Contract بقية المشروع، وتتناول مقالة ما الذي نبنيه في مواجهة هلوسة الذكاء الاصطناعي القانوني المجال نفسه.

والمبدأ نفسه يوجّه أداة التجزئة في هذا الموقع: فكل جزء (chunk) سجلّ دلالي واحد. لا يُفصل منصب في السيرة الذاتية عن تواريخه أبدًا، وتُقسَّم دراسة الحالة عند أقسامها كما كُتبت، والمقالة عند عناوينها، ولا ينقسم القسم الطويل إلا عند حدود فقرة. ويرافق عنوانُ المستند كلَّ جزء من أجزاء الأقسام، فيبقى الجزء المسترجَع وحده قادرًا على أن يخبر من أين جاء.

وفي العقود الفارسية، يوحي ذلك ببضع قواعد، أقدّمها نصيحةً عامة لا وصفًا لكيفية بناء Kartez:

  • أبقِ الرقم مع النص. «ماده ۵» ونصّها مكانهما جزء واحد.

  • انقل عنوان المادة الأم إلى كل جزء يحوي بندًا أو ملاحظة منها.

  • لا تقطع بندًا عند عدد معيّن من الرموز أبدًا. فالملاحظة المفصولة عن بندها قد تغيّر ما يبدو أن البند يقوله.

  • خزِّن المسار البنيوي، مادة › بند › ملاحظة، كي تستطيع الإجابة أن تدلّ المختص على الموضع الدقيق.

pgvector: مسح دقيق أم HNSW؟

يجيب pgvector بنفسه عن نصف هذا السؤال: «افتراضيًا، يُجري pgvector بحثًا دقيقًا عن أقرب الجيران، وهو يوفّر استدعاءً كاملًا». أما الفهرس التقريبي فهو «يقايض بعض الاستدعاء بالسرعة».

والمرشِّحات هي الكلفة الأقل وضوحًا. «مع الفهارس التقريبية، تُطبَّق التصفية بعد مسح الفهرس. فإذا طابق شرطٌ ١٠٪ من الصفوف، فمع HNSW والقيمة الافتراضية ٤٠ ل hnsw.ef_search، لن يطابق في المتوسط إلا ٤ صفوف». ويستطيع pgvector بدءًا من الإصدار 0.8.0 أن يمسح الفهرس تكراريًا حتى يطابق عدد كافٍ من الصفوف، بعد أن تفعّل ذلك.

ولا يوجد فهرس تقريبي في جدول الاسترجاع في هذا الموقع: فالبحث المتجهي مسح دقيق. وحالة النشر جزء من شرط WHERE في SQL، وليست أبدًا مرشِّحًا يُطبَّق بعد LIMIT، إذ كان من شأنه أن يقلّص مجموعة النتائج بصمت. ابدأ بالمسح الدقيق، وأضف فهرسًا حين يستدعي زمن الاستجابة ذلك، وقارن نتائجه الأعلى (top-k) بنتائج المسح الدقيق على أسئلتك أنت قبل أن تثق به.

خطأ الإزاحة بواحد في sparsevec ضمن pgvector

لا يهمّك هذا إلا إن أضفت متجهات متفرقة مُتعلَّمة إلى PostgreSQL، وهو يخفق بصمت.

الصيغة النصية للمتجهات المتفرقة في pgvector هي {index1:value1,index2:value2}/dimensions، «والمؤشرات تبدأ من ١ مثل مصفوفات SQL». أما معرّفات الرموز في المُقطِّع فتبدأ من ٠. وعميل Python يأخذ مؤشرات تبدأ من الصفر («تبدأ المؤشرات من ٠») ويضيف ١ بنفسه حين يكتب الصيغة النصية.

from pgvector import SparseVector

weights = {0: 0.05, 2: 0.14, 4: 0.04}  # tokeniser ids, 0-based

# Client library: pass 0-based ids; it adds 1 when it writes SQL text.
vec = SparseVector(weights, 6)

# Hand-built SQL literal: shift exactly once, here.
literal = "{" + ",".join(f"{i + 1}:{w}" for i, w in weights.items()) + "}/6"
# "{1:0.05,3:0.14,5:0.04}/6"

أزِح المؤشرات مرة واحدة بالضبط، في الموضع الذي تُبنى فيه القيمة الحرفية (literal) في SQL يدويًا. فمُدخِل النص في pgvector يحوّل كل مؤشر إلى مؤشر يبدأ من الصفر، ولا يرفع الخطأ sparsevec index out of bounds إلا إذا كانت النتيجة أقل من ٠ أو مساوية لعدد الأبعاد أو أكبر منه. لذا فالإزاحة المزدوجة لا تُخفق إلا عند أعلى المفردات، والإزاحة الغائبة لا تُخفق إلا عند الرمز ذي المعرّف ٠، وكل وزن آخر يقع بصمت على الرمز المجاور.

اختيار نموذج تضمين (embedding) للغة الفارسية

ما الذي يقوله MIRACL عن الفارسية والعربية

MIRACL معيار للاسترجاع يشمل ١٨ لغة، منها الفارسية والعربية. وتُبلغ الورقة التي يقوم عليها BGE-M3 عن nDCG@10، وهو مقياس لجودة الترتيب في النتائج العشر الأولى، على مجموعة التطوير فيه (نسخة ACL؛ وقد حدّث المؤلفون هذه النتائج في يوليو ٢٠٢٤):

المُسترجِع

الفارسية (fa)

العربية (ar)

BM25 على رموز XLM-RoBERTa

٢٨٫٧

٣٩٫٥

mE5-large (نموذج E5 متعدد اللغات)

٥٩٫٠

٧٦٫٠

BGE-M3: متفرق

٤٥٫١

٦٧٫١

BGE-M3: كثيف

٦٠٫٩

٧٨٫٤

BGE-M3: كثيف + متفرق + متعدد المتجهات

٦٣٫٤

٨٠٫٢

على الفارسية، يسجّل الاسترجاع الكثيف درجات أعلى بكثير من BM25 هذا، ويقع المتفرق المُتعلَّم بين الاثنين، ويضيف الجمع بين الأوضاع الثلاثة قليلًا فوق الكثيف. وترافق هذه الأرقامَ أربعة تحفظات:

  • BM25 هذا يعمل على رموز جزئية (subword). لمجاراة النموذج، شغّل المؤلفون BM25 بمُقطِّع XLM-RoBERTa؛ ويُظهر ملحق ورقتهم أن المُحلِّل اللغوي في Lucene يرفع متوسط MIRACL عبر اللغات من ٣١٫٩ إلى ٣٨٫٥. أما خط الأساس BM25 في ورقة MIRACL نفسها، الذي يستخدم مُحلِّلات Lucene الخاصة بكل لغة حيثما وُجدت، فيبلغ ٠٫٣٣٣ في nDCG@10 على الفارسية و٠٫٤٨١ على العربية، على مقياس من ٠ إلى ١.

  • MIRACL أحادي اللغة. فهو «مصمَّم لدعم مهام الاسترجاع أحادية اللغة، حيث تكون الاستعلامات والمدوّنات باللغة نفسها»، لذا لا يقول شيئًا عن أسئلة فارسية على مستندات إنجليزية.

  • المقارنة بين الفارسية والعربية ليست مقارنة مضبوطة. فلكل منهما أسئلة ومدوّنات ويكيبيديا مختلفة؛ وتسرد صفحة المشروع ٦٣٢ استعلامًا في مجموعة التطوير للفارسية و٢٬٨٩٦ استعلامًا للعربية، فالفارق إشارة لا مقياسًا للصعوبة.

  • ضُبط BGE-M3 ضبطًا دقيقًا على بيانات تدريب MIRACL، لذا فصفوفه درجات داخل النطاق (in-domain).

FaMTEB وHakim

FaMTEB، المنشور في Findings of EMNLP عام ٢٠٢٥، معيار فارسي مبني على MTEB، يضم «٦٣ مجموعة بيانات تغطي سبع مهام مختلفة»، منها الاسترجاع.

وHakim نموذج تضمين فارسي موصوف في ورقة أولية (preprint) على arXiv، نُشرت أول مرة في مايو ٢٠٢٥ ونُقّحت في أكتوبر ٢٠٢٥. ويُبلغ مؤلفوه عن تحسّن بنسبة ٨٫٥٪ على معيار FaMTEB مقارنةً بالأساليب القائمة. وثمة حقيقتان ينبغي ذكرهما إلى جانب هذه النتيجة: مؤلفو Hakim هم أيضًا مؤلفو FaMTEB، وبيانات التدريب في المرحلة الثانية ل Hakim تتضمن MIRACL_fa. ولا تجعل أيٌّ منهما النتيجة خاطئة؛ لكن كلتيهما سبب للاختبار على بياناتك أنت.

ما الذي تختبره قبل أن تختار

  • أسئلتك أنت بالفارسية والإنجليزية، بما فيها الصياغات العامية.

  • أزواج عابرة للغات: أسئلة فارسية إجاباتها في نص إنجليزي، والعكس.

  • صيغ إملائية مختلفة للسؤال الواحد: أشكال الياء والكاف، وZWNJ أو المسافة، وكل مجموعة من مجموعات الأرقام.

  • الترخيص. تنص بطاقة نموذج BGE-M3 على أنه MIT.

  • الحد الأقصى لطول المُدخَل مقارنةً بحجم أجزائك، محسوبًا بالرموز. يصف مؤلفو BGE-M3 مستندات طويلة تصل إلى ٨٬١٩٢ رمزًا، وقد يحتاج النص الفارسي إلى رموز أكثر من الإنجليزي للمحتوى نفسه؛ وقد وجدت مقالة ضريبة الرموز على الفارسية أن هذه الزيادة صغيرة على مُقطِّع BGE-M3 نفسه، وأكبر بكثير على مُقطِّعات نماذج التوليد التي قاستها.

  • بُعد المتجه. في هذا الموقع هو مثبَّت في مخطط قاعدة البيانات (schema)، لذا فتغييره يعني ترحيلًا (migration) وإعادة فهرسة كاملة.

ومستكشف التضمينات في الموقع ليس عصبيًا عن قصد: فمتجهاته المبنية على n-gram المحارف تجمع الكلمات بحسب الإملاء لا المعنى، وهذه هي الفجوة التي على النموذج الكثيف أن يسدّها في الفارسية العامية.

تقييم RAG الفارسية على مجموعة ذهبية ثنائية اللغة

تحتاج المجموعة الذهبية (golden set) ل RAG بالفارسية إلى أسئلة باللغتين: أسئلة لها إجابات مع المقاطع التي يجب أن تُسترجع لها، وأسئلة يجب رفضها لأنه لا دليل عليها في المدوّنة، وأسئلة يوجد أفضل دليل عليها باللغة الأخرى. قِس recall@k على الأسئلة التي لها إجابات، وتحقّق من أن كل سؤال بلا دليل يقع دون الحدّ الأدنى للتشابه، وأضف اختبارًا يفشل إذا لم تستند أي إجابة إلى مقطع باللغة الأخرى. فهو يكشف مرشِّح اللغة في اليوم الذي يعيد فيه أحدهم إضافته.

في هذا الموقع، تعمل المجموعة الذهبية، وهي بلغات الموقع العشر كلها وأكثر أسئلتها بالإنجليزية والفارسية، في CI عبر شيفرة الدمج والتجميع والترتيب واختيار top-k الحقيقية، لكن على مدوّنة في الذاكرة ومع نموذج تضمين بديل من نوع كيس الرموز (bag-of-tokens)، لا نموذج الإنتاج. وهذا البديل يغذّي أيضًا قائمة متفرقة لا يشغّلها الإنتاج، ومجموعة الأسئلة التي بلا دليل صغيرة. ف CI يكشف التراجعات في آلية الترتيب، لا استدعاء المساعد العامل على الموقع. وثمة أمر يشغّل الاسترجاع وحده، ولا يستدعي نموذج اللغة أبدًا، يتحقق من حدّ الرفض على نموذج التضمين المنشور؛ أما الاستدعاء فلم يُقَس بعدُ على ذلك النموذج، لذا لا أنشر أي رقم استدعاء لهذا المُسترجِع.

ما الذي ينتقل من هذا إلى العربية؟

لم أصمّم استرجاعًا للنص العربي ولم أقيّمه. يفهرس مُسترجِع هذا الموقع محتواه المكتوب بالعربية، لكن لا شيء فيه بُني للعربية أو اختُبر عليها. وهذا القسم هو ما يقتضيه التصميم الفارسي على الورق.

معظمه ينتقل: دمج الرتب، وعدم التصفية حسب اللغة، والتجزئة وفق البنية، وحذف التشكيل داخل المقارنات (فعبارة W3C تشمل الخط العربي عمومًا)، والبدء بالمسح الدقيق، ومجموعة ذهبية ثنائية اللغة. وتختلف ثلاثة أمور:

  • يوفّر PostgreSQL العربية فعلًا. ففي Snowball مُجذِّع عربي وفي PostgreSQL قاموس arabic_stem؛ أما هل يفيد التجذيع، فهذا ما تحسمه مجموعتك الذهبية.

  • يجب ألا يعمل توحيد الحروف الفارسي. ففي العربية، الياء (U+064A) والكاف (U+0643) هما الحرفان الصحيحان، لذا يجب أن تُطبِّع المدوّنة المختلطة كل نص بحسب لغته.

  • الأرقام مختلفة. الأرقام الهندية العربية (Arabic-Indic) هي من U+0660 إلى U+0669، لا النطاق U+06F0 الذي تستخدمه الفارسية.

وللعربية أيضًا تغطية أوسع في المعايير، في MIRACL وMKQA. وفي جدول MKQA في ورقة BGE-M3، تبلغ الأسئلة العربية على ويكيبيديا الإنجليزية في Recall@100 قيمة ٧١٫١ مع BGE-M3 الكثيف، مقابل ١٨٫٩ ل BM25 القائم على الرموز الجزئية في الورقة؛ ويعدّ Recall@100 السؤالَ موجودًا حين يقع مقطع يحوي إجابته ضمن النتائج المئة الأولى. ولا تملك الفارسية رقمًا عابرًا للغات مثله.

قائمة تحقق: البحث الهجين في RAG للفارسية

  • طبِّع أشكال الحروف الفارسية عند الكتابة، وفي المحتوى الفارسي وحده؛ واحذف علامات الاتجاه (bidi) من نسخة الاسترجاع.

  • شغّل المُطبِّع نفسه على كل سؤال، وأبقِ ZWNJ في النص المخزَّن.

  • أبقِ الجانب المعجمي خاليًا من التجذيع، وأضف تشابه الثلاثيات لأخطاء الكتابة.

  • ادمج القائمتين الكثيفة والمعجمية بدمج الرتب المتبادل، مع k = 60 والقائمة المعجمية بنصف الوزن، مع العلم أن نصف الوزن لا ينقذ مقطعًا فات القائمةَ الكثيفة.

  • اجعل حدّ الرفض على تشابه، لا على درجة RRF أبدًا.

  • لا تُصفِّ حسب اللغة أبدًا؛ ودع اللغة تحسم التعادل التام فقط، وتختار الترجمة التي تُعرض من المقطع.

  • جزِّئ وفق بنية المستند، مع العنوان في كل جزء.

  • ابدأ بالمسح الدقيق، وقِس أي فهرس تقريبي عليه.

  • قيِّم على مجموعة ذهبية بكل لغة تخدمها، فيها أسئلة عابرة للغات وأسئلة بلا دليل.

  • اختبر نماذج التضمين على أسئلتك الفارسية أنت، بما فيها العامية.

إن كنت تخطط لاسترجاع على محتوى فارسي أو متعدد اللغات، فإن تشخيص المشروع يحوّل خمسة أسئلة إلى هندسة معمارية أولية.

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

هل يكفي BM25 للبحث بالفارسية؟

ليس وحده. يلتقط BM25 الأسماء والأرقام والرموز الدقيقة، لكنه يفوّت إعادة الصياغة، وقد لا يشترك سؤال فارسي عامي مع مستند رسمي إلا في القليل من الألفاظ. وعلى مجموعة الفارسية في MIRACL، تقع النتائج المنشورة للاسترجاع الكثيف أعلى بكثير من BM25، سواء عمل BM25 على رموز جزئية أو بمُحلِّلات Lucene اللغوية. استخدم BM25، أو تغطية الكلمات الكاملة، نصفًا معجميًا لبحث هجين.

هل أترجم الاستعلامات الفارسية إلى الإنجليزية قبل الاسترجاع؟

ليس افتراضيًا. فنموذج التضمين متعدد اللغات يضع اللغتين أصلًا في فضاء متجهي واحد، والترجمة تضيف استدعاءً لنموذج قد يشوّه الأسماء والمصطلحات الدقيقة. والمعيار العابر للغات الذي استخدمه مؤلفو BGE-M3 يشمل العربية لا الفارسية، لذا اختبر النهجين على أزواجك الفارسية–الإنجليزية.

ما أفضل نموذج تضمين ل RAG بالفارسية؟

المعايير تعطيك قائمة مختصرة، لا إجابة: فنتائج MIRACL تغطي BGE-M3 وE5 متعدد اللغات على الفارسية، وFaMTEB يمتد على ٦٣ مجموعة بيانات، ومؤلفو Hakim يُبلغون عن تحسّن على FaMTEB. اختبر القائمة المختصرة على أسئلتك العامية والعابرة للغات، بعد التحقق من الترخيص وبُعد المتجه.

هل أحتاج إلى قاعدة بيانات متجهات منفصلة ل RAG بالفارسية؟

ليس في البداية. ف pgvector داخل PostgreSQL الذي لديك يُجري افتراضيًا بحثًا دقيقًا عن أقرب الجيران، باستدعاء كامل، ويُبقي المرشِّحات مثل حالة النشر داخل استعلام SQL. ولا تُضف فهرسًا تقريبيًا إلا حين يستلزم زمن الاستجابة ذلك.

لماذا لا يجد البحث الفارسي كلمة موجودة بوضوح في النص؟

عادةً لأن السلاسل تختلف بطريقة لا تراها العين: الياء أو الكاف العربيتان (U+064A، U+0643) مقابل «ی» أو «ک» الفارسيتين، أو ZWNJ كُتب مسافةً أو أُسقط، أو مجموعة أرقام مختلفة، أو علامة تشكيل شاردة. طبِّع المقاطع المخزَّنة والأسئلة بالدالة نفسها، ووحِّد الأرقام وأزِل التشكيل على طرفَي المقارنة.

المصادر