انتقل إلى المحتوى الرئيسي

الوحدة 4 — التضمينات وقواعد البيانات المتّجهية

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

ما التضمين؟

التضمين (embedding) دالّة تُنتج شعاعًا بحجم ثابت (768، أو 1024، أو 3072 بُعدًا في العادة) انطلاقًا من نصّ. الخاصية المطلوبة: نصّان قريبان في المعنى ينتجان شعاعين قريبين في الفضاء. المسافة المستعملة عادةً هي جيب تمام الزاوية، رياضيًّا:

cos(a,b)=abab\cos(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{\|\vec{a}\| \, \|\vec{b}\|}

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

اختيار نموذج التضمين: أربعة معايير

الخطأ الأشيع اختيار النموذج «المشهور» بلا فحص. القرار يفكَّك على أربعة محاور مستقلّة:

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

    • intfloat/multilingual-e5-large (من sentence-transformers، مفتوح)
    • BAAI/bge-m3 (مفتوح، بُعد 1024، يدعم أكثر من 100 لغة)
    • text-embedding-3-large من OpenAI (مغلق، بُعد 3072، جودة عالية)
    • cohere/embed-multilingual-v3.0 (مغلق، بُعد 1024)
  2. البُعد. أبعاد أعلى تعطي في العموم فروقًا أدقّ، لكنّها تكلّف ذاكرة وحسابًا أكثر. 384 يكفي غالبًا للمشاريع الصغيرة، و1024 قياس معقول للمشاريع المؤسّسية، و3072 مبرَّر إن لاحظتَ فقدان دقّة.

  3. الرخصة والاستضافة. نموذج مغلق مريح لكن يعني إرسال كلّ مقطع إلى مزوّد خارجي (وهذه إشكالية السرّية من الوحدة 1). نموذج مفتوح يُستضاف محلّيًا، فتُحسم قضيّة الخروج من المؤسّسة.

  4. الكلفة. مغلق: بضع سنتات لكلّ مليون رمز. مفتوح على GPU مؤسّسي: كلفة الجهاز. للحساب: 300 مستند × نحو 40 مقطعًا في المتوسّط × 400 رمزًا يعطي 4.8 مليون رمز للفهرسة الكاملة، ثم كلّ سؤال يستهلك بضع مئات من الرموز.

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("intfloat/multilingual-e5-large")

texts = [
"query: مدّة إشعار الاستقالة",
"passage: تنصّ المادّة 14 من اللائحة الداخلية على إشعار مدّته شهر...",
"passage: تحدَّد رسوم التسجيل السنوية بمبلغ...",
]
vectors = model.encode(texts, normalize_embeddings=True)
print(vectors.shape) # (3, 1024)

# تشابه جيب التمام يصير جداءً سُلَّميًّا بعد التطبيع
sim = vectors[0] @ vectors[1]
print(f"تشابه السؤال بالمقطع الأوّل: {sim:.3f}")

بادئتا query: وpassage: ليست حلية شكلية: نماذج E5 مدرَّبة عليها لتفريق دور السؤال عن دور المقطع، وحذفهما يخفض الأداء بعشرة نقاط أحيانًا.

قواعد البيانات المتّجهية

الشعاع وحده لا يكفي. نحتاج قاعدة تخزّن الأشعّة مع ما وراء بياناتها، وتردّ في مللي‌ثانية على «أعطني أقرب k مقاطع لهذا الشعاع». لن نُجري بحثًا خطّيًا على مئات الآلاف من الأشعّة عند كلّ سؤال.

الحلّ التقني الشائع: فهرس HNSW (Hierarchical Navigable Small World)، بنية رسم بيانيّ متعدّدة الطبقات تعطي بحثًا تقريبيًّا لكن سريعًا جدًّا. الثمن: البحث ليس مضمونًا العودة إلى أفضل k مطلقًا (ليس دقيقًا 100 %)، لكنّ الدقّة العملية أعلى من 98 % مع الإعدادات القياسية.

نظرة عابرة على الخيارات الشائعة:

القاعدةنموذج التشغيلاستعمال ملائم
Chromaمضمَّنة في العملية، تخزين على القرصمشاريع صغيرة إلى متوسّطة، تجريب سريع
pgvectorامتداد لـPostgreSQLمؤسّسة تملك بالفعل PostgreSQL وتريد SQL بجانب المتّجه
Pineconeخدمة مُدارةمشاريع كبيرة، إطلاق سريع، بيانات غير حسّاسة
Qdrantخادم مستقلّ، مفتوحأداء عالٍ وفلاتر متقدّمة
FAISSمكتبة، بلا خادممشاريع بحثية، لا يحفظ ما وراء البيانات ذاتيًّا
Milvus / Weaviateخوادم مؤسّسيةمقاييس كبيرة جدًّا

للفيل الأحمر (300 مستند، ربّما 15 ألف مقطع بعد التقطيع)، Chroma أو pgvector كافيان جدًّا. المشاريع التي تعلن الحاجة إلى Pinecone من اليوم الأوّل مبالغة عادةً.

import chromadb

client = chromadb.PersistentClient(path="./index")
collection = client.get_or_create_collection(
name="procedures",
metadata={"hnsw:space": "cosine"},
)

collection.add(
ids=[f"chunk-{i:05d}" for i in range(len(chunks))],
documents=[c.text for c in chunks],
embeddings=vectors.tolist(),
metadatas=[
{
"source": c.source,
"page": c.page,
"section": c.section,
"access_level": c.access_level,
"updated_at": c.updated_at,
}
for c in chunks
],
)

فلاتر ما وراء البيانات

النقطة الأكثر تجاهلًا في المشاريع الأولى. البحث المتّجهي يجد مقاطع «قريبة معنى»، لكنّه لا يعرف أنّ المستخدم لا يجب أن يرى المقاطع المصنَّفة RH، ولا أنّه يبحث فقط في مستندات المديرية المالية. الفلاتر تُطبَّق قبل حساب الجوار أو بعده حسب القاعدة:

results = collection.query(
query_embeddings=[query_vector.tolist()],
n_results=5,
where={
"$and": [
{"access_level": {"$in": ["public", user.role]}},
{"updated_at": {"$gte": "2024-01-01"}},
]
},
)

نسيان فلتر الأذونات هنا ليس عيبًا فنيًّا، بل تسرّبًا للبيانات: مساعد الشؤون الإدارية يجيب موظّفًا عاديًّا عن راتب زميله. لا بدّ من اختبار قسريّ لهذه الحالة قبل الإطلاق.

التحديث والحذف: قاعدة حيّة، لا ملفّ مسطَّح

المستندات تُعدَّل: صدور نسخة جديدة من إجراء، إلغاء مذكّرة. على الفهرس أن يعكس ذلك.

  • إضافة: collection.add(...) مع معرّفات جديدة.
  • حذف: collection.delete(ids=[...]) أو where={"source": path} لحذف كلّ مقاطع مستند بأكمله.
  • تحديث: أبسط استراتيجية: احذف كلّ المقاطع المرتبطة بالمصدر ثم أعد إضافتها.

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

تغيير نموذج التضمين يعني إعادة كلّ شيء

الأشعّة الناتجة من نموذجين مختلفين لا تُقارَن، لأنّ فضاءَيهما مستقلّان. تغيير النموذج من bge-m3 إلى text-embedding-3-large يفرض إعادة تضمين كامل لكلّ المقاطع. لا يقدر أن يعمل نصف نصف: يجب اتّخاذ القرار قبل بناء فهرس كبير.

فحص العقل: أقرب مقطع لنفسه

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

في الخلاصة

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

الوحدة التالية: البحث نفسه — كثيف بالمتّجهات، ومعجمي بـBM25، ثم هجين بينهما، وكيف نعالج الأسئلة القصيرة والأسماء المختصرة التي يُخفق فيها البحث الدلاليّ.