Understanding cosine similarity, Euclidean distance, and how machines find relevant text and images.
1. Why do we need semantic search?
Imagine you're building a search feature for your application.
A user searches:
How do I make my application faster?
Your database contains these articles:
- How to optimize database queries
- Improving API performance with caching
- A beginner's guide to JavaScript arrays
- How to bake a chocolate cake
Which articles should appear first?
Traditional keyword search looks for matching words. Since the first two articles don't contain the exact phrase "make my application faster," a basic keyword search might rank them poorly.
Semantic search takes a different approach: it searches for meaning, not just matching words.
It can recognize that "make my application faster," "optimize database queries," and "improve API performance" may describe related ideas.
To understand how semantic search works, we need three concepts:
- Embeddings: Representing text or images as numbers.
- Vectors: Working with those numbers mathematically.
- Similarity and distance: Comparing vectors to rank relevant results.
Let's build these concepts from scratch using simple mathematics and JavaScript.
2. What is a vector?
A vector is an ordered list of numbers.
For example:
const vector = [3, 4];
We can imagine this vector as a point on a two-dimensional graph:
3 represents the horizontal coordinate.4 represents the vertical coordinate.
The point is therefore located at (3, 4).
A vector can have two dimensions, three dimensions, or thousands of dimensions.
For learning purposes, we'll use two-dimensional vectors because they're easy to visualize and calculate by hand.
Important: These examples use illustrative numbers. Real embedding dimensions generally don't have simple, individually interpretable meanings.
3. What is an embedding?
An embedding is a numerical representation of content generated by a machine-learning model.
Imagine an embedding model converts three sentences into vectors:
SentenceIllustrative embedding"Make my app faster"
[0.9, 0.1]"Improve application performance"
[0.8, 0.2]"Chocolate cake recipe"
[0.1, 0.9]
The first two vectors point in similar directions. The third points in a different direction.
This gives us a way to compare content mathematically.
The embedding model learns useful representations during training. Our search system uses those representations to find and rank related content.
A simplified semantic-search pipeline looks like this:
User query
|
v
Embedding model
|
v
Query vector
|
v
Compare with document vectors
|
v
Rank relevant documents
Now let's explore the mathematics behind those comparisons.
4. Vector magnitude: how long is a vector?
Before comparing vectors, let's understand their length, also called their magnitude or L2 norm.
Suppose we have:
a = [3, 4]
The magnitude is calculated using the Pythagorean theorem.
Step 1: Square both coordinates.
‖a‖₂ = √(3² + 4²)
Step 2: Simplify the squares.
‖a‖₂ = √(9 + 16)
Step 3: Add the values and calculate the square root.
‖a‖₂ = √25 = 5
Therefore, the magnitude of vector [3, 4] is 5.
In simple terms, the magnitude tells us how far the point is from the origin, (0, 0).
Implementing magnitude in JavaScript
function magnitude(vector) {
const sumOfSquares = vector.reduce(
(sum, value) => sum + value * value,
0
);
return Math.sqrt(sumOfSquares);
}
console.log(magnitude([3, 4])); // 5
Here's what the code does:
- Squares every value.
- Adds the squared values together.
- Takes the square root of the sum.
We'll reuse this function when calculating cosine similarity.
5. Euclidean distance: how far apart are two vectors?
Magnitude measures the distance from the origin. Euclidean distance measures the distance between two points.
Suppose we have two points.
Point A
a = [1, 2]
Point B
b = [4, 6]
How far apart are they?
First, calculate the difference between their coordinates:
- Horizontal difference: 4 − 1 = 3
- Vertical difference: 6 − 2 = 4
Using the Pythagorean theorem:
d = √(3² + 4²)
d = √(9 + 16)
d = √25 = 5
The Euclidean distance is 5 units.
The general L2 distance formula
For two vectors with the same number of dimensions:
dL2(a, b) = √[Σᵢ₌₁ⁿ (aᵢ − bᵢ)²]
You don't need to memorize the notation immediately. Remember the process:
- Subtract corresponding coordinates.
- Square each difference.
- Add the squared differences.
- Take the square root.
Implementing L2 distance in JavaScript
function l2Distance(a, b) {
if (a.length !== b.length) {
throw new Error("Vectors must have equal dimensions");
}
const sumOfSquares = a.reduce((sum, value, i) => {
const difference = value - b[i];
return sum + difference * difference;
}, 0);
return Math.sqrt(sumOfSquares);
}
const a = [1, 2];
const b = [4, 6];
console.log(l2Distance(a, b)); // 5
How does this help with semantic search?
Imagine our query has already been converted into a vector:
const query = [2, 3];
We also have two document embeddings:
const documentA = [4, 4];
const documentB = [2, 5];
Let's calculate the distance from the query to each document.
Document A
Distance = √((2 − 4)² + (3 − 4)²)
Distance = √(4 + 1)
Distance ≈ 2.236
Document B
Distance = √((2 − 2)² + (3 − 5)²)
Distance = √(0 + 4)
Distance = 2
Document B is closer to the query because 2 is smaller than 2.236.
When ranking by L2 distance, a smaller distance means the vectors are closer together.
However, Euclidean distance isn't the only way to compare vectors. Another popular metric is cosine similarity.
6. Dot product: multiplying corresponding values
The dot product is an operation used in vector mathematics. It also plays an important role in calculating cosine similarity.
Suppose we have two vectors:
a = [2, 3]
b = [4, 5]
Multiply their corresponding values and add the results:
a · b = (2 × 4) + (3 × 5)
a · b = 8 + 15 = 23
The dot product is 23.
Implementing dot product in JavaScript
function dotProduct(a, b) {
if (a.length !== b.length) {
throw new Error("Vectors must have equal dimensions");
}
return a.reduce(
(sum, value, i) => sum + value * b[i],
0
);
}
console.log(dotProduct([2, 3], [4, 5])); // 23
The dot product combines information from corresponding dimensions. When divided by the magnitudes of both vectors, it gives us the cosine of the angle between them.
That is the foundation of cosine similarity.
7. Cosine similarity: are the vectors pointing in the same direction?
Euclidean distance asks:
How far apart are two points?
Cosine similarity asks:
How closely do two vectors point in the same direction?
Its formula is:
cosineSimilarity(a, b) = (a · b) / (‖a‖₂ × ‖b‖₂)
Let's work through an example.
Suppose:
a = [1, 2]
b = [2, 4]
Notice that vector B is twice vector A. Both point in exactly the same direction.
Step 1: Calculate the dot product
a · b = (1 × 2) + (2 × 4) = 10
Step 2: Calculate both magnitudes
For vector A:
‖a‖₂ = √(1² + 2²) = √5
For vector B:
‖b‖₂ = √(2² + 4²) = √20
Step 3: Calculate cosine similarity
cosineSimilarity = 10 / (√5 × √20)
Since √5 × √20 = 10:
cosineSimilarity = 10 / 10 = 1
The cosine similarity is 1, meaning the vectors point in exactly the same direction.
Interpreting cosine similarity
ScoreGeometric interpretation
1Same direction
0.8Strong positive directional alignment
0Perpendicular directions
-1Opposite directions
These are geometric interpretations, not universal thresholds for semantic relevance. Whether a score indicates similar meaning depends on the embedding model and the data.
Implementing cosine similarity in JavaScript
function magnitude(vector) {
return Math.sqrt(
vector.reduce((sum, value) => sum + value * value, 0)
);
}
function dotProduct(a, b) {
if (a.length !== b.length) {
throw new Error("Vectors must have equal dimensions");
}
return a.reduce(
(sum, value, i) => sum + value * b[i],
0
);
}
function cosineSimilarity(a, b) {
const magnitudeA = magnitude(a);
const magnitudeB = magnitude(b);
if (magnitudeA === 0 || magnitudeB === 0) {
throw new Error(
"Cosine similarity is undefined for zero vectors"
);
}
return dotProduct(a, b) / (magnitudeA * magnitudeB);
}
console.log(cosineSimilarity([1, 2], [2, 4])); // 1
console.log(cosineSimilarity([1, 0], [0, 1])); // 0
console.log(cosineSimilarity([1, 0], [-1, 0])); // -1
Cosine similarity is useful because multiplying a nonzero vector by a positive number doesn't change its direction. Consequently, cosine similarity is unaffected by positive scaling of either vector.
This can be helpful when comparing embeddings whose lengths differ.
8. Cosine distance: turning similarity into distance
Sometimes we want a distance metric where lower values indicate a closer match.
A common definition is:
cosineDistance = 1 − cosineSimilarity
Suppose Document A has a cosine similarity score of 0.9.
Distance A = 1 − 0.9 = 0.1
Document B has a cosine similarity score of 0.6.
Distance B = 1 − 0.6 = 0.4
Document A is the closer match under this metric because its cosine distance is smaller.
Implementing cosine distance
function cosineDistance(a, b) {
return 1 - cosineSimilarity(a, b);
}
console.log(cosineDistance([1, 2], [2, 4])); // 0
console.log(cosineDistance([1, 0], [0, 1])); // 1
Remember the distinction:
- Cosine similarity: Higher is better for directional alignment.
- Cosine distance: Lower is better.
- L2 distance: Lower means the points are closer.
Implementations and vector databases may define or report metrics differently, so check the documentation for the system you're using.
9. L2 distance vs. cosine similarity
Let's compare two vectors:
a = [1, 1]
b = [10, 10]
These vectors point in exactly the same direction, but their magnitudes differ.
Their cosine similarity is:
cosineSimilarity(a, b) = 1
Their cosine distance is:
cosineDistance = 1 − 1 = 0
Now calculate their Euclidean distance:
dL2 = √((1 − 10)² + (1 − 10)²)
dL2 = √(81 + 81)
dL2 = √162 ≈ 12.728
The vectors have perfect directional alignment, yet their Euclidean distance is greater than 12.
This illustrates the key difference:
PropertyL2 distanceCosine similarityMeasuresDistance between coordinatesAlignment of directionsAffected by vector magnitude?YesNot by positive scalingBetter scoreSmallerLargerUseful whenCoordinate distance mattersDirectional alignment matters
Neither metric is always better. The right choice depends on how the embedding model was trained and how its vectors should be compared.
10. Build a simple semantic-ranking example
Now let's combine our functions to build a small vector-ranking demonstration in JavaScript.
Imagine an embedding model has already converted a search query and three documents into two-dimensional vectors.
const query = [0.8, 0.6];
const documents = [
{
title: "API performance optimization",
embedding: [0.9, 0.4]
},
{
title: "Database query optimization",
embedding: [0.7, 0.7]
},
{
title: "Chocolate cake recipe",
embedding: [-0.5, 0.8]
}
];
These vectors are invented for demonstration. In a real application, an embedding model would generate them.
Let's rank the documents by cosine similarity.
function magnitude(vector) {
return Math.sqrt(
vector.reduce((sum, value) => sum + value * value, 0)
);
}
function cosineSimilarity(a, b) {
if (a.length !== b.length) {
throw new Error("Vectors must have equal dimensions");
}
const dot = a.reduce(
(sum, value, i) => sum + value * b[i],
0
);
const normA = magnitude(a);
const normB = magnitude(b);
if (normA === 0 || normB === 0) {
throw new Error("Zero vectors are not supported");
}
return dot / (normA * normB);
}
const rankedDocuments = documents
.map(document => ({
title: document.title,
score: cosineSimilarity(query, document.embedding)
}))
.sort((a, b) => b.score - a.score);
console.log(rankedDocuments);
The results are approximately:
[
{
title: "Database query optimization",
score: 0.990
},
{
title: "API performance optimization",
score: 0.975
},
{
title: "Chocolate cake recipe",
score: 0.085
}
]
We sort the results in descending order because higher cosine similarity means stronger directional alignment.
Notice something important: our JavaScript code doesn't understand language. It only compares vectors.
The embedding model is responsible for learning useful representations of language. The ranking code uses those representations to order the results.
Our example demonstrates the mechanics of vector search, not a complete semantic search system.
11. Where do real embeddings come from?
So far, we've manually supplied vectors such as [0.8, 0.6].
Real applications use an embedding model to generate vectors from text or images.
A typical workflow looks like this:
- Choose an embedding model.
- Convert each document into a vector.
- Store the vectors alongside document IDs and content.
- Convert the user's query into a vector using a compatible model.
- Find the nearest document vectors.
- Return the corresponding documents in ranked order.
A JavaScript application might call an embedding API like this:
// Illustrative pseudocode.
// Replace this with your embedding provider's SDK.
const queryEmbedding = await embeddingModel.embed(
"How do I make my application faster?"
);
const documentEmbedding = await embeddingModel.embed(
"Improve API performance with caching."
);
const score = cosineSimilarity(
queryEmbedding,
documentEmbedding
);
console.log(score);
The exact API depends on your embedding provider. The key requirement is that both vectors must come from compatible embedding representations.
For thousands or millions of documents, calculating every distance in application code can become expensive. A vector database or search engine can index embeddings and retrieve nearby candidates more efficiently.
12. Does the same mathematics work for image search?
Yes. The comparison mathematics is the same, but the process that generates the vectors differs.
Imagine you're building an image-search application. A user enters:
A dog playing on a beach.
A compatible text-image model converts the query into an embedding. It also converts the available images into embeddings in a shared vector space.
The application compares the query vector against the image vectors and ranks the images by similarity.
The process looks like this:
Text query
|
v
Text encoder
|
v
Query embedding
|
v
Compare against image embeddings
|
v
Rank matching images
This works directly only when the model is designed to produce comparable text and image embeddings.
Why not simply compare raw pixels?
Two photographs of the same dog might have different lighting, camera angles, cropping, or backgrounds. Raw pixel differences may be large even when the images depict similar content.
Learned image embeddings can capture useful visual patterns that raw pixel distance misses.
However, the model can still make mistakes. A high similarity score is a ranking signal, not proof that an image contains every detail in the query.
13. Why normalized vectors matter
Some embedding models produce vectors with a magnitude of 1. These are called L2-normalized vectors.
For unit-normalized vectors, Euclidean distance and cosine similarity are mathematically related:
L2 distance² = 2 × (1 − cosine similarity)
Equivalently:
L2 distance = √[2 × (1 − cosine similarity)]
This means that ranking the same unit-normalized vectors by L2 distance or cosine similarity produces the same ordering:
- Higher cosine similarity means lower L2 distance.
- Lower cosine similarity means higher L2 distance.
This relationship can be useful when choosing a metric for a vector index.
Don't assume every embedding model automatically normalizes its output. Check the model documentation before relying on this property.
14. What would I use in a real JavaScript application?
For a practical semantic-search feature, I'd separate the system into four responsibilities.
1. Embedding model
Converts text or images into vectors that represent useful features of the content.
2. Vector database
Stores the vectors and retrieves nearby candidates efficiently.
3. Similarity metric
Determines how the system compares vectors, using cosine similarity, Euclidean distance, or another supported metric.
4. Application code
Handles queries, metadata, filtering, ranking, and displaying results to users.
Start with the metric recommended by your embedding model, then test it against real queries and expected results.
For example, if you're building documentation search, create a test set of queries such as "make API faster" and identify which articles should appear first. Measure whether those articles show up in the top five results.
That gives you a practical way to evaluate your search quality rather than choosing a metric based only on intuition.
You may also want hybrid search, which combines vector similarity with keyword search. This can be especially useful when users search for exact identifiers, error messages, version numbers, or technical terms that must match precisely.
Conclusion
Semantic search becomes much easier to understand when we break it into smaller mathematical concepts.
A vector is an ordered list of numbers. An embedding model converts text or images into vectors. Euclidean distance measures how far apart two vectors are, while cosine similarity measures how closely their directions align. Cosine distance converts similarity into a distance-style score.
With a few JavaScript functions, we can implement these calculations and build a simple vector-ranking demonstration.
The most important distinction is this:
Vector mathematics gives us a way to compare representations. The embedding model learns representations that can capture useful relationships in meaning.
Both parts matter when building a useful semantic-search system.#ai #machinelearning #search #software #coding #development #engineering #inclusive #community
How Machines Find Meaning: The Math Behind Semantic Search