llm-integration

Cosine Similarity مقابل Dot Product ل

٣٠ يونيو ٢٠٢٦

Cosine Similarity vs Dot Product for Embeddings (2026)

يقيس تشابه جيب التمام (Cosine similarity) الزاوية بين متجهين؛ بينما يأخذ حاصل الضرب النقطي (dot product) مقدار المتجهين (magnitude) في الاعتبار أيضًا. بالنسبة للتضمينات (embeddings) التي تم تطبيعها (normalized) بالفعل إلى الطول 1 — مثل تضمينات OpenAI — يعطي كلاهما ترتيبًا متطابقًا، لذا اختر جيب التمام عندما يهمك الاتجاه فقط، وحاصل الضرب النقطي عندما يكون للمقدار معنى.

ملخص

تتلخص مسألة "تشابه جيب التمام مقابل حاصل الضرب النقطي للتضمينات" في حقيقة واحدة: عندما تكون المتجهات بطول الوحدة (unit length)، فإن حاصل الضرب النقطي هو نفسه تشابه جيب التمام. تُرجع OpenAI تضمينات مطبعة إلى الطول 1، لذا فإن جيب التمام، وحاصل الضرب النقطي، وحتى المسافة الإقليدية (Euclidean distance) كلها ترتب النتائج بنفس الطريقة1. لا يهم المقياس إلا عندما لا تكون المتجهات مطبعة — حينها يتجاهل جيب التمام المقدار، بينما يتأثر به حاصل الضرب النقطي والمسافة الإقليدية. يوفر لك هذا الدليل المعادلات، وأكواد TypeScript بدون مكتبات خارجية للمقاييس الثلاثة، ومثالاً عمليًا يوضح متى تختلف النتائج، وماذا تستخدم OpenAI و Cohere و pgvector فعليًا.

ما ستتعلمه

  • المعادلات الدقيقة لحاصل الضرب النقطي، وتشابه جيب التمام، والمسافة الإقليدية
  • تطبيق المقاييس الثلاثة باستخدام TypeScript بدون مكتبات خارجية
  • لماذا يتفق تشابه جيب التمام وحاصل الضرب النقطي في التضمينات المطبعة (مع التحقق من ذلك بتشغيل الكود)
  • هل تحتاج إلى تطبيع التضمينات — ومتى يغير ذلك إجابتك
  • متى يكون المقدار مهمًا، مما يجعل حاصل الضرب النقطي أو المسافة الإقليدية الخيار الأفضل
  • ماذا تستخدم OpenAI و Cohere و pgvector، وكيف تختار المقياس المناسب لنموذجك

ما الفرق بين تشابه جيب التمام وحاصل الضرب النقطي؟

يقوم حاصل الضرب النقطي بجمع نواتج ضرب العناصر المتقابلة في متجهين. أما تشابه جيب التمام فهو نفس حاصل الضرب النقطي مقسومًا على طول كلا المتجهين، مما يلغي تأثير المقدار ويترك الزاوية بينهما فقط. لذا، يتأثر حاصل الضرب النقطي بكل من الاتجاه والمقدار، بينما يتأثر تشابه جيب التمام بالاتجاه فقط2.

بصيغة رياضية للمتجهين a و b:

  • حاصل الضرب النقطي: a · b = a₁b₁ + a₂b₂ + ... + aₙbₙ
  • المقدار (L2 norm): ‖a‖ = √(a · a)
  • تشابه جيب التمام: (a · b) / (‖a‖ · ‖b‖)، والذي يساوي cos θ
  • المسافة الإقليدية (L2): ‖a − b‖ = √((a₁−b₁)² + ... + (aₙ−bₙ)²)

تتراوح قيمة تشابه جيب التمام من -1 (اتجاهات متعاكسة) مرورًا بـ 0 (متعامدة) إلى 1 (نفس الاتجاه)2. ولأن حاصل الضرب النقطي يساوي ‖a‖ · ‖b‖ · cos θ، فإنه يشترك في الإشارة مع جيب التمام ولكنه يتأثر بالمقادير — فالمتجهان اللذان يشيران إلى نفس الاتجاه يحصلان على درجة أعلى إذا كانا أطول.

كيف تحسب تشابه جيب التمام وحاصل الضرب النقطي والمسافة الإقليدية في TypeScript؟

لا تحتاج إلى مكتبة. المقاييس الثلاثة عبارة عن بضعة أسطر من العمليات الحسابية على number[]. إليك وحدة برمجية واحدة بدون مكتبات خارجية:

// similarity.ts — dependency-free vector similarity for embeddings
export type Vec = number[];

export function dot(a: Vec, b: Vec): number {
  if (a.length !== b.length) {
    throw new Error(`vectors must be the same length: ${a.length} vs ${b.length}`);
  }
  let sum = 0;
  for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
  return sum;
}

export function norm(a: Vec): number {
  return Math.sqrt(dot(a, a));
}

export function cosineSimilarity(a: Vec, b: Vec): number {
  const denom = norm(a) * norm(b);
  if (denom === 0) return 0; // a zero vector has no direction
  return dot(a, b) / denom;
}

export function cosineDistance(a: Vec, b: Vec): number {
  return 1 - cosineSimilarity(a, b);
}

export function euclideanDistance(a: Vec, b: Vec): number {
  if (a.length !== b.length) {
    throw new Error(`vectors must be the same length: ${a.length} vs ${b.length}`);
  }
  let sum = 0;
  for (let i = 0; i < a.length; i++) {
    const d = a[i] - b[i];
    sum += d * d;
  }
  return Math.sqrt(sum);
}

export function normalize(a: Vec): Vec {
  const n = norm(a);
  if (n === 0) return a.slice();
  return a.map((x) => x / n);
}

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

هل تشابه جيب التمام وحاصل الضرب النقطي هما نفس الشيء؟

بالنسبة لـ المتجهات بطول الوحدة (المطبعة)، نعم — فهما متطابقان. عندما يكون ‖a‖ = ‖b‖ = 1، يصبح المقام في معادلة جيب التمام 1، لذا يختزل تشابه جيب التمام إلى مجرد حاصل الضرب النقطي12. هذا هو السبب في أن العديد من الأنظمة تقوم بالتطبيع مرة واحدة ثم تستخدم حاصل الضرب النقطي الأقل تكلفة في كل مكان.

إليك عرض توضيحي يثبت ذلك، بالإضافة إلى حالة يختلف فيها المقياسان:

import {
  dot,
  cosineSimilarity,
  euclideanDistance,
  normalize,
} from "./similarity.ts";

const r = (x: number) => Math.round(x * 1000) / 1000;

const query = [1, 0, 1];
const docA = [1, 0, 1]; // same direction, same magnitude
const docB = [0, 1, 0]; // orthogonal
const docC = [2, 0, 2]; // same direction as query, larger magnitude

for (const [name, d] of [["docA", docA], ["docB", docB], ["docC", docC]] as const) {
  console.log(
    `${name}: cos=${r(cosineSimilarity(query, d))} dot=${r(dot(query, d))} euclid=${r(euclideanDistance(query, d))}`,
  );
}

const nq = normalize(query);
const nc = normalize(docC);
console.log(`\nnormalized query  = [${nq.map(r).join(", ")}]`);
console.log(`normalized docC   = [${nc.map(r).join(", ")}]`);
console.log(`dot(normalized)   = ${r(dot(nq, nc))}`);
console.log(`cosineSimilarity  = ${r(cosineSimilarity(query, docC))}`);

قم بتشغيله باستخدام npx tsx demo.ts، أو مباشرة على Node 22.6+ باستخدام node --experimental-strip-types demo.ts (Node 23.6+ يشغل TypeScript بدون أي علامات)3. يتطلب تجريد الأنواع الأصلي وجود امتداد .ts الصريح في الاستيراد، ولهذا السبب يستورد المثال ./similarity.ts حرفيًا. المخرجات:

docA: cos=1 dot=2 euclid=0
docB: cos=0 dot=0 euclid=1.732
docC: cos=1 dot=4 euclid=1.414

normalized query  = [0.707, 0, 0.707]
normalized docC   = [0.707, 0, 0.707]
dot(normalized)   = 1
cosineSimilarity  = 1

انظر إلى docC. إنه يشير إلى نفس اتجاه الاستعلام ولكنه أطول بمرتين. يعتبره تشابه جيب التمام تطابقًا مثاليًا (1، متعادلًا مع docA) لأنه يتجاهل المقدار. ومع ذلك، فإن حاصل الضرب النقطي الخام يرتب docC فوق docA (4 مقابل 2) لأنه يكافئ المقدار الأكبر. بعد التطبيع، يصبح docC نفس متجه الاستعلام المطبع، ويتساوى حاصل ضربه النقطي مع تشابه جيب التمام تمامًا. هذا المثال الواحد هو ملخص لجدل "تشابه جيب التمام مقابل حاصل الضرب النقطي" بالكامل.

هل تحتاج إلى تطبيع التضمينات قبل حساب تشابه جيب التمام؟

لا — فدالة cosineSimilarity تقسم بالفعل على كلا المقدارين، لذا فهي تعمل على المتجهات الخام. التطبيع مهم لسبب آخر: فهو يسمح لك باستبدال حساب جيب التمام المكلف بحاصل ضرب نقطي بسيط دون تغيير الترتيب. قم بتطبيع كل متجه مرة واحدة عند الكتابة، وخزن متجه الوحدة، وستكون كل مقارنة لاحقة عبارة عن حاصل ضرب نقطي رخيص1.

هناك أيضًا تكافؤ أعمق للمتجهات المطبعة. تربيع المسافة الإقليدية لمتجهي وحدة يعطي 2 − 2·cos θ، لذا فإن المسافة الإقليدية وتشابه جيب التمام يتحركان جنبًا إلى جنب — فكلما اقتربت الزاوية، صغرت المسافة. يؤكد العرض التوضيحي ذلك: بالنسبة للاستعلام المطبع و docB، فإن مربع المسافة الإقليدية هو 2 و 2·(1 − cosine) هو أيضًا 2. هذا هو بالضبط سبب ذكر توثيق OpenAI أن تشابه جيب التمام والمسافة الإقليدية ينتجان ترتيبًا متطابقًا لتضميناتها (المطبعة بالفعل)1.

متى يغير التطبيع إجابتك؟ فقط عندما يكون المقدار نفسه يمثل إشارة. إذا كانت متجهاتك تشفر الأعداد أو التكرارات أو الثقة في طولها — وليس فقط الاتجاه — فإن التطبيع يتخلص من هذه المعلومات. بالنسبة لتضمينات النصوص الدلالية، الاتجاه هو الإشارة، لذا فإن التطبيع (أو استخدام جيب التمام) هو الخيار الافتراضي الآمن.

متى يجب استخدام حاصل الضرب النقطي بدلاً من تشابه جيب التمام؟

استخدم الضرب النقطي (dot product) عندما يكون للمقدار (magnitude) معنى وتريد أن يؤثر على الترتيب2. الحالة الكلاسيكية هي أنظمة التوصية المبنية على تحليل المصفوفات (matrix factorization): حيث يمكن أن يعني متجه العنصر الأطول نفس الموضوع ولكن لعنصر أكثر شعبية، وتريد أن تدفعه هذه الشعبية إلى الأعلى. أما جيب التمام (Cosine) فسيقوم بمحو هذا التأثير. يوضح مثال docC أعلاه الآلية — حيث قام الضرب النقطي بترقية المتجه الأطول بينما عامله جيب التمام كتعادل.

السبب الثاني هو السرعة البحتة. إذا كانت متجهاتك معيرة (normalized) بالفعل، فإن الضرب النقطي يعطي نفس ترتيب جيب التمام بجزء بسيط من المجهود، لأنك تتخطى حساب مقدارين لكل مقارنة. هذا هو بالضبط التحسين الذي تشير إليه OpenAI: مع التضمينات ذات الطول 1، "يمكن حساب تشابه جيب التمام بشكل أسرع قليلاً باستخدام مجرد ضرب نقطي"1.

متى يجب استخدام المسافة الإقليدية للتضمينات؟

الجأ إلى المسافة الإقليدية (L2) عندما يكون الموقع المطلق للمتجهات مهماً، وليس فقط زاويتها. إنها المسافة المستقيمة بين نقطتين، لذا فهي حساسة لكل من الاتجاه والمقدار2. وهي تتألق مع التضمينات التي تمثل قيمها كميات مقاسة أو أعداداً، ومع الترميزات الكلاسيكية مثل locality-sensitive hashing.

بالنسبة لتضمينات النصوص الحديثة القائمة على التعلم العميق، نادراً ما تكون المسافة الإقليدية هي الخيار الأول. توجيهات Pinecone صريحة: في معظم الحالات، تقوم بدمجها مع طرق ترميز المتجهات الأبسط مثل locality-sensitive hashing بدلاً من نماذج التعلم العميق2. وتذكر التكافؤ المذكور أعلاه — بمجرد تعيير المتجهات، تنتج المسافة الإقليدية وتشابه جيب التمام نفس الترتيب على أي حال، لذا لا توجد فائدة في الترتيب من اختيارها على جيب التمام.

ما هو مقياس المسافة الذي تستخدمه OpenAI و Cohere و pgvector؟

القاعدة العملية من Pinecone هي مطابقة المقياس مع المقياس الذي تم تدريب نموذج التضمين الخاص بك عليه2. النموذج الذي يرجع تضمينات معيرة (على سبيل المثال، all-MiniLM-L6-v2) يرتب بنفس الطريقة تحت جيب التمام أو الضرب النقطي، بينما النموذج الذي تم ضبطه للضرب النقطي (مثل msmarco-bert-base-dot-v5، الذي لا يعاير مخرجاته) يجب مقارنته بالضرب النقطي4. إليك ما يفعله المزودون الرئيسيون:

  • تقوم OpenAI بتعيير كل تضمين إلى الطول 1 افتراضياً — بما في ذلك بعد تقصيره باستخدام معامل dimensions — لذا فإن جيب التمام والضرب النقطي والإقليدية جميعها ترتب بشكل متطابق. تتبع نماذجهم text-embedding-3-small و text-embedding-3-large هذا النهج، وتوصي الوثائق باستخدام جيب التمام (المحسوب كضرب نقطي للسرعة)1.
  • مثال التضمينات الخاص بـ Cohere يقارن المتجهات باستخدام تشابه جيب التمام، مع حساب np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) مباشرة — وهي صيغة جيب التمام المدرسية — لنموذجها embed-v4.05.
  • pgvector، وهي إضافة Postgres، توفر عاملاً واحداً لكل مقياس. ولأن Postgres يدعم فقط مسح الفهرس التصاعدي، فإن <#> يرجع الناتج الداخلي السلبي6:
عامل pgvectorالمقياسملاحظات
<->المسافة الإقليدية (L2)المسافة المستقيمة
<=>مسافة جيب التمام1 − cosine similarity
<#>الناتج الداخلي السلبياضرب في -1 للحصول على الضرب النقطي
<+>مسافة L1 (تاكسي كاب)مجموع الفروق المطلقة

للحصول على تشابه جيب التمام في SQL، تطرح مسافة جيب التمام من واحد. توفر pgvector أيضاً وظيفة l2_normalize()، وتكرر وثائقها نصيحة OpenAI: بالنسبة للمتجهات المعيرة للطول 1 (مثل متجهات OpenAI)، الجأ إلى الناتج الداخلي للحصول على أفضل أداء6.

-- cosine similarity (1 - cosine distance), highest first
SELECT id, 1 - (embedding <=> '[0.12, -0.03, 0.88]') AS cosine_similarity
FROM items
ORDER BY embedding <=> '[0.12, -0.03, 0.88]'
LIMIT 5;

ما هو نطاق تشابه جيب التمام ومسافة جيب التمام؟

يتراوح تشابه جيب التمام من -1 إلى 1: حيث تعني 1 أن المتجهات تشير إلى نفس الاتجاه، وتعني 0 أنها متعامدة، وتعني -1 أنها تشير في اتجاهات متعاكسة2. تُعرف مسافة جيب التمام بأنها 1 − cosine similarity، لذا فهي تتراوح من 0 إلى 26. في الممارسة العملية، تقع العديد من تشابهات تضمين النصوص في النطاق الإيجابي، ولكن الحد الأدنى النظري هو -1، لذا لا تفترض أن الدرجات دائماً غير سالبة ما لم تكن متجهاتك كذلك.

الخلاصة

للبحث الدلالي (semantic search) عبر تضمينات النصوص الحديثة، اعتمد بشكل افتراضي على تشابه جيب التمام، وقم بتطبيع متجهاتك مرة واحدة، واستخدم الضرب النقطي (dot product) كبديل سريع — فهما يرتبان النتائج بشكل متطابق عندما تكون المتجهات بطول الوحدة. انتقل إلى الضرب النقطي الخام أو المسافة الإقليدية فقط عندما يكون مقدار المتجه إشارة حقيقية، كما هو الحال في نماذج التوصية. الرياضيات بسيطة بما يكفي لتنفيذها في ملف TypeScript مستقل، مما يجعلك متحكماً في حماية المتجهات الصفرية واتجاه الترتيب.

الخطوات التالية: تعرف على كيفية تأثير المشفر الذي تختاره على كل هذا في مقارنة نماذج التضمين من word2vec إلى المحولات الحديثة، واختر وسيلة التخزين في اختيار قاعدة بيانات المتجهات المناسبة للذكاء الاصطناعي والبحث، وقم بضبط بحث جيب التمام على نطاق واسع في ضبط HNSW للإنتاج باستخدام pgvector على Postgres 18. إذا كنت تبني مسار استرجاع (retrieval pipeline)، فقم بدمج هذا مع تقطيع النصوص المدرك للتوكنات لـ RAG في TypeScript.

Footnotes

  1. OpenAI, "Embeddings FAQ — Which distance function should I use?" https://help.openai.com/en/articles/6824809-embeddings-faq 2 3 4 5 6 7 8 9

  2. Pinecone, "Vector Similarity Explained" (Euclidean, dot product, cosine; match metric to training). https://www.pinecone.io/learn/vector-similarity/ 2 3 4 5 6 7 8 9 10 11 12

  3. Node.js, "Node.js 22.6.0" release notes (--experimental-strip-types). https://nodejs.org/en/blog/release/v22.6.0

  4. Sentence-Transformers, "MSMARCO Models" — normalized models work with cosine/dot/Euclidean alike, while msmarco-bert-base-dot-v5 is non-normalized and uses dot-product. https://www.sbert.net/docs/pretrained-models/msmarco-v5.html

  5. Cohere, "Introduction to Embeddings at Cohere" (embed-v4.0 cosine example). https://docs.cohere.com/docs/embeddings

  6. pgvector, README (distance operators, l2_normalize, normalized-vector guidance), v0.8.2. https://GitHub.com/pgvector/pgvector 2 3 4

الأسئلة الشائعة

تشابه جيب التمام هو الضرب النقطي مقسوماً على طولي المتجهين، لذا فهو يقيس الزاوية (الاتجاه) فقط. أما الضرب النقطي الخام فيحتفظ بالمقدار، لذا فإن المتجهات الأطول تسجل درجات أعلى حتى عند نفس الزاوية 2 .