cd ../back to blog
$Guide//June 4, 2026//8 min read

الـ Embeddings والبحث الدلالي: دليل عملي عبر Brievio

ولّد الـ embeddings عبر نقطة نهاية متوافقة مع OpenAI، قيّمها بـ cosine similarity، خزّنها في pgvector، واربطها بخط RAG — مع أعداد توكنات صادقة والتحفظات التي تؤذيك فعلاً.

البحث بالكلمات المفتاحية يطابق السلاسل النصية. أما البحث الدلالي فيطابق المعنى — عبارة "I forgot my login" تعثر على المقال المعنون "Resetting your password" رغم أنهما لا يشتركان في أي كلمة. الآلية وراء ذلك هي الـ embeddings: يحوّل النموذج كل قطعة نص إلى متجه من بضعة آلاف من الأرقام، فتقع النصوص المتقاربة المعنى قريبة بعضها من بعض في ذلك الفضاء. ولّد تلك المتجهات مرة واحدة، خزّنها، وبذلك يمكنك ترتيب أي شيء حسب صلته — وهذا هو نصف الاسترجاع في كل نظام RAG.

يتيح Brievio نقطة النهاية /v1/embeddings بوصفها بديلاً متوافقاً مع OpenAI، فيكون نداء الـ SDK مطابقاً لما تكتبه أصلاً — يتغيّر base_url فقط. الـ embeddings زهيدة الكلفة، وتدفع مقابل أعداد توكنات صادقة، والنداءات الفاشلة من فئة 4xx/5xx لا تكلّفك شيئاً. هذا المقال هو المسار العملي: ولّد المتجهات، قيّمها بـ cosine similarity، خزّنها واستعلم عنها عبر pgvector، ثم اربط النتيجة بخط استرجاع — مع التحفظات التي تؤذيك فعلاً.

توليد الـ embeddings

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

embed.py
# توليد الـ embeddings عبر OpenAI SDK — النداء نفسه، يتغير base_url فقط.
from openai import OpenAI

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
)

EMBED_MODEL = "text-embedding-3-large"   # اختر نموذجاً واحداً والتزم به

def embed(texts: list[str]) -> list[list[float]]:
    # ادفع حتى بضع مئات من المدخلات في كل نداء — تقليل كبير في الرحلات
    # وبالسعر نفسه لكل توكن. الاستجابة تحافظ على ترتيب المدخلات.
    resp = client.embeddings.create(model=EMBED_MODEL, input=texts)
    return [row.embedding for row in resp.data]

vecs = embed(["How do I reset my password?", "Where is my invoice?"])
print(len(vecs), "x", len(vecs[0]))     # 2 x 3072  (البُعد يعتمد على النموذج)

# يُبلَّغ عن الاستهلاك بصدق — الـ embeddings تُقاس بالتوكن كأي نداء آخر
print(resp.usage.prompt_tokens, "tokens billed")

طول المتجه (أي بُعده) يحدّده النموذج — وهو غالباً 768 أو 1536 أو 3072. الأبعاد الأعلى تلتقط فروقاً دقيقة أكثر بقليل لكنها أغلى في التخزين والمقارنة. وأهم قاعدة على الإطلاق: اختر نموذج embedding واحداً ولا تخلط أبداً. متجه من نموذج ومتجه من آخر يعيشان في فضاءين مختلفين غير متوافقين — ومقارنتهما تنتج ضوضاء. راجع كتالوج النماذج الحي لمعرفة نماذج الـ embedding المتاحة وأبعادها؛ وتسعير كل نموذج موجود في صفحة الأسعار.

التقييم بـ cosine similarity

لإيجاد أقرب التطابقات، تقارن متجه الاستعلام بمتجهاتك المخزّنة. والمقياس المعياري هو cosine similarity: إذ يقيس الزاوية بين متجهين ويتجاهل طولهما، فيهتم بالاتجاه (المعنى) لا بالمقدار. القيمة 1.0 تعني الاتجاه نفسه، و0 تعني عدم وجود علاقة:

search.py
# البحث الدلالي = حوّل الاستعلام إلى متجه، قارنه بكل متجه مخزّن،
# وأعد الأقرب. دالة التقييم هي cosine similarity.
import numpy as np

def cosine(a: np.ndarray, b: np.ndarray) -> float:
    # 1.0 = الاتجاه ذاته، 0 = لا علاقة، -1 = اتجاه معاكس.
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

# كثير من نماذج الـ embedding تعيد متجهات بطول وحدة أصلاً. عندها
# تختزل cosine similarity إلى dot product بسيط — أرخص على نطاق واسع:
def cosine_unit(a: np.ndarray, b: np.ndarray) -> float:
    return float(a @ b)

query = np.array(embed(["I forgot my login"])[0])
corpus = np.array(embed(documents))                 # (N, dim)

scores = corpus @ query / (
    np.linalg.norm(corpus, axis=1) * np.linalg.norm(query)
)
top = scores.argsort()[::-1][:5]                    # فهارس أفضل 5 تطابقات
for i in top:
    print(round(float(scores[i]), 3), documents[i][:60])

ملاحظتان عمليتان. أولاً، كثير من نماذج الـ embedding تعيد أصلاً متجهات بطول وحدة — وعندها تكون cosine similarity مجرد dot product، وهذا ما يجعل البحث الشامل بالقوة الغاشمة على عشرات الآلاف من المتجهات سريعاً بما يكفي للاستغناء عن قاعدة بيانات تماماً. ثانياً، حلقة القوة الغاشمة مقبولة حتى نحو بضع عشرات الآلاف من المتجهات. وبعد ذلك يصبح مسح كل متجه في كل استعلام بطيئاً، وتحتاج إلى متجر متجهات حقيقي مزوّد بفهرس.

التخزين والاستعلام عبر pgvector

إذا كانت بياناتك تعيش أصلاً في Postgres، فلست بحاجة إلى قاعدة بيانات متجهات منفصلة — إذ تضيف إضافة pgvector نوع عمود vector ومعاملات الجار الأقرب. تحوّل كل مقطع إلى embedding مرة واحدة، تخزّن المتجه بجوار نصه، وتترك Postgres ينفّذ البحث:

store.sql
-- يحوّل pgvector قاعدة Postgres إلى متجر متجهات. فعّله مرة واحدة، ثم خزّن
-- embedding كل مقطع إلى جانب نصه وبياناته الوصفية.
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
    id        bigserial PRIMARY KEY,
    doc_id    text NOT NULL,
    content   text NOT NULL,
    embedding vector(3072)            -- يجب أن يطابق بُعد نموذجك
);

-- فهرس الجار الأقرب التقريبي. <=> هو cosine distance في pgvector
-- (0 = الأقرب). ابنِ الفهرس بعد التحميل المجمّع، لا قبله.
CREATE INDEX ON chunks
    USING hnsw (embedding vector_cosine_ops);

-- الاسترجاع: مرّر embedding الاستعلام كوسيط ($1) وخذ الصفوف
-- الأقرب. الترتيب تصاعدياً بالمسافة = الأكثر تشابهاً أولاً.
SELECT id, doc_id, content, 1 - (embedding <=> $1) AS similarity
FROM   chunks
WHERE  doc_id = ANY($2)               -- ترشيح مسبق اختياري بالبيانات الوصفية
ORDER  BY embedding <=> $1
LIMIT  5;

المعامل <=> هو cosine distance (0 = متطابق)، لذا فإن 1 - (embedding <=> $1) يعيد لك درجة تشابه. وفهرس hnsw يجعل البحث تقريبياً لكن سريعاً — وبالنسبة لمعظم أعباء الاسترجاع تكون مقايضة الاستدعاء الضئيلة غير محسوسة، وتبنيه بعد التحميل المجمّع لا قبله أبداً. وجملة WHERE الاختيارية هي القوة الخارقة الهادئة: رشّح حسب المستأجر أو المستند أو اللغة أو التاريخ قبل بحث المتجهات، حتى لا تسترجع أبداً جاراً غير مسموح للمستخدم برؤيته.

ربطه بـ RAG

التوليد المعزّز بالاسترجاع خطوتان متشابكتان: بحث دلالي لإيجاد السياق ذي الصلة، ثم نموذج توليد يجيب بالاعتماد على ذلك السياق. جانب الاسترجاع هو كل ما سبق. أما جانب التوليد فهو chat completion عادي — وهنا تقرن نموذج embedding زهيداً بنموذج استدلال قوي:

  • قطّع، ثم حوّل إلى embedding. قسّم المستندات إلى مقاطع بحجم بضع مئات من التوكنات مع قليل من التداخل، حوّل كلاً منها إلى embedding، وخزّنها. التقطيع هو الذراع التي تقصّر معظم الفرق في الاستثمار فيها: المقاطع الكبيرة جداً تدفن الإجابة في الضوضاء؛ والصغيرة جداً تفقد السياق الذي يمنحها معناها. اضبطه على بياناتك أنت.
  • استرجع وقت الاستعلام. حوّل سؤال المستخدم إلى embedding بالنموذج نفسه، اسحب أفضل 3 إلى 8 مقاطع، والصق نصها في الـ prompt بوصفه سياقاً.
  • ولّد الإجابة. أرسل ذلك السياق مع السؤال إلى نموذج قادر — Claude أو Gemini عبر نقطة نهاية Brievio نفسها — ووجّهه ليجيب من السياق المقدّم فقط وأن يستشهد بالمقطع الذي استخدمه.

الاقتصاد ينجح لأن نصفي العملية لهما نقطتا سعر شديدتا التباين. تحويل كامل مجموعتك النصية إلى embeddings مهمة دفعية لمرة واحدة منخفضة الكلفة؛ وإعادة التحويل لا تحدث إلا عند تغيّر المحتوى. وكلفة كل استعلام تهيمن عليها خطوة التوليد لا الاسترجاع — وهذا بالضبط سبب أن تقنيات ضبط التكلفة في دليل تحسين تكلفة واجهة الذكاء الاصطناعي (التخزين المؤقت للتعليمات الثابتة في الـ prompt، وضع سقف للمخرجات، اختيار النموذج المناسب لكل مهمة) تحرّك المؤشر أكثر بكثير من أي شيء في جانب الـ embedding.

التحفظات التي تؤذيك فعلاً

  • المتجهات غير قابلة للتبادل بين النماذج. إذا بدّلت نموذج الـ embedding وجب عليك إعادة تحويل المجموعة النصية بأكملها — فمتجهات الاستعلام والمتجهات المخزّنة يجب أن تأتي من النموذج نفسه، وإلا كانت الدرجات بلا معنى. تعامل مع اختيار النموذج بوصفه قراراً على مستوى المخطط.
  • البُعد مقايضة بين التخزين والسرعة. المتجه بـ 3072 بُعداً يساوي أربعة أضعاف بايتات المتجه بـ 768 بُعداً وأبطأ في المقارنة. والأكبر ليس بالضرورة أفضل لمهمتك — قِس الاستدعاء على استعلاماتك أنت قبل أن تدفع مقابل أكبر نموذج.
  • جودة التقطيع تتفوق على جودة النموذج. معظم إجابات RAG السيئة تعود إلى مقاطع سيئة، لا إلى نموذج embedding ضعيف. احترم بنية المستند — قسّم على العناوين والفقرات، لا على عدّ أعمى للأحرف.
  • البحث الدلالي قد يفوّت المصطلحات الحرفية. أرقام المنتجات (SKU) ورموز الأخطاء والأسماء تطابقها أحياناً البحث بالكلمات المفتاحية بشكل أفضل. وعملياً، أقوى الأنظمة هجينة: تجمع بين تشابه المتجهات ودرجة كلاسيكية من نوع full-text أو BM25.
  • للـ embeddings حدّ سياق أيضاً. النص الذي يتجاوز حد إدخال النموذج يُقتطع بصمت — عندها يمثّل "الـ embedding" الخاص بك الجزء الأول فقط من مقطع مفرط الطول. أبقِ المقاطع تحت الحد براحة.

الخلاصة الملموسة

البحث الدلالي أربعة أجزاء متحركة: نموذج embedding، ومقياس تشابه، ومتجر، واستراتيجية تقطيع. اختر نموذج embedding واحداً والتزم به؛ استخدم cosine similarity؛ ابدأ بالقوة الغاشمة عبر NumPy وانتقل إلى فهرس HNSW في pgvector حين تتجاوز مجموعتك حدود مسح واحد؛ وأنفق ميزانية الضبط على التقطيع، فهناك تُكسب جودة الاسترجاع أو تُخسر. وعبر Brievio، نداء الـ embedding هو OpenAI SDK الذي تعرفه أصلاً بسطر واحد متغيّر، يُقاس على أعداد توكنات صادقة، ويقع إلى جانب نماذج Claude وGemini التي ستستخدمها في خطوة التوليد — مفتاح واحد، وbase_url واحد، وخط RAG بأكمله. مرجع نقطة النهاية وبداية سريعة قابلة للتشغيل موجودان في الوثائق.