الوحدة 4 — التضمينات وقواعد البيانات المتّجهية
الوحدة السابقة أعطتنا مقاطع نصّية ملائمة الحجم. نحوّلها الآن إلى أشعّة عددية تلتقط المعنى، ونخزّنها في فهرس يُتيح البحث في مئات آلاف المقاطع في أقلّ من عشر ميليثانية. الخيارات كثيرة والفوارق حقيقية، فلا بدّ من مبدأ اختيار واضح.
ما التضمين؟
التضمين (embedding) دالّة تُنتج شعاعًا بحجم ثابت (768، أو 1024، أو 3072 بُعدًا في العادة) انطلاقًا من نصّ. الخاصية المطلوبة: نصّان قريبان في المعنى ينتجان شعاعين قريبين في الفضاء. المسافة المستعملة عادةً هي جيب تمام الزاوية، رياضيًّا:
قيمة قريبة من 1 تعني تشابهًا عاليًا، وقيمة قريبة من 0 تعني استقلالًا دلاليًّا. الفكرة الجوهرية: تعلّم النموذج فضاءً حيث تكون الأشياء المتقاربة معنى متقاربة هندسة.
اختيار نموذج التضمين: أربعة معايير
الخطأ الأشيع اختيار النموذج «المشهور» بلا فحص. القرار يفكَّك على أربعة محاور مستقلّة:
-
اللغة. نموذج مُدرَّب على الإنجليزية فقط سيؤدّي أداءً ضعيفًا على العربية. للفيل الأحمر (مستندات فرنسية وعربية أحيانًا مختلطة) نحتاج نموذجًا متعدّد اللغات. المرشّحون العمليون:
intfloat/multilingual-e5-large(منsentence-transformers، مفتوح)BAAI/bge-m3(مفتوح، بُعد 1024، يدعم أكثر من 100 لغة)text-embedding-3-largeمن OpenAI (مغلق، بُعد 3072، جودة عالية)cohere/embed-multilingual-v3.0(مغلق، بُعد 1024)
-
البُعد. أبعاد أعلى تعطي في العموم فروقًا أدقّ، لكنّها تكلّف ذاكرة وحسابًا أكثر. 384 يكفي غالبًا للمشاريع الصغيرة، و1024 قياس معقول للمشاريع المؤسّسية، و3072 مبرَّر إن لاحظتَ فقدان دقّة.
-
الرخصة والاستضافة. نموذج مغلق مريح لكن يعني إرسال كلّ مقطع إلى مزوّد خارجي (وهذه إشكالية السرّية من الوحدة 1). نموذج مفتوح يُستضاف محلّيًا، فتُحسم قضيّة الخروج من المؤسّسة.
-
الكلفة. مغلق: بضع سنتات لكلّ مليون رمز. مفتوح على 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
],
)