Engineering

Understanding cosine similarity vs dot product in vector search

ManyVector Team 2026-07-08 8 min read

When you create a vector namespace, you choose a distance metric: cosine similarity, dot product (inner product), or Euclidean distance. For most users, cosine vs. dot product is the most common decision — and the nuance matters.

The math

Cosine similarity

Cosine similarity measures the angle between two vectors, ignoring their magnitude:

cosine_sim(A, B) = (A · B) / (|A| × |B|)

Range: -1 to 1. Two vectors pointing in the same direction score 1.0 regardless of their magnitude. Two perpendicular vectors score 0. Opposite vectors score -1.

Dot product (inner product)

The dot product is the sum of the products of corresponding elements:

dot_product(A, B) = Σ Aᵢ × Bᵢ

The relationship: dot_product(A, B) = |A| × |B| × cosine_sim(A, B)

When vectors are unit-normalized (magnitude = 1), the dot product equals the cosine similarity. This is the key insight.

When they’re equivalent

If your embedding model returns L2-normalized vectors (unit vectors with magnitude = 1), cosine similarity and dot product produce identical rankings. Computing the dot product is faster (no normalization step), so many systems use dot product as an optimization when they know vectors are already normalized.

Most modern text embedding models normalize their outputs. OpenAI’s text-embedding-3-* models return unit vectors. You can verify:

import numpy as np

embedding = model.embed("hello world")
magnitude = np.linalg.norm(embedding)
print(f"Magnitude: {magnitude:.6f}")  # Should print ~1.000000

When they diverge

The divergence happens when vectors are not normalized — their magnitudes vary. In this case:

  • Dot product favors vectors with larger magnitude. A long document produces a larger embedding magnitude (if not normalized), so it scores higher on dot product regardless of semantic similarity.
  • Cosine similarity normalizes for magnitude. A short and long document about the same topic score similarly.

For recommendation systems that use retrieval-augmented scoring (where the magnitude of the item vector encodes popularity or relevance), dot product is intentional — you want popular or high-quality items to score higher.

For pure semantic similarity (finding the most semantically related document), cosine similarity is usually more appropriate.

Euclidean distance

L2 (Euclidean) distance is the straight-line distance between two points:

L2(A, B) = sqrt(Σ (Aᵢ - Bᵢ)²)

Note: with L2, smaller is more similar (lower distance = closer). With cosine and dot product, higher is more similar.

L2 is common in:

  • Computer vision (image embeddings are often compared with L2)
  • Scientific/spatial applications where geometric distance has inherent meaning
  • Clustering algorithms (k-means uses L2 by default)

For text embeddings from transformer models, L2 and cosine produce similar rankings when vectors are normalized. The choice between them is primarily based on convention and the model’s training objective.

Practical guidance

Use cosine similarity when:

  • Your embedding model doesn’t guarantee normalized outputs
  • You want consistent scores regardless of document length
  • You’re comparing text documents or semantic similarity

Use dot product when:

  • Your embedding model returns normalized vectors (verify this) and you want faster queries
  • You’re building a recommendation system where magnitude encodes a quality signal
  • The model was explicitly trained with dot product as the objective (check the model card)

Use Euclidean (L2) when:

  • Your embedding model was trained with L2 as the objective (common in vision models)
  • You’re clustering vectors or doing spatial analysis
  • The model documentation explicitly recommends it

In practice: check the model card

The definitive answer is always in the embedding model’s documentation. From OpenAI’s text-embedding-3 docs:

“We recommend cosine similarity for text-embedding-3-small and text-embedding-3-large. The outputs are already normalized.”

From sentence-transformers documentation for all-MiniLM-L6-v2:

“For many applications, cosine-similarity is the preferred metric. The model is fine-tuned with cosine similarity as the objective.”

For models where the card is ambiguous, normalize the embeddings before indexing and use cosine similarity — it’s the safe default.

Creating a namespace with the right metric

from manyvector import ManyVector

client = ManyVector(api_key="mv-...")

# For text embeddings (most models)
ns = client.create_namespace(
    name="docs",
    dimensions=1536,
    metric="cosine",
)

# For normalized outputs where dot product is faster
ns_fast = client.create_namespace(
    name="docs-dot",
    dimensions=1536,
    metric="dot_product",
)

# For vision embeddings or spatial data
ns_images = client.create_namespace(
    name="images",
    dimensions=512,
    metric="euclidean",
)

Important: The metric is set at namespace creation and cannot be changed. Choose correctly upfront.

Benchmarking your metric choice

If you’re unsure, run both and measure recall on your eval set:

def evaluate_metric(namespace, eval_queries, k=10):
    scores = []
    for q in eval_queries:
        results = namespace.query(vector=q["vector"], top_k=k)
        retrieved = {r.id for r in results}
        relevant = set(q["relevant_ids"])
        scores.append(len(retrieved & relevant) / len(relevant))
    return sum(scores) / len(scores)

cosine_recall = evaluate_metric(cosine_ns, eval_queries)
dot_recall    = evaluate_metric(dot_ns, eval_queries)

print(f"Cosine recall@10: {cosine_recall:.3f}")
print(f"Dot product recall@10: {dot_recall:.3f}")

For normalized embeddings (which most text models produce), the scores should be nearly identical. If they differ significantly, your embeddings may not be normalized — stick with cosine.

Related articles

Start searching every vector today.