Cosine Similarity vs Dot Product: When to Use Each for Vector Search
TL;DR — Cosine similarity and dot product are mathematically identical for L2-normalized vectors. AI/TLDR: "Cosine similarity is the right default for RAG, semantic search, and Q&A. It is the default in Pinecone, what Qdrant recommends, and tolerates unnormalized inputs. When vectors are unit-normalized, dot product and cosine are numerically identical." LLMBestPractices: "Normalize before storing and normalize the query. Both must be normalized, or dot product no longer equals cosine. Without normalization, longer documents produce higher-magnitude embeddings — a silent bug where verbose but weakly relevant docs rank above short, highly relevant ones." Brenndoerfer: "For normalized vectors, cosine, dot product, and Euclidean all induce the same ranking. Normalize once at ingest, then use the fastest metric (dot product)." AI/TLDR: "Normalize once at write time, then search with inner product — you pay normalization cost once per vector instead of recomputing on millions of comparisons." Learn more with what are embeddings, choose embedding model, hybrid search, and what is RAG.
AI/TLDR gives the core rule: "The single most reliable rule: check your embedding model's documentation and match its training metric. Most modern text embedding models are trained with cosine similarity or with normalized vectors and dot product (which are the same thing). Using a different metric will degrade retrieval quality in ways that may only show up in production."
LLMBestPractices warns about the silent bug: "Without normalization, longer documents tend to produce higher-magnitude embeddings. A dot product between an unnormalized query and an unnormalized corpus vector rewards length over relevance. A short, highly relevant document can rank below a verbose but weakly relevant one solely because of magnitude imbalance. This is a silent bug; the index does not error, results just degrade."
Similarity Metrics Architecture
= (A · B) / (|A| × |B|)
Direction only
Range: -1 to +1
Ignores magnitude"] Dot["Dot Product
= A · B = Σ(aᵢ × bᵢ)
Direction + magnitude
Sensitive to vector length"] Euclidean["Euclidean Distance
= √Σ(aᵢ - bᵢ)²
Straight-line distance
Sensitive to position"] end subgraph Normalized["L2-Normalized Vectors (|v| = 1)"] N_Cosine["Cosine = A · B"] N_Dot["Dot = A · B"] N_Euclidean["L2 = √(2 - 2cos θ)"] Identical["All three produce
IDENTICAL rankings"] end subgraph Decision["When to Use"] Text["Text embeddings / RAG
→ Cosine (or dot on normalized)"] Rec["Recommendations
→ Dot product (magnitude = activity)"] Vision["Computer vision
→ Euclidean (magnitude = features)"] end Cosine --> Normalized Dot --> Normalized Euclidean --> Normalized N_Cosine --> Identical N_Dot --> Identical N_Euclidean --> Identical Identical --> Text Dot --> Rec Euclidean --> Vision
Similarity Metrics Comparison
| Metric | Formula | Magnitude | Best For | Speed | Normalized? |
|---|---|---|---|---|---|
| Cosine | (A · B) / ( | A | × | B | ) |
| Dot product | Σ(aᵢ × bᵢ) | Sensitive | Recommendations, activity-based | Fastest | Must normalize first |
| Euclidean | √Σ(aᵢ - bᵢ)² | Sensitive | Computer vision, geometric | Slow (sqrt) | Same ranking as cosine |
| Manhattan | Σ | aᵢ - bᵢ | Sensitive | Sparse/quantized vectors | |
| Hamming | Count differing bits | N/A | Binary embeddings, extreme throughput | Fastest | N/A |
Normalization Guide by Situation
| Situation | Normalize? | Why |
|---|---|---|
| Search with cosine metric | No | Cosine divides by norms internally |
| Search with dot product / IP | Yes | Magnitude will leak into scores |
| Search with Euclidean (L2) | Usually yes | Same ranking as cosine on unit vectors |
| Model outputs unit vectors | No | Already normalized (OpenAI, Sentence-BERT) |
| Mix vectors from different models | Yes | Normalize to common scale |
| Truncate vectors to fewer dims | Yes | Truncation breaks unit length |
Implementation
import math
from dataclasses import dataclass
from typing import Optional
from enum import Enum
class SimilarityMetric(Enum):
COSINE = "cosine"
DOT_PRODUCT = "dot_product"
EUCLIDEAN = "euclidean"
MANHATTAN = "manhattan"
@dataclass
class SimilarityComparator:
"""Compare vectors using different similarity metrics."""
def cosine_similarity(self, v1: list, v2: list) -> float:
"""Cosine similarity: angle between vectors (-1 to +1).
Ignores magnitude. Best for text embeddings."""
dot = sum(a * b for a, b in zip(v1, v2))
mag1 = math.sqrt(sum(a * a for a in v1))
mag2 = math.sqrt(sum(b * b for b in v2))
if mag1 == 0 or mag2 == 0:
return 0.0
return dot / (mag1 * mag2)
def dot_product(self, v1: list, v2: list) -> float:
"""Dot product: sensitive to both direction and magnitude.
Use ONLY on normalized vectors for similarity search."""
return sum(a * b for a, b in zip(v1, v2))
def euclidean_distance(self, v1: list, v2: list) -> float:
"""Euclidean distance: straight-line distance.
Lower = more similar. Suffers curse of dimensionality."""
return math.sqrt(sum((a - b) ** 2 for a, b in zip(v1, v2)))
def manhattan_distance(self, v1: list, v2: list) -> float:
"""Manhattan distance: sum of absolute differences.
Useful for sparse or quantized representations."""
return sum(abs(a - b) for a, b in zip(v1, v2))
def l2_normalize(self, v: list) -> list:
"""L2 normalize: scale vector to unit length.
After normalization, dot product = cosine similarity."""
magnitude = math.sqrt(sum(x * x for x in v))
if magnitude == 0:
return v
return [x / magnitude for x in v]
def is_normalized(self, v: list, tolerance: float = 0.001) -> bool:
"""Check if a vector is L2-normalized (unit length)."""
magnitude = math.sqrt(sum(x * x for x in v))
return abs(magnitude - 1.0) < tolerance
def compare_all_metrics(self, v1: list, v2: list) -> dict:
"""Compare two vectors using all metrics."""
# Normalize for dot product comparison
norm1 = self.l2_normalize(v1)
norm2 = self.l2_normalize(v2)
cosine = self.cosine_similarity(v1, v2)
dot_raw = self.dot_product(v1, v2)
dot_normalized = self.dot_product(norm1, norm2)
euclidean_raw = self.euclidean_distance(v1, v2)
euclidean_normalized = self.euclidean_distance(norm1, norm2)
manhattan = self.manhattan_distance(v1, v2)
return {
"cosine_similarity": round(cosine, 6),
"dot_product_raw": round(dot_raw, 6),
"dot_product_normalized": round(dot_normalized, 6),
"euclidean_raw": round(euclidean_raw, 6),
"euclidean_normalized": round(euclidean_normalized, 6),
"manhattan_distance": round(manhattan, 6),
"v1_normalized": self.is_normalized(v1),
"v2_normalized": self.is_normalized(v2),
"cosine_equals_dot_normalized": abs(
cosine - dot_normalized) < 0.0001,
"euclidean_normalized_relation": round(
math.sqrt(2 - 2 * cosine), 6),
}
def search(self, query: list, vectors: list,
metric: SimilarityMetric = SimilarityMetric.COSINE,
top_k: int = 5,
normalize: bool = True) -> list:
"""Search vectors using specified metric."""
if normalize and metric != SimilarityMetric.COSINE:
query = self.l2_normalize(query)
vectors = [(self.l2_normalize(v["vector"]), v)
for v in vectors]
else:
vectors = [(v["vector"], v) for v in vectors]
scored = []
for vec, metadata in vectors:
if metric == SimilarityMetric.COSINE:
score = self.cosine_similarity(query, vec)
elif metric == SimilarityMetric.DOT_PRODUCT:
score = self.dot_product(query, vec)
elif metric == SimilarityMetric.EUCLIDEAN:
score = -self.euclidean_distance(query, vec)
elif metric == SimilarityMetric.MANHATTAN:
score = -self.manhattan_distance(query, vec)
else:
score = self.cosine_similarity(query, vec)
scored.append({
"content": metadata.get("content", ""),
"source": metadata.get("source", ""),
"score": round(score, 6),
"metric": metric.value,
})
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]
def demonstrate_magnitude_effect(self) -> dict:
"""Show how magnitude affects dot product but not cosine."""
# Two vectors with same direction, different magnitude
short = [0.3, 0.4, 0.5]
long = [0.6, 0.8, 1.0] # 2x magnitude, same direction
query = [0.3, 0.4, 0.5]
return {
"query": query,
"short_vector": short,
"long_vector": long,
"long_is_2x_magnitude": True,
"cosine_query_vs_short": round(
self.cosine_similarity(query, short), 6),
"cosine_query_vs_long": round(
self.cosine_similarity(query, long), 6),
"dot_query_vs_short": round(
self.dot_product(query, short), 6),
"dot_query_vs_long": round(
self.dot_product(query, long), 6),
"cosine_treats_them_as_identical": abs(
self.cosine_similarity(query, short) -
self.cosine_similarity(query, long)) < 0.0001,
"dot_rewards_longer_vector": (
self.dot_product(query, long) >
self.dot_product(query, short)
),
"lesson": (
"Cosine treats same-direction vectors as identical "
"regardless of magnitude. Dot product rewards longer "
"vectors. This is why you must normalize before using "
"dot product for similarity search."
),
}
Cosine Similarity vs Dot Product Checklist
- [ ] Use cosine similarity as the default for text embeddings and RAG
- [ ] Cosine is the default in Pinecone, Qdrant, and most vector databases
- [ ] Cosine tolerates unnormalized inputs gracefully
- [ ] For L2-normalized vectors, dot product and cosine are mathematically identical
- [ ] Normalize at ingest time, then search with dot product (inner product) for speed
- [ ] Normalization cost is paid once per vector, not on every comparison
- [ ] Dot product is the fastest metric on every major vector engine
- [ ] Only use dot product on normalized vectors — otherwise magnitude leaks into scores
- [ ] Without normalization, longer documents produce higher-magnitude embeddings
- [ ] Silent bug: verbose but weakly relevant docs rank above short, highly relevant ones
- [ ] Never mix normalized and unnormalized vectors in the same index
- [ ] Normalize your query vector the same way you normalized stored vectors
- [ ] Check if your embedding model outputs normalized vectors (OpenAI, Sentence-BERT do)
- [ ] OpenAI text-embedding-3-small/large output unit-normalized vectors
- [ ] Voyage AI client returns raw (unnormalized) vectors — normalize before dot product
- [ ] Hugging Face transformers with mean pooling: unnormalized — normalize before dot product
- [ ] If using cosine metric, no need to normalize — database handles it internally
- [ ] If using dot product / IP metric, must normalize first
- [ ] If using Euclidean distance, usually normalize for same ranking as cosine
- [ ] Euclidean distance suffers curse of dimensionality at 768+ dimensions
- [ ] For normalized vectors, Euclidean and cosine produce identical rankings
- [ ] Euclidean distance: d = √(2 - 2cos θ) — monotonically related to cosine
- [ ] Use dot product for recommendations where magnitude = activity level
- [ ] Use Euclidean for computer vision where magnitude = feature strength
- [ ] Use cosine for text where chunk length varies and should not affect similarity
- [ ] Manhattan distance: rarely best for text, useful for sparse/quantized vectors
- [ ] Hamming distance: for binary embeddings with extreme throughput needs
- [ ] Match the metric your embedding model was trained with — check documentation
- [ ] Most modern models trained with cosine similarity or normalized dot product
- [ ] Using wrong metric degrades retrieval quality in ways that only show in production
- [ ] pgvector: use
<#>(inner product, negated) or<=>(cosine) after normalization - [ ] ChromaDB: set
metadata={"hnsw:space": "ip"}or"cosine" - [ ] Pinecone: set
metric="dotproduct"at index creation - [ ] Faiss: use
IndexFlatIPorMETRIC_INNER_PRODUCTin HNSW - [ ] Qdrant: use
Dotsimilarity for normalized vectors,Cosinefor unnormalized - [ ] Add sanity assertion at index load: check vectors are unit length
- [ ] Truncating vectors to fewer dimensions breaks normalization — re-normalize
- [ ] Similarity scores are relative to model geometry — 0.82 from MiniLM ≠ 0.82 from OpenAI
- [ ] Read what are embeddings for fundamentals
- [ ] Choose embedding model that matches your metric
- [ ] Use embeddings in RAG pipelines
- [ ] Add hybrid search with your similarity metric
- [ ] Build RAG from scratch with correct metric
- [ ] Add reranking after similarity search
- [ ] Apply chunk size optimization with correct metric
- [ ] Evaluate with RAG metrics
- [ ] Test: cosine and dot product produce identical rankings on normalized vectors
- [ ] Test: dot product rewards longer vectors on unnormalized inputs
- [ ] Test: cosine treats same-direction vectors as identical regardless of magnitude
- [ ] Test: Euclidean and cosine produce same ranking on unit vectors
- [ ] Test: normalization produces unit-length vectors (magnitude = 1.0)
- [ ] Document chosen metric, normalization strategy, and vector database configuration
FAQ
What is the difference between cosine similarity and dot product?
Cosine similarity measures the angle between two vectors (direction only, -1 to +1), while dot product is sensitive to both direction and magnitude. AI/TLDR: "The dot product A · B multiplies each pair of corresponding components and sums the results. Unlike cosine similarity, it is sensitive to vector magnitude. A longer vector produces a larger dot product even with the same directional alignment. When both vectors are unit-normalised (magnitude = 1), the dot product and cosine similarity are numerically identical, because the normalisation step vanishes." Brenndoerfer: "If your vectors are L2-normalized (magnitude = 1), then the dot product and cosine similarity are identical. In practice, many libraries use squared Euclidean distance to avoid the square root computation, since it preserves the same ordering." Key difference: cosine ignores magnitude (good for text where chunk length varies), dot product rewards magnitude (good for recommendations where activity level matters).
When should you use cosine similarity vs dot product?
Use cosine similarity as the default for text embeddings and RAG. Use dot product only when vectors are L2-normalized or when magnitude carries meaning. AI/TLDR: "For the vast majority of RAG, semantic search, and Q&A applications built with modern embedding APIs, cosine similarity is the right default. It is the default in Pinecone, it is what Qdrant recommends when in doubt, and it tolerates unnormalised inputs gracefully. Only deviate when you have a specific reason tied to your model or your data." Brenndoerfer: "Cosine similarity / normalized dot product: default choice for most sentence embedding models (Sentence-BERT, E5, GTE). Dot product: use when your model explicitly trains with dot product and encodes meaningful information in vector magnitude." LLMBestPractices: "Normalize before storing in the index and normalize the query vector before each search. Both must be normalized, or the dot product no longer equals cosine similarity."
Why does L2 normalization make cosine and dot product identical?
L2 normalization scales every vector to unit length (magnitude = 1), which causes the magnitude term in cosine similarity to become 1 and drop out. AI/TLDR: "Dot product becomes cosine similarity. Once every vector has length 1, the cheap dot product of two vectors equals their cosine similarity exactly. You get the meaning-based metric you want at the speed of the metric that is fastest to compute. The standard advice is: normalize once, when you write a vector into the store, then search with the plain dot product (often labeled inner product or IP). You pay the normalization cost a single time per vector instead of recomputing lengths on every one of millions of comparisons." LLMBestPractices: "Because |u| = |v| = 1, the denominator drops out. This means you can configure the vector index to use inner product (IP / dot product), which is faster than computing cosine explicitly on every comparison. Set the metric to dot or ip after normalizing, never before." Math: cosine(A,B) = (A · B) / (|A| × |B|). When |A| = |B| = 1, cosine(A,B) = A · B.
Should you normalize embeddings before storing in a vector database?
Yes, normalize at ingest time if you plan to use dot product for search. If using cosine metric, the database handles normalization internally. AI/TLDR: "If you search with dot product / inner product: yes, normalize first, or magnitude will leak into your scores. If you search with cosine: no, the database handles length internally. Your model already outputs unit vectors: no, re-normalizing is harmless but pointless. You mix vectors from two different models: yes, normalize everything to one common scale." LLMBestPractices: "Without normalization, longer documents tend to produce higher-magnitude embeddings. A dot product between an unnormalized query and an unnormalized corpus vector rewards length over relevance. A short, highly relevant document can rank below a verbose but weakly relevant one solely because of magnitude imbalance. This is a silent bug; the index does not error, results just degrade." Warning: never mix normalized and unnormalized vectors in the same index. Normalize your query the same way you normalized the stored vectors.
Why not use Euclidean distance for text embeddings?
Euclidean distance suffers from the curse of dimensionality in high-dimensional spaces (768+ dimensions), where distances between all points converge toward similar values. AI/TLDR: "Euclidean distance measures the straight-line distance between two points. It is sensitive to both direction and magnitude shifts. If one vector is simply twice as long as another but points the same way, Euclidean distance will call them far apart even though they represent the same concept. This makes it less robust for text embeddings where chunk length can vary, but ideal for geometric tasks or feature vectors where absolute scale carries meaning." Brenndoerfer: "For normalized vectors, searching by minimum Euclidean distance produces the same ranking as searching by maximum cosine similarity. The two metrics induce identical orderings over all candidate vectors, differing only in the numerical values they assign. Euclidean distance: common in computer vision embeddings and some specialized models; equivalent to cosine for normalized vectors." Kanojiya: "Cosine similarity treats a_short and a_long as identical (they point in the same direction). Euclidean distance sees them as very different (they are at different positions in space). Choose the metric that matches the geometry of your embedding model."
Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →