تطبيع البحث العربي: نقص الدعم لمتغيرات الهمزة، وأشكال الياء/الكاف، والتكافؤ الإملائي

لست خبيرًا تقنيًا، ولكني كنت أبحث في هذه المشكلة لأنني أريد التأكد من عدم تفويت أي استعلامات بحث في منتدى ثنائي اللغة بالفارسية والعربية. نظرًا لأن Discourse يستخدم PostgreSQL، فإن التطبيع يصبح ضروريًا: قد يبحث المستخدم باستخدام أحرف فارسية، بينما يتم تخزين نفس الكلمة باستخدام أحرف عربية - أو العكس. بدون تطبيع مناسب، ستفشل عملية البحث.
بناءً على ما تعلمته، فإن استخدام تطبيع Unicode NFKC هو نقطة انطلاق قوية - فهو يتعامل مع العديد من حالات التوافق مثل الروابط، وأشكال العرض، والأرقام العربية/الفارسية.
ومع ذلك، بالنسبة للنص الفارسي والعربي، لا يكفي NFKC وحده. فهو لا يقوم بتطبيع العديد من المتغيرات الحرفية الهامة المتكافئة بصريًا ودلاليًا ولكنها تختلف على المستوى الثنائي.

أدناه، أقدم الإجراءات والرؤى التي توصلت إليها من خلال بحثي واستكشافي.


रणनी استراتيجية التصميم الشاملة

  1. طبق تطبيع Unicode NFKC أولاً للتعامل مع الروابط، وأشكال العرض، وتوحيد الأرقام.
  2. ثم طبق تعيينات الأحرف المخصصة بترتيب محدد (على سبيل المثال، تطبيع متغيرات الهمزة قبل الياء العربية).
  3. فصل سياسات التطبيع للتخزين مقابل البحث:
    • استخدم ملف تعريف محافظ للتخزين القياسي (احتفظ بـ ZWNJ، وتجنب التحولات الدلالية).
    • استخدم ملف تعريف متساهل للبحث (تجاهل ZWNJ، ووحد متغيرات الهمزة، وطبع الأرقام).
  4. يجب أن تكون جميع التعيينات قابلة للتكوين عبر جدول تعيين مركزي في قاعدة البيانات أو تجزئة Ruby في التطبيق.

:one: ملفات تعريف التطبيع

:green_circle: محافظ (للتخزين)

  • تحويل أدنى
  • طبق NFKC
  • طبع الكاف/الياء العربية إلى ما يعادلها بالفارسية
  • إزالة علامات التشكيل
  • احتفظ بـ ZWNJ
  • التخزين كـ original_text + normalized_conservative

:blue_circle: متساهل (للبحث)

  • مطابقة قوية
  • طبق جميع قواعد المحافظ
  • إزالة/تجاهل ZWNJ
  • طبع متغيرات الهمزة إلى الأحرف الأساسية
  • تحويل جميع الأرقام إلى ASCII
  • اختياريًا توحيد التاء المربوطة → الهاء
  • يستخدم لمعالجة استعلامات البحث مسبقًا

:two: جدول تعيين شامل

المصدر الهدف يونيكود ملاحظات
ك ک U+0643 → U+06A9 الكاف العربية → الكاف الفارسية
ي ی U+064A → U+06CC الياء العربية → الياء الفارسية
ى ی U+0649 → U+06CC متغير الياء النهائي
أ، إ، ٱ ا مختلف → U+0627 أشكال الهمزة → الألف
ؤ و U+0624 → U+0648 الهمزة على الواو
ئ ی U+0626 → U+06CC الهمزة على الياء
ء — U+0621 إزالة أو الاحتفاظ (قابل للتكوين)
ة ه U+0629 → U+0647 التاء المربوطة → الهاء (اختياري)
ۀ هٔ U+06C0 ↔ U+0647+U+0654 تطبيع الشكل المركب
ڭ گ U+06AD → U+06AF المتغيرات الإقليمية
‌ — U+200C ZWNJ: الاحتفاظ في المحافظ، الإزالة في المتساهل
٤، ۴ 4 U+0664، U+06F4 → ASCII تطبيع الأرقام
علامات التشكيل — U+064B–U+0652 إزالة جميع الحركات
ZWJ — U+200D إزالة الروابط غير المرئية
مسافات متعددة مسافة واحدة — تطبيع المسافات

:three: مقتطف تعيين سريع (لـ SQL أو Ruby)

ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (تمت إزالته في المتساهل)
الحركات (U+064B..U+0652) → تمت إزالتها
ZWJ (U+200D) → تمت إزالته

:four: التنفيذ في PostgreSQL

  • إنشاء جدول text_normalization_map
  • استخدام regexp_replace أو سلاسل TRANSLATE لتحسين الأداء
  • اختياريًا، التنفيذ في PL/Python أو PL/v8 لدعم Unicode
  • تطبيع كل من المحتوى المخزن والاستعلامات الواردة باستخدام نفس المنطق

استراتيجية الفهرسة

  • تخزين normalized_conservative للفهرسة القياسية
  • تطبيع الاستعلامات باستخدام normalize_persian_arabic(query, 'permissive')
  • إذا كنت تستخدم البحث المتساهل، فيجب أن يتطابق الفهرس مع نفس الملف الشخصي
  • اختياريًا، تخزين كلا الإصدارين للمقارنة المتبادلة

:five: مثال تجزئة Ruby (لـ Discourse)

NORMALIZATION_MAP = {
  "ك" => "ک",
  "ي" => "ی",
  "ى" => "ی",
  "أ" => "ا",
  "إ" => "ا",
  "ٱ" => "ا",
  "ؤ" => "و",
  "ئ" => "ی",
  "ة" => "ه",
  "ۀ" => "هٔ",
  "۴" => "4",
  "٤" => "4",
  "\u200C" => "", # ZWNJ
  "\u200D" => "", # ZWJ
}

:six: ملاحظات الأداء والعملية

  1. طبق NFKC في طبقة التطبيق (على سبيل المثال، Ruby unicode_normalize(:nfkc))
  2. استخدم فهارس منفصلة لملفات التعريف المحافظة مقابل المتساهلة
  3. تجنب التعيينات الإجبارية للأحرف الحساسة دلاليًا (مثل الهمزة، التاء المربوطة) ما لم يتم تكوينها صراحةً
  4. قم بإجراء اختبارات A/B على بيانات المنتدى الفعلية لقياس معدل الإصابات والإيجابيات الخاطئة
  5. وثق كل تعيين مع الأساس المنطقي والأمثلة
  6. حدد اختبارات الوحدة في كل من Ruby و SQL لكل تعيين

:seven: التوصية النهائية

  • استخدم Unicode NFKC كأساس
  • قم بتوسيعه باستخدام طبقة تعيين مخصصة
  • حافظ على ملفات تعريف مزدوجة للتخزين والبحث
  • نفذ التطبيع في كل من طبقات التطبيق وقاعدة البيانات
  • وثق واختبر كل تعيين
  • أنشئ فهارس مناسبة (GIN + to_tsvector) على الأعمدة المطبعة