n4nAI

Cosine similarity vs Euclidean distance: which to use

A practical comparison of cosine similarity and Euclidean distance for vector embeddings, with code examples and clear guidance on when to use each metric.

n4n Team6 min read1,258 words

Audio narration

Coming soon — every post will get a voice note here.

Cosine similarity vs Euclidean distance is the first decision you make when building anything on top of vector embeddings — search, clustering, recommendation, anomaly detection. The choice changes your results materially, and most tutorials gloss over why. Here’s the practical breakdown.

What each metric actually measures

Cosine similarity measures the angle between two vectors, ignoring their magnitude entirely. It returns a value in [-1, 1], where 1 means identical direction, 0 means orthogonal, and -1 means opposite direction. For normalized vectors, it’s equivalent to the dot product.

import numpy as np

def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Identical direction, different magnitude
a = np.array([1.0, 2.0, 3.0])
b = np.array([2.0, 4.0, 6.0])  # 2x magnitude
print(cosine_similarity(a, b))  # 1.0

Euclidean distance measures the straight-line distance between two points in vector space. It returns a non-negative value in [0, ∞), where 0 means identical vectors. It is sensitive to both direction and magnitude.

def euclidean_distance(a: np.ndarray, b: np.ndarray) -> float:
    return np.linalg.norm(a - b)

print(euclidean_distance(a, b))  # ~3.74 — not zero!

If you L2-normalize your embeddings first, Euclidean distance and cosine similarity become monotonically related: d = sqrt(2 - 2*cos). But most embedding models don’t output normalized vectors by default, and normalizing loses information you might need.

When magnitude matters (and when it doesn’t)

Text embeddings from models like text-embedding-3-large or bge-large-en-v1.5 tend to have magnitude that correlates with semantic “information content” — longer documents, more specific concepts, or higher-confidence predictions often produce larger-norm vectors. Cosine similarity discards this signal. Euclidean distance preserves it.

Consider a concrete scenario: you’re building a document-level deduplication system. Two documents cover the same topic but one is a 500-word summary and the other is a 5000-word deep dive. Their embeddings point in roughly the same direction, but the longer document has a larger norm. Cosine similarity says “these are the same.” Euclidean distance says “these are different lengths.” Which is correct depends entirely on your downstream task.

For semantic search over heterogeneous document lengths, cosine similarity usually wins — you want “about the same thing” to rank higher than “same thing but longer.” For clustering where cluster cohesion should reflect both topic and scope, Euclidean distance often produces tighter, more meaningful groups.

Image embeddings behave differently. CLIP and similar models typically output normalized vectors by design. In that regime, the metrics are interchangeable up to monotonic transformation. But if you’re working with raw penultimate-layer activations from a ResNet or ViT, magnitude carries signal about activation strength, and Euclidean distance captures it.

Computational considerations

At query time, the difference is negligible for most workloads. Both reduce to a dot product plus a norm computation. But at index-build time and for certain ANN algorithms, the choice constrains your options.

HNSW (Hierarchical Navigable Small World), the dominant graph-based ANN index, works natively with inner product (which equals cosine similarity for normalized vectors) and L2 distance. IVF (Inverted File) with product quantization in FAISS also supports both. But some quantization schemes — particularly scalar quantization and certain PQ codebooks — assume L2 geometry. If you quantize for cosine similarity, you typically normalize first, then quantize, which adds a normalization step at query time.

# FAISS index for cosine similarity (inner product on normalized vectors)
import faiss

dim = 1536
index = faiss.IndexFlatIP(dim)  # inner product
# Must normalize at add time AND query time
faiss.normalize_L2(vectors)
index.add(vectors)

# FAISS index for Euclidean distance
index_l2 = faiss.IndexFlatL2(dim)
index_l2.add(vectors)  # no normalization needed

DiskANN and ScaNN have similar trade-offs. If you’re using a managed vector database (Pinecone, Weaviate, Qdrant, Milvus), they abstract this away — but the underlying index type still determines which distance functions are efficient. Check the docs before committing.

One practical note: cosine similarity requires storing or computing vector norms. For large-scale systems, that’s either extra storage (a float per vector) or extra compute at query time. Euclidean distance needs neither if you expand the squared L2 formula: ||a-b||^2 = ||a||^2 + ||b||^2 - 2*a·b. You can precompute ||a||^2 at index time and ||b||^2 at query time, reducing the online computation to a single dot product plus two scalar adds.

Comparison table

Dimension Cosine similarity Euclidean distance
Range [-1, 1] (higher = more similar) [0, ∞) (lower = more similar)
Magnitude sensitivity Invariant to scaling Sensitive to scaling
Normalization required Yes, for correct semantics Optional; changes semantics
ANN index support Inner product (IP) / cosine L2 / squared L2
Quantization friendliness Requires pre-normalization Native for most PQ schemes
Interpretability Angle = semantic alignment Distance = combined semantic + magnitude diff
Typical use case Semantic search, deduplication, classification Clustering, anomaly detection, length-aware retrieval
Zero-vector behavior Undefined (division by zero) Well-defined (distance = norm of other)

Common pitfalls

Not normalizing for cosine similarity. This is the single most common bug. If you feed raw embeddings into an inner-product index without normalizing, you’re effectively computing a hybrid of cosine similarity and magnitude comparison. The results will be subtly wrong in ways that are hard to debug — longer documents will rank higher regardless of relevance.

Normalizing for Euclidean distance without realizing what you lost. If you L2-normalize everything and then use Euclidean distance, you’ve thrown away magnitude information. Sometimes that’s intentional. Often it’s accidental — a default in a library or a copy-pasted preprocessing step. Be explicit about what you’re discarding.

Mixing metrics at index and query time. Building an HNSW index with space_type="cosine" but querying with L2 distance (or vice versa) produces garbage results. The graph topology is optimized for one geometry; querying with another breaks the approximation guarantees.

Assuming high-dimensional geometry behaves like 2D/3D. In high dimensions, cosine similarity concentrates around 0 for random vectors, and Euclidean distances concentrate around a constant. The dynamic range shrinks. This affects threshold selection: a cosine threshold of 0.8 might be extremely selective in 768 dimensions but permissive in 512. Calibrate on your actual embedding distribution, not rules of thumb.

Ignoring the zero vector. Cosine similarity is undefined for zero vectors (division by zero). Embedding models rarely produce exact zeros, but padding tokens, failed generations, or preprocessing bugs can. Guard against it:

def safe_cosine(a: np.ndarray, b: np.ndarray, eps: float = 1e-8) -> float:
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    if norm_a < eps or norm_b < eps:
        return 0.0  # or -1.0, depending on your semantics
    return np.dot(a, b) / (norm_a * norm_b)

Which to choose

Semantic search over text — cosine similarity

Users expect “same topic” to rank higher than “same topic but longer.” Query and document lengths vary independently. Cosine similarity aligns with the mental model of relevance. Normalize at index and query time.

Document clustering where scope matters — Euclidean distance

If a 50-page technical spec and a 1-page executive summary on the same feature should land in different clusters, use Euclidean distance on raw (non-normalized) embeddings. The magnitude difference reflects scope difference.

Deduplication and near-duplicate detection — cosine similarity with high threshold

You want to catch paraphrases and minor edits, not length variations. Cosine similarity ≥ 0.95 (calibrated on your data) works well. Normalize first.

Anomaly detection — Euclidean distance

Outliers often have unusual magnitude and direction. Euclidean distance on raw embeddings catches both. Cosine similarity misses magnitude anomalies entirely.

Recommendation with implicit feedback — cosine similarity

User and item embeddings from matrix factorization or two-tower models are typically trained with dot product or cosine objectives. The geometry matches the training loss. Don’t re-normalize if the model already outputs normalized vectors (check the model card).

Cross-modal retrieval (text-to-image, etc.) — cosine similarity

CLIP-style models are trained with contrastive loss on normalized embeddings. The cosine similarity is the training objective. Use inner product search on normalized vectors.

Hybrid search (vector + keyword) — cosine similarity

BM25 scores are roughly cosine-like in spirit (term frequency normalized by document length). Combining cosine-similarity vectors with BM25 via reciprocal rank fusion or weighted sum is more principled than mixing Euclidean distances with BM25.

When you genuinely don’t know — start with cosine similarity

It’s the safer default for semantic tasks. You can always switch to Euclidean distance if magnitude proves informative. The reverse is harder: once you’ve normalized and discarded magnitude, you can’t recover it without re-embedding.


The metric you choose is a modeling decision, not an implementation detail. It encodes your assumption about what “similarity” means for your domain. Make that assumption explicit, test both on a labeled evaluation set if you have one, and document the choice so the next engineer doesn’t have to reverse-engineer it from the index configuration.

Tagscosine-similarityeuclidean-distancevector-distancecomparison

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All cosine similarity & vector distance metrics posts →