البحث المتجهي
درس 4 من 10
4البحث المتجهي
الصورة الكبيرةالبحث المتّجهي يحوّل كل مستند إلى متّجه مرة واحدة، ثم يحوّل السؤال إلى متّجه ويبحث عن أقرب المستندات إليه. يجد «متى أستلم بدل السفر؟» في فقرة عن «صرف البدلات» دون كلمة مشتركة تقريبًا. وفي هذا الدرس ترى أيضًا كيف يُسرَّع البحث في ملايين المتّجهات، وأين يخدعك التشابه.
ماذا تشاهد في الرسم: الزاوية الضيقة بين السؤال ومستند البدلات تعني تشابهًا 0.90، ومستند ترحيل الإجازة على زاوية أوسع بـ0.64. لاحظ أن الثاني لا يجيب أصلًا: التشابه يقرّب ما يتشابه في الشكل والاستخدام، لا ما يجيب السؤال.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
البحث العادي يجد الكلمة نفسها فقط، فلو سألت عن «السيارة» لن يجد «المركبة». البحث المتجهي يحوّل كل نص إلى عنوان على خريطة المعنى، ثم يجد أقرب النصوص إلى سؤالك على الخريطة، حتى لو لم تتشارك معه كلمة واحدة.
تضمّن المقاطع بنموذج تضمين وتخزّنها في pgvector بعمود vector(n)، ثم تبحث بأقرب الجيران بمسافة جيبية <=> مع فهرس تقريبي HNSW. التشابه يقرّب الموضوع لا الحقيقة، فقد يعيد مقطعًا عن الموضوع نفسه بأرقام خاطئة أو منفية.
قبل أن تبدأ
🧭التضمينات
تعرف الجيب تمام، وأن المتّجهات المطبّعة يكفيها ضرب نقطي.
🧩التجميع وتخفيض الأبعاد
تعرف k-means، وهي الفكرة التي يقوم عليها فهرس IVF.
الفكرة الجوهرية
البحث المتّجهي يجد المعنى حتى بلا كلمات مشتركة. تبني في هذا الدرس بحثًا كاملًا، ثم ترى أين يخدعك التشابه، ثم تسرّعه بفهرس IVF، ثم تكتبه في pgvector.
1من نص إلى متّجه إلى نتيجة
كل مستند يتحوّل إلى متّجه مرة واحدة عند الفهرسة. وعند البحث يتحوّل السؤال إلى متّجه، والنتائج هي المستندات الأعلى تشابهًا بالجيب تمام.
انظر إلى الرسم: العمل الثقيل يحدث مرة، والبحث مع كل سؤال ضرب نقطي واحد.
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
docs = [
"يحق للموظف إجازة سنوية مدتها واحد وعشرون يوم عمل في السنة.",
"تقدم طلبات الإجازة السنوية قبل أسبوعين على الأقل من موعدها.",
"الإجازة المرضية تحتاج تقريرا طبيا إذا زادت عن يومين متتاليين.",
"يوافق المدير المباشر على الإجازة المرضية والسنوية.",
"يمكن ترحيل عشرة أيام من الإجازة السنوية إلى السنة التالية فقط.",
"تصرف بدلات السفر خلال عشرة أيام من تقديم الفواتير.",
"يجب حجز تذاكر السفر عبر بوابة الحجز الداخلية.",
"ساعات العمل من الثامنة صباحا حتى الرابعة عصرا.",
"العمل عن بعد مسموح يومين في الأسبوع بموافقة المدير.",
"رمز الخطأ E-1043 في بوابة الحجز يعني انتهاء الجلسة.",
]
# a stand-in for an embedding model: character n-grams compressed to 8 dimensions
tfidf = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 4))
svd = TruncatedSVD(n_components=8, random_state=0)
vectors = svd.fit_transform(tfidf.fit_transform(docs))
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)
def search(query, k=3):
q = svd.transform(tfidf.transform([query]))[0]
q /= np.linalg.norm(q)
scores = vectors @ q
top = np.argsort(-scores)[:k]
return [(int(i), round(float(scores[i]), 2)) for i in top]
for query in ["كم يوم إجازة سنوية لي؟", "متى أستلم بدل السفر؟"]:
print(query)
for i, s in search(query):
print(f" {s:.2f} [{i}] {docs[i]}")
السؤال الأول وجد مستند الإجازة السنوية بتشابه 0.89، والثاني مستند البدلات بـ0.90. لاحظ المرتبة الثانية في السؤال الثاني: مستند ترحيل الإجازة (0.64) لأنه يشترك مع مستند البدلات في «عشرة أيام». التشابه يقرّب ما يتشابه في الشكل أو الاستخدام، لا ما يجيب عن السؤال بالضرورة.
2أين يخدعك التشابه
التشابه يقرّب ما يتشابه شكلًا أو استعمالًا، لا ما يجيب بالضرورة. انظر إلى الرسم: لسؤال الإجازة السنوية جاء مستند الترحيل ثانيًا بـ0.74، وهو لا يجيب عن عدد الأيام.
والتشابه يخلط الأضداد، ويضعف مع الرموز والأرقام والأسماء النادرة مثل E-1043. لهذا نضيف البحث بالكلمات في درس البحث الهجين.
3فهرس تقريبي: IVF
البحث الشامل يقارن السؤال بكل متّجه. دقيق تمامًا، ويكفي حتى نحو مئة ألف مستند. بعدها تحتاج فهرسًا يفحص جزءًا صغيرًا ويجد أغلب الأقرب.
انظر إلى الرسم: المتّجهات تتجمّع حول مراكز. عند البحث تفحص المجموعات الأقرب إلى السؤال فقط، وعددها اسمه nprobe.
عشرون ألف متّجه بـ64 بعدًا، مقسّمة إلى 100 مجموعة. نقيس الاسترجاع عند 10 مقارنة بالبحث الشامل، مع عدد المجموعات المفحوصة.
import numpy as np
from sklearn.cluster import KMeans
rng = np.random.default_rng(0)
centers = rng.normal(size=(50, 64))
base = centers[rng.integers(0, 50, 20000)] + rng.normal(scale=1.5, size=(20000, 64))
base /= np.linalg.norm(base, axis=1, keepdims=True)
queries = base[rng.choice(20000, 100, replace=False)] + rng.normal(scale=0.05, size=(100, 64))
def exact(q, k=10):
return set(np.argsort(-(base @ q))[:k])
km = KMeans(n_clusters=100, n_init=1, random_state=0).fit(base)
lists = [np.where(km.labels_ == c)[0] for c in range(100)]
def ivf(q, k=10, nprobe=5):
near = np.argsort(-(km.cluster_centers_ @ q))[:nprobe]
cand = np.concatenate([lists[c] for c in near])
return set(cand[np.argsort(-(base[cand] @ q))[:k]]), len(cand)
truth = [exact(q) for q in queries]
for nprobe in [1, 5, 20]:
recall, scanned = 0, 0
for q, t in zip(queries, truth):
found, n = ivf(q, nprobe=nprobe)
recall += len(found & t) / 10
scanned += n
print(f"nprobe={nprobe:2}: recall@10 = {recall / 100:.2f}, vectors scanned = {scanned / 100 / 20000:.0%} of all")
بفحص مجموعة واحدة (1% من المتّجهات) نجد 63% من الأقرب الحقيقيين. وبخمس مجموعات (5%) نجد 94%، أي عشرين ضعف السرعة تقريبًا مقابل خسارة 6%. هذا هو المقبض الذي ستضبطه في أي قاعدة متّجهات: في pgvector اسمه ivfflat.probes، وفي HNSW اسمه ef_search. اضبطه بقياس الاسترجاع على أسئلتك، لا بالقيمة الافتراضية.
HNSW طريقة أخرى: يبني رسمًا من الجيران يقفز البحث عبره. سريع ودقيق جدًّا، ويستهلك ذاكرة أكثر.
4المتّجهات داخل Postgres
pgvector إضافة لـPostgreSQL تضيف عمود متّجهات وعمليات مسافة وفهارس. فتبقى مستنداتك وصلاحياتها ومتّجهاتها في قاعدة واحدة، وهي بداية ممتازة لأغلب المشاريع.
هذا ما ستكتبه فعليًّا في Postgres. يحتاج قاعدة بيانات بإضافة pgvector، فهو للقراءة هنا.
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE chunks ( id bigserial PRIMARY KEY, doc_id text NOT NULL, body text NOT NULL, model text NOT NULL, -- which embedding model made this vector embedding vector(384) NOT NULL ); CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops); -- $1 is the question's vector; <=> is cosine distance, so smaller is closer SELECT doc_id, body, 1 - (embedding <=> $1) AS similarity FROM chunks WHERE model = 'multilingual-e5-small' ORDER BY embedding <=> $1 LIMIT 5;
<=> مسافة لا تشابه، فنرتّب تصاعديًّا ونحوّلها بـ1 − للعرض. وعمود model يمنع خلط متّجهات من نموذجين. وأهم ميزة لـpgvector: تستطيع إضافة WHERE على الصلاحيات أو القسم أو التاريخ في نفس الاستعلام، وهذا ما نعتمد عليه في درس حدود النظام.
كيف يعمل، خطوة بخطوة: كيف يجد فهرس IVF أقرب المستندات
عند الفهرسة تقسّم كل المتّجهات إلى مجموعات بـk-means، 100 مجموعة في مثال العشرين ألفًا.
عند البحث تقارن متّجه السؤال بمراكز المجموعات فقط، وتختار أقرب nprobe منها.
تقارن السؤال بالمتّجهات داخل تلك المجموعات وحدها، وتعيد الأعلى تشابهًا.
تقيس الاستدعاء مقابل البحث الشامل على أسئلتك، وتضبط nprobe حتى يرضيك التوازن بين الدقة والسرعة.
مثال محلول
عشرة مستندات قصيرة من سياسات شركة. نحوّلها إلى متّجهات، ونطبّعها، ونبحث بالضرب النقطي. لأن نماذج التضمين الحقيقية لا تعمل في المتصفّح، نستخدم بديلًا صغيرًا: مقاطع حروف مضغوطة إلى ثمانية أبعاد. الفكرة والكود حول البحث هما نفسهما مع أي نموذج.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| كل مشروع بحث يحتاج قاعدة بيانات متّجهات متخصّصة. | حتى مئة ألف مستند تقريبًا يكفي البحث الشامل بضرب مصفوفة واحد، وpgvector يبقي المتّجهات بجانب بياناتك وصلاحياتها. |
| الفهرس التقريبي يعطي نفس نتائج البحث الشامل. | يتنازل عن جزء من الدقة: مجموعة واحدة وجدت 63% من الأقرب الحقيقيين، وخمس مجموعات 94%. |
| التشابه العالي يعني أن المستند يجيب السؤال. | مستند الترحيل أخذ 0.64 لسؤال البدلات لأنه يشارك «عشرة أيام» فقط. التشابه يقيس القرب، لا الإجابة. |
جرّب بنفسك
رفعت nprobe من 5 إلى 20 في فهرس IVF. ماذا يحدث؟
تفحص مجموعات أكثر، فتقارن متّجهات أكثر: الاسترجاع يرتفع والسرعة تنخفض. هذا ما رأيته: من 94% إلى 98% بأربعة أضعاف العمل.
لماذا نخزّن اسم نموذج التضمين مع كل متّجه؟
كل نموذج فضاء مستقل. حين تنتقل لنموذج أفضل، هذا العمود يخبرك ما الذي لم يُعَد تضمينه بعد، ويمنع البحث من خلط الاثنين.
بطاقات مراجعة سريعة
تحقق من فهمك
عندك 5000 مستند فقط. هل تحتاج فهرس HNSW؟
غالبًا لا. 5000 متّجه بـ384 بعدًا مقارنتها كلها أجزاء من الألف من الثانية، ودقيقة 100%. الفهرس يضيف تعقيدًا وخسارة صغيرة في الدقة بلا فائدة تُذكر هنا. أضفه حين تقيس بطئًا حقيقيًّا.
في استعلام pgvector، لماذا نرتّب تصاعديًّا بـ<=> ثم نعرض 1 - (embedding <=> $1)؟
لأن <=> مسافة جيب تمام، والأصغر أقرب، فالترتيب التصاعدي يضع الأقرب أولًا. وطرحها من 1 يعيدها تشابهًا مألوفًا للعرض.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.