llm-integration

Cosine Similarity vs Dot Product for Embeddings (2026)

June 30, 2026

Cosine Similarity vs Dot Product for Embeddings (2026)

Cosine similarity measures the angle between two vectors; the dot product also factors in their magnitude. For embeddings that are already normalized to length 1 — like OpenAI's — the two give identical rankings, so pick cosine when only direction matters and the dot product when magnitude carries meaning.

TL;DR

The whole "cosine similarity vs dot product for embeddings" question collapses into one fact: when vectors are unit length, the dot product is the cosine similarity. OpenAI returns embeddings normalized to length 1, so cosine, dot product, and even Euclidean distance all rank results the same way1. The metric only matters when vectors are not normalized — then cosine ignores magnitude, while dot product and Euclidean distance respond to it. This guide gives you the formulas, dependency-free TypeScript for all three, a worked example where they disagree, and what OpenAI, Cohere, and pgvector actually do.

What you'll learn

  • The exact formulas for dot product, cosine similarity, and Euclidean distance
  • Dependency-free TypeScript implementations of all three metrics
  • Why cosine similarity and the dot product agree for normalized embeddings (verified by running the code)
  • Whether you need to normalize embeddings — and when it changes your answer
  • When magnitude matters, so dot product or Euclidean distance is the better choice
  • What OpenAI, Cohere, and pgvector use, and how to pick a metric for your model

What is the difference between cosine similarity and dot product?

The dot product adds up the element-wise products of two vectors. Cosine similarity is that same dot product divided by both vectors' lengths, which cancels out magnitude and leaves only the angle between them. So the dot product reacts to both direction and magnitude, while cosine similarity reacts to direction only2.

Written out for vectors a and b:

  • Dot product: a · b = a₁b₁ + a₂b₂ + ... + aₙbₙ
  • Magnitude (L2 norm): ‖a‖ = √(a · a)
  • Cosine similarity: (a · b) / (‖a‖ · ‖b‖), which equals cos θ
  • Euclidean (L2) distance: ‖a − b‖ = √((a₁−b₁)² + ... + (aₙ−bₙ)²)

Cosine similarity ranges from -1 (opposite directions) through 0 (orthogonal) to 1 (same direction)2. Because the dot product equals ‖a‖ · ‖b‖ · cos θ, it shares the sign of the cosine but is scaled by the magnitudes — two vectors pointing the same way score higher if they are longer.

How do you compute cosine similarity, dot product, and Euclidean distance in TypeScript?

You don't need a library. All three metrics are a few lines of arithmetic over number[]. Here is a single dependency-free module:

// 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);
}

Two implementation notes that bite people in production. First, always guard the zero vector: dividing by a zero magnitude yields NaN, which then poisons every downstream comparison. Second, higher cosine similarity is better, but higher distance is worse — if you sort search results, cosine similarity sorts descending while cosine or Euclidean distance sorts ascending. Mixing those up silently returns your least similar documents.

Are cosine similarity and dot product the same thing?

For unit-length (normalized) vectors, yes — they are identical. When ‖a‖ = ‖b‖ = 1, the denominator in the cosine formula becomes 1, so cosine similarity reduces to the bare dot product12. That is why so many systems normalize once and then use the cheaper dot product everywhere.

Here is a demo that proves it, plus a case where the two metrics disagree:

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))}`);

Run it with npx tsx demo.ts, or directly on Node 22.6+ with node --experimental-strip-types demo.ts (Node 23.6+ runs TypeScript with no flag)3. Native type stripping requires the explicit .ts extension on the import, which is why the example imports ./similarity.ts literally. The output:

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

Look at docC. It points in the same direction as the query but is twice as long. Cosine similarity calls it a perfect match (1, tied with docA) because it ignores magnitude. The raw dot product, however, ranks docC above docA (4 vs 2) because it rewards the larger magnitude. After normalizing, docC becomes the same vector as the normalized query, and its dot product equals its cosine similarity exactly. That single example is the entire "cosine similarity vs dot product" debate in miniature.

Do you need to normalize embeddings before cosine similarity?

No — cosineSimilarity already divides by both magnitudes, so it works on raw vectors. Normalization matters for a different reason: it lets you swap the expensive cosine calculation for a plain dot product without changing the ranking. Normalize each vector once at write time, store the unit vector, and every later comparison is a cheap dot product1.

There is also a deeper equivalence for normalized vectors. Squaring the Euclidean distance of two unit vectors gives 2 − 2·cos θ, so Euclidean distance and cosine similarity move in lockstep — the closer the angle, the smaller the distance. The demo confirms it: for the normalized query and docB, the squared Euclidean distance is 2 and 2·(1 − cosine) is also 2. This is exactly why OpenAI's documentation states that cosine similarity and Euclidean distance produce identical rankings for its (already normalized) embeddings1.

When does normalization change your answer? Only when the magnitude itself is signal. If your vectors encode counts, frequencies, or confidence in their length — not just direction — normalizing throws that information away. For semantic text embeddings, direction is the signal, so normalizing (or using cosine) is the safe default.

When should you use dot product instead of cosine similarity?

Use the dot product when magnitude carries meaning and you want it to influence the ranking2. The classic case is recommendation systems built on matrix factorization: a longer item vector can mean the same topic but a more popular item, and you want that popularity to push it up. Cosine would erase it. The docC example above shows the mechanism — the dot product promoted the longer vector while cosine treated it as a tie.

The second reason is pure speed. If your vectors are already normalized, the dot product gives the same ranking as cosine for a fraction of the work, because you skip computing two magnitudes per comparison. That is precisely the optimization OpenAI calls out: with length-1 embeddings, "cosine similarity can be computed slightly faster using just a dot product"1.

When should you use Euclidean distance for embeddings?

Reach for Euclidean (L2) distance when the absolute position of vectors matters, not just their angle. It is the straight-line distance between two points, so it is sensitive to both direction and magnitude2. It shines with embeddings whose values represent measured quantities or counts, and with classical encodings such as locality-sensitive hashing.

For modern deep-learning text embeddings, Euclidean distance is rarely the first choice. Pinecone's guidance is blunt: in most cases you pair it with simpler vector-encoding methods such as locality-sensitive hashing rather than deep-learning models2. And remember the equivalence above — once vectors are normalized, Euclidean distance and cosine similarity produce the same ordering anyway, so there is no ranking benefit to choosing it over cosine.

Which distance metric do OpenAI, Cohere, and pgvector use?

The pragmatic rule from Pinecone is to match the metric to the one your embedding model was trained with2. A model that returns normalized embeddings (for example, all-MiniLM-L6-v2) ranks the same under cosine or the dot product, while a model tuned for the dot product (such as msmarco-bert-base-dot-v5, which does not normalize its output) should be compared with the dot product4. Here is what the major providers do:

  • OpenAI normalizes every embedding to length 1 by default — including after you shorten it with the dimensions parameter — so cosine, dot product, and Euclidean all rank identically. Their text-embedding-3-small and text-embedding-3-large models follow this, and the docs recommend cosine (computed as a dot product for speed)1.
  • Cohere's own embeddings example compares vectors with cosine similarity, computing np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) directly — the textbook cosine formula — for its embed-v4.0 model5.
  • pgvector, the Postgres extension, exposes one operator per metric. Because Postgres only supports ascending index scans, <#> returns the negative inner product6:
pgvector operatorMetricNotes
<->Euclidean (L2) distancestraight-line distance
<=>Cosine distance1 − cosine similarity
<#>Negative inner productmultiply by -1 for the dot product
<+>L1 (taxicab) distancesum of absolute differences

To get cosine similarity in SQL you subtract the cosine distance from one. pgvector also ships an l2_normalize() function, and its docs echo the OpenAI advice: for vectors normalized to length 1 (like OpenAI's), reach for the inner product to get the best performance6.

-- 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;

What is the range of cosine similarity and cosine distance?

Cosine similarity ranges from -1 to 1: 1 means the vectors point the same way, 0 means they are orthogonal, and -1 means they point in opposite directions2. Cosine distance is defined as 1 − cosine similarity, so it ranges from 0 to 26. In practice, many text-embedding similarities land in the positive range, but the theoretical floor is -1, so don't assume scores are always non-negative unless your vectors are.

Bottom line

For semantic search over modern text embeddings, default to cosine similarity, normalize your vectors once, and use the dot product as the fast equivalent — they rank identically when vectors are unit length. Switch to the raw dot product or Euclidean distance only when a vector's magnitude is genuine signal, such as in recommendation models. The math is small enough to own in a dependency-free TypeScript file, so you stay in control of zero-vector guards and sort direction.

Next steps: see how the encoder you choose affects all of this in how embedding models compare from word2vec to modern transformers, pick storage in choosing the right vector database for AI and search, and tune cosine search at scale in production HNSW tuning with pgvector on Postgres 18. If you're building a retrieval pipeline, pair this with token-aware text chunking for RAG in 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

Frequently Asked Questions

Cosine similarity is the dot product divided by both vectors' lengths, so it measures only the angle (direction). The raw dot product keeps magnitude, so longer vectors score higher even at the same angle 2 .