MODULE 3  Β·  Embeddings & Vector Search

Vector Similarity: How Machines Measure "Closeness" of Meaning

You've turned text into numbers. Every sentence is now a point in 768-dimensional space. Now how do you find what's similar?

πŸ“… Mar 2026
⏱ 10 min read
🎯 Episode 16 of 98
Vector SimilarityCosine SimilarityEmbeddingsSearch
In this episode

You've turned text into numbers. Every sentence is now a point in 768-dimensional space. Cool.

But how do you know which points are near each other? When someone searches "how to cook biryani," how does the system know that a document about "Hyderabadi dum biryani recipe" is closer than one about "history of Indian railways"?

You need a distance metric β€” a mathematical ruler for meaning. And the one you pick changes everything about your search quality.

The Three Rulers

There are three main ways to measure similarity between vectors. Each sees the world differently.

  1. Cosine Similarity β€” The Angle Measurer

Cosine similarity measures the angle between two vectors. It doesn't care about length β€” only direction.

example
code
B (0.8, 0.6)
                /
              /  ΞΈ = small angle β†’ high similarity
            /
          /

Origin --------β†’ A (0.4, 0.3)

Formula:

cosine_similarity(A, B) = (A Β· B) / (AΓ—B)

In plain English: take the dot product, divide by both magnitudes. Result is between -1 and 1:

Score Meaning

1.0Identical direction (same meaning)
0.0Perpendicular (unrelated)
-1.0Opposite direction (opposite meaning)

Why it works for embeddings: Embedding models produce vectors where direction encodes meaning and magnitude can vary based on text length. Cosine similarity ignores magnitude, focusing purely on what the text means.

python

import numpy as np

def cosine_similarity(a, b):

example
code
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

Example

snippet
code
a = np.array([0.4, 0.3])
b = np.array([0.8, 0.6])  # Same direction, different magnitude
print(cosine_similarity(a, b))  # 1.0 β€” identical meaning!
  1. Dot Product β€” The Magnitude-Aware Scorer

The dot product (also called inner product) multiplies corresponding elements and sums them:

snippet
code
dot(A, B) = A₁×B₁ + Aβ‚‚Γ—Bβ‚‚ + ... + Aβ‚™Γ—Bβ‚™

Unlike cosine similarity, dot product cares about both direction AND magnitude. A longer vector gets a higher score.

python

snippet
code
a = np.array([0.4, 0.3])
b = np.array([0.8, 0.6])
c = np.array([4.0, 3.0])  # Same direction as a, 10x longer
print(np.dot(a, b))  # 0.5
print(np.dot(a, c))  # 2.5 β€” higher because c is longer!

When magnitude matters: If you embed products and the vector magnitude correlates with popularity or importance, dot product naturally boosts popular items. Some recommendation systems exploit this.

Pro tip: If your vectors are normalized (length = 1), cosine similarity and dot product give identical results. Most embedding models output normalized vectors β€” so the choice often doesn't matter.

  1. Euclidean Distance β€” The Straight-Line Measurer

Euclidean distance is the straight-line distance between two points β€” what you learned in school:

snippet
code
distance = √((A₁-B₁)Β² + (Aβ‚‚-Bβ‚‚)Β² + ... + (Aβ‚™-Bβ‚™)Β²)
B β€’
     |  \
     |   \ distance = 5
     |    \
     A β€’---+
       3    4  (3-4-5 triangle)

Important: Lower distance = more similar. This is the opposite of cosine similarity where higher = more similar.

python

def euclidean_distance(a, b):

example
code
return np.linalg.norm(a - b)
snippet
code
a = np.array([1.0, 2.0])
b = np.array([4.0, 6.0])
print(euclidean_distance(a, b))  # 5.0
⚑

Head-to-Head Comparison

MetricRangeHigher =Cares About Magnitude?SpeedMost Used In
Cosine Similarity[-1, 1]More similarNoFastSemantic search, RAG
Dot Product(-∞, +∞)More similarYesFastestRecommendations, ranking
Euclidean Distance[0, +∞)Less similarYesMediumClustering, anomaly detection

πŸ”§

Visual Intuition: When Each Metric Disagrees

Here's the scenario that makes the difference concrete:

example
code
^ dim 2
           |
     C β€’   |        β€’ B (far but same direction as A)
           |      /
           |    /
           |  /
           A β€’
           +--------------------> dim 1
snippet
code
A = "I like cricket"
B = "I like cricket I like cricket I like cricket" (same meaning, longer text)
C = "I enjoy tennis" (different but nearby)
MetricA vs BA vs CWinner for A's nearest neighbor
Cosine1.0 (perfect)0.85B
Euclidean4.2 (far)1.1 (close)C
Dot Product3.8 (high)0.9 (low)B

Cosine and dot product say B is more similar (same meaning). Euclidean says C is more similar (physically closer in space). For text search, cosine is almost always right.

πŸ“Š

The Normalized Vector Trick

Here's a secret most tutorials skip: if you normalize your vectors to unit length, all three metrics become equivalent for ranking.

python

def normalize(v):

example
code
return v / np.linalg.norm(v)
snippet
code
a_norm = normalize(a)
b_norm = normalize(b)

These now give the SAME ranking:

snippet
code
cosine = np.dot(a_norm, b_norm)          # cosine similarity
dot = np.dot(a_norm, b_norm)              # dot product (same!)
euclidean = np.linalg.norm(a_norm - b_norm)  # euclidean (inverse, but same ranking)
Most modern embedding models (OpenAI, Cohere, nomic-embed) return normalized vectors by default. So in practice, cosine similarity = dot product, and you can use whichever your vector database optimizes for. Qdrant, for example, is fastest with dot product on normalized vectors.

The Curse of Dimensionality

One thing that breaks your intuition: in high dimensions, everything looks equally far apart.

In 2D, if you throw 100 random points in a box, some will cluster. In 768 dimensions? The distances between random points converge to nearly the same value.

python

Demonstration

import numpy as np

snippet
code
dims = [2, 10, 100, 768]

for d in dims:

example
code
points = np.random.randn(1000, d)
    distances = []
    for i in range(100):
        for j in range(i+1, 100):
            distances.append(np.linalg.norm(points[i] - points[j]))
    print(f"Dims={d:4d}  Mean dist: {np.mean(distances):.2f}  "
          f"Std: {np.std(distances):.2f}  Ratio: {np.std(distances)/np.mean(distances):.3f}")
snippet
code
Dims=   2  Mean dist: 1.38  Std: 0.52  Ratio: 0.377
Dims=  10  Mean dist: 4.32  Std: 0.65  Ratio: 0.150
Dims= 100  Mean dist: 14.01  Std: 0.71  Ratio: 0.051
Dims= 768  Mean dist: 39.12  Std: 0.72  Ratio: 0.018

At 768 dimensions, the standard deviation is just 1.8% of the mean β€” everything is roughly the same distance apart. This is why cosine similarity works better than Euclidean in high dimensions β€” it ignores the magnitude problem and focuses on angle, which still varies meaningfully.

πŸ’‘

Which Metric Should You Pick?

Decision tree:

Are your vectors normalized?

β”œβ”€β”€ Yes β†’ Use dot product (fastest, equivalent to cosine)

└── No

example
code
β”œβ”€β”€ Semantic search / RAG β†’ Cosine similarity
    β”œβ”€β”€ Recommendations with popularity β†’ Dot product
    └── Clustering / anomaly detection β†’ Euclidean distance

The 90% answer: Use cosine similarity. If your vectors are normalized (most are), use dot product for speed. You'll rarely need Euclidean distance for text applications.

πŸ”¬

Try It Yourself

python

import numpy as np

Three "embeddings" (simplified to 5D)

snippet
code
king   = np.array([ 0.8,  0.3, -0.2,  0.9,  0.1])
queen  = np.array([ 0.75, 0.35, -0.15, 0.85, 0.15])
banana = np.array([-0.1,  0.9,  0.7, -0.3,  0.2])

def all_metrics(a, b):

example
code
cos = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
    dot = np.dot(a, b)
    euc = np.linalg.norm(a - b)
    return f"Cosine: {cos:.3f}  Dot: {dot:.3f}  Euclidean: {euc:.3f}"
snippet
code
print("King vs Queen: ", all_metrics(king, queen))

Cosine: 0.993 Dot: 1.428 Euclidean: 0.137

snippet
code
print("King vs Banana:", all_metrics(king, banana))

Cosine: -0.179 Dot: -0.240 Euclidean: 1.927

King and queen: cosine 0.99 (nearly identical meaning). King and banana: cosine -0.18 (unrelated). The math captures meaning.

πŸ›‘οΈ

Practical Takeaways

Cosine similarity is the default for text β€” it measures direction (meaning) and ignores magnitude (text length)

Normalized vectors make metrics interchangeable β€” most embedding models normalize by default

Dot product is fastest β€” prefer it when vectors are normalized

Euclidean distance struggles in high dimensions β€” the curse of dimensionality makes distances converge

Your vector database choice matters β€” each DB optimizes for specific metrics

When in doubt, use cosine β€” it's the most robust for semantic similarity

πŸ“¦

What's Next?

Episode 17: HNSW Algorithm β€” You have millions of vectors. A user searches and you need the nearest ones in milliseconds. Brute-force comparison is too slow. Enter HNSW β€” the algorithm that makes vector search feel instant, even with billions of vectors.

← Previous

Ep 15: Embeddings

Next β†’

Ep 17: HNSW

Next: Episode 17 β€” HNSW

10 million embeddings. One search query. You need the 10 most similar vectors β€” in under 50 milliseconds.

This is part of a 98-episode series covering AI engineering from tokens to production deployment.

← Previous Ep 15: Embeddings: Turning Words Into Numbers Machines Actuall…