Pick the distance function that matches how your embeddings behave (normalised or not) and the question you’re answering.
Three ways to measure how close two points are. Cosine measures the angle between them (direction); Euclidean measures the straight-line distance (direction + magnitude); dot product measures both together.
cosine similarity = (A . B) / (||A|| * ||B||)
euclidean dist = sqrt(sum((A_i - B_i)^2))
dot product = sum(A_i * B_i)
When embeddings are L2-normalised:
cosine and dot product are equivalent.
Cosine similarity is angle-only, ignores magnitude, the default for most embeddings. Euclidean (L2) distance is straight-line; sensitive to high-magnitude outliers. Dot product is fastest and equivalent to cosine when embeddings are L2-normalised. Always store embeddings normalised if you mix distance metrics across systems; always use the operator that matches your ANN index’s op-class (cosine index + L2 query = silently ignored index).
import numpy as np
def cos(a, b): return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def l2(a, b): return float(np.linalg.norm(np.array(a) - np.array(b)))
def dot(a, b): return float(np.dot(a, b))
a = np.array([0.6, 0.8])
b = np.array([0.3, 0.4])
print('cosine :', cos(a, b)) # 1.0 -- direction is identical
print('l2 :', l2(a, b))
print('dot :', dot(a, b))
Cosine is the default for normalised or ‘direction matters’ embeddings.
# L2 normalise so dot equals cosine
def l2_normalise(v):
return v / np.linalg.norm(v)
a_n = l2_normalise(a); b_n = l2_normalise(b)
print('cos == dot after L2 normalise?', np.isclose(cos(a_n, b_n), dot(a_n, b_n)))
L2 normalising turns cosine into dot product with one vectorised division; pick one and stay consistent.
# @> containment with embedding metadata
# (covered in detail under pgvector-similarity)
SELECT id FROM chunks
WHERE embedding <=> :query_vec < 0.3 -- cosine distance operator
ORDER BY embedding <=> :query_vec LIMIT 5;
Postgres operators match ANN operator classes: <=> is cosine, <-> is L2, <#> is inner product.
Mixing operators. A cosine index with an L2 query silently returns sequential-scan results. Always pair operator with index op-class.
Cosine for normalised; L2 when magnitude matters; dot product when embeddings are pre-normalised and you want raw speed. Pair operator with ANN op-class.