In Plain English
Imagine two people standing in the centre of a city, each pointing their arm in a different direction to indicate where they want to go. Cosine similarity asks: how closely are they pointing in the same direction? It only cares about the angle between the arms, not how far each person can reach. Dot product asks: how much do the directions agree, weighted by how confidently each person is pointing? If one person raises their arm high (big magnitude) and points the same way, the dot product is large. Euclidean distance simply measures the straight-line gap between the two fingertip positions — how far apart the tips end up in space.

In practice, each embedding produced by a model is just a list of numbers — a vector. When you want to know whether two pieces of text are semantically similar, you compare their vectors using one of these three metrics. They each measure "closeness" differently, and the choice affects both the quality of your results and the speed of your system.
Why It Matters for Builders
Every vector database — Pinecone, Qdrant, Weaviate, pgvector, Chroma — forces you to pick a distance metric when you create a collection. Choose the wrong one and your search results will be subtly degraded in ways that are hard to debug: the right answer is in your database, but it ranks 20th instead of 1st.
The choice also affects performance at scale. Dot product skips a square-root and a division step compared to Euclidean distance and cosine similarity respectively, making it marginally faster on CPU. When you are running millions of comparisons per second in an ANN index, that matters. Most production RAG pipelines default to cosine or dot product for exactly this reason.
- Semantic / RAG search — wrong metric degrades recall silently; your LLM gets worse context.
- Recommendation engines — dot product captures popularity (magnitude) signals; cosine ignores them.
- Clustering — Euclidean distance shapes cluster geometry; switching to cosine can split or merge clusters unexpectedly.
- Cost — a misconfigured index can require a full re-index (re-inserting every vector) to fix.
How Each Metric Works
Cosine Similarity
Cosine similarity measures the angle between two vectors, returning a value from -1 (opposite directions) to +1 (identical directions). Magnitude is normalised away entirely. The formula is cos(θ) = (A · B) / (||A|| × ||B||) — the dot product divided by the product of the lengths. A value of 1 means the vectors point in exactly the same direction; 0 means they are orthogonal (unrelated); -1 means they are diametrically opposite.
Dot Product
The dot product A · B = Σ (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. This is useful in recommendation systems where a high-magnitude user embedding might represent an especially active or influential user. When both vectors are unit-normalised (magnitude = 1), the dot product and cosine similarity are numerically identical, because the normalisation step vanishes.
Euclidean Distance (L2)
Euclidean distance measures the straight-line distance between two points in the embedding space: d = sqrt(Σ (aᵢ - bᵢ)²). Lower is more similar. 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.
- Measures: angle between vectors
- Range: -1 to +1 (higher = more similar)
- Magnitude: ignored
- Best for: text, NLP, RAG
- Cost: division + sqrt
- Measures: directional agreement × magnitude
- Range: -∞ to +∞ (higher = more similar)
- Magnitude: amplifies score
- Best for: normalised embeddings, recommendations
- Cost: cheapest (just multiply-add)
- Measures: straight-line distance
- Range: 0 to +∞ (lower = more similar)
- Magnitude: fully influences distance
- Best for: geometric features, image vectors
- Cost: subtraction + sqrt
The Math in Code
Here are all three metrics implemented from scratch in Python, followed by a NumPy one-liner for each. Seeing the code side by side makes it obvious why dot product is the cheapest operation.
import numpy as np
A = np.array([0.6, 0.8]) # already unit-length: sqrt(0.36+0.64) = 1.0
B = np.array([0.0, 1.0]) # unit vector pointing straight up
# --- Cosine Similarity ---
# formula: (A . B) / (||A|| * ||B||)
cosine = np.dot(A, B) / (np.linalg.norm(A) * np.linalg.norm(B))
print(f"Cosine similarity: {cosine:.4f}") # 0.8000
# --- Dot Product ---
# formula: sum(a_i * b_i)
dot = np.dot(A, B)
print(f"Dot product: {dot:.4f}") # 0.8000 (same! because A is unit-length)
# --- Euclidean Distance ---
# formula: sqrt(sum((a_i - b_i)^2))
euclidean = np.linalg.norm(A - B)
print(f"Euclidean distance: {euclidean:.4f}") # 0.6000
# Now try a non-normalised vector
C = np.array([1.2, 1.6]) # C = 2 * A, same direction, double magnitude
cosine_AC = np.dot(A, C) / (np.linalg.norm(A) * np.linalg.norm(C))
dot_AC = np.dot(A, C)
euclidean_AC = np.linalg.norm(A - C)
print("\nA vs C (same direction, C is 2x longer):")
print(f"Cosine similarity: {cosine_AC:.4f}") # 1.0000 (perfect match)
print(f"Dot product: {dot_AC:.4f}") # 2.0000 (boosted by magnitude)
print(f"Euclidean distance: {euclidean_AC:.4f}") # 1.0000 (non-zero despite same direction!)The final block is the key insight: vectors A and C point in exactly the same direction (same semantic meaning) but C is twice as long. Cosine correctly scores them as identical (1.0). Dot product doubles the score — useful if magnitude encodes confidence or popularity. Euclidean distance says they are 1.0 apart — treating the length difference as "distance" even though the meaning is the same.
Which Metric Should You Pick?
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 normalised 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.
| Scenario | Recommended metric | Why |
|---|---|---|
| OpenAI text-embedding-3-small / large | Cosine or dot product | Outputs are unit-normalised; both metrics produce identical rankings |
| sentence-transformers (all-MiniLM-L6-v2, etc.) | Cosine | Models were trained with cosine loss; documentation recommends it |
| Recommendation system with user/item embeddings | Dot product | Magnitude encodes engagement signals you want to preserve |
| Image or audio feature vectors | Euclidean (L2) | Absolute spatial distance often maps to perceptual distance |
| Sparse TF-IDF-style vectors | Dot product | Length varies with document word count; sparse interactions are multiplicative |
| Clustering before labelling | Euclidean (L2) | K-means and DBSCAN are geometrically defined in L2 space |
In practice, 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 from it when you have a specific reason tied to your model or your data.
Vector Database Defaults
- Pinecone — default index metric is cosine; dot product and Euclidean are available options set at collection creation.
- Qdrant — cosine recommended as the safe default; internally normalises vectors in cosine collections so client-side normalisation is optional.
- Weaviate — cosine by default; supports dot product and L2.
- pgvector — three operators:
<->(L2),<#>(negative dot product),<=>(cosine). Must add anivfflatorhnswindex per operator — a common pitfall is indexing for L2 then querying with<=>. - Chroma — cosine by default for text collections; L2 and inner product (dot product) also available.
Going Deeper
The Equivalence Proof
When both vectors are unit-normalised (||A|| = ||B|| = 1), cosine similarity reduces algebraically to cos(θ) = A · B / (1 × 1) = A · B. The normalisation constants cancel, leaving just the dot product. Furthermore, there is a direct algebraic relationship between Euclidean distance and dot product on unit vectors: ||A - B||² = ||A||² - 2(A · B) + ||B||² = 1 - 2(A · B) + 1 = 2 - 2(A · B). So euclidean_distance = sqrt(2 - 2 × cosine_similarity). This means on unit vectors, all three metrics produce the same ranking — minimising Euclidean distance is equivalent to maximising cosine similarity and maximising dot product simultaneously.
When Cosine Similarity Can Mislead
Because cosine similarity ignores magnitude entirely, it can produce unintuitive results in edge cases. Two vectors that both point in the same direction but represent completely different concepts (due to an embedding model failure, or domain shift) will score 1.0. There is also a less-known issue in high-dimensional spaces: cosine similarity scores cluster tightly near 0 as dimensionality grows (a consequence of the curse of dimensionality), which can make all pairs look similarly unrelated. In practice, this rarely matters for modern embeddings with 1024-3072 dimensions trained on diverse data, but it is a reason to always evaluate retrieval quality on your actual data rather than relying solely on the metric name.
Matryoshka Embeddings and Variable-Dimension Truncation
OpenAI's text-embedding-3 models and some sentence-transformers models use Matryoshka Representation Learning (MRL), which trains embeddings so that the first d dimensions are themselves a useful embedding at dimension d. This means you can truncate a 3072-dimension vector down to 256 dimensions and still get solid retrieval quality. The truncated vectors are no longer unit-normalised in the original space, so you should re-normalise after truncation before computing cosine similarity. The OpenAI API accepts a dimensions parameter that both truncates and re-normalises in one step.
import numpy as np
def truncate_and_renormalize(embedding: list[float], dims: int) -> list[float]:
"""Truncate an MRL embedding and re-normalise to unit length."""
v = np.array(embedding[:dims], dtype=np.float32)
norm = np.linalg.norm(v)
if norm == 0:
return v.tolist()
return (v / norm).tolist()
# Example: shrink a 1536-dim OpenAI embedding to 256 dims for a cheaper index
full_embedding = [...] # 1536 floats from the API
small_embedding = truncate_and_renormalize(full_embedding, 256)Inner Product vs Dot Product in Vector DB Docs
You will see both "dot product" and "inner product" in vector database documentation — they mean the same thing for real-valued vectors. For complex-valued or quaternion embeddings the distinction matters, but for standard float32 embedding vectors used in NLP and vision, dot_product(A, B) == inner_product(A, B). Weaviate and pgvector use "inner product"; Pinecone and Qdrant use "dot product". Do not let the naming difference confuse you when migrating between databases.
FAQ
Is cosine similarity always better than dot product for text search?
Not always, but it is a safer default. Cosine similarity is direction-only, so it is robust to variations in embedding magnitude caused by chunk length or preprocessing differences. Dot product is strictly faster and numerically equivalent when vectors are unit-normalised, which most modern embedding models guarantee. If you are uncertain whether your vectors are normalised, cosine is the safer choice.
Do I need to normalise my embeddings before storing them in a vector database?
It depends on the database and metric. Qdrant automatically normalises vectors when a collection uses cosine distance, so client-side normalisation is optional. For dot product collections in Qdrant, Pinecone, and Weaviate, vectors are stored as-is — if you want cosine-equivalent rankings you must normalise them yourself before insertion. Always check your database documentation.
Why do all three metrics give the same ranking for unit-normalised vectors?
Because on unit vectors (||A|| = ||B|| = 1), cosine similarity reduces to the dot product (the denominator becomes 1), and Euclidean distance equals sqrt(2 - 2 * dot_product). Maximising dot product, maximising cosine similarity, and minimising Euclidean distance are all equivalent operations on unit-length vectors — they sort every pair of vectors in the same order.
Which metric should I use with OpenAI embeddings?
OpenAI officially recommends cosine similarity for text-embedding-3-small and text-embedding-3-large. Because these models output unit-normalised vectors, dot product produces identical rankings and is slightly faster. Euclidean distance also gives the same ranking when vectors are unit-normalised. In practice, use whichever metric your vector database defaults to for text workloads — cosine is most common.
Can I change the distance metric on an existing Pinecone or Qdrant collection?
No. The distance metric is set at collection creation time and cannot be changed without re-creating the collection and re-inserting all vectors. This is why choosing the right metric upfront matters — switching later requires a full re-index, which can be costly at scale.
What is the difference between cosine distance and cosine similarity?
Cosine similarity is the cosine of the angle between two vectors, ranging from -1 to +1 where higher means more similar. Cosine distance is 1 - cosine_similarity, ranging from 0 to 2 where lower means more similar. Vector databases that minimise distance (like most ANN implementations) use cosine distance internally. The two are equivalent for ranking purposes — they just flip the direction of the comparison.