To understand how an AI can understand that the word “cat” is similar to “kitten,” you must realize cosine similarity. In short, with the help of embeddings, we can represent words as vectors in a high-dimensional space. If the word “cat” is represented as a vector [1, 0, 0], the word “kitten” would be represented as [1, 0, 1]. Now, we can use cosine similarity to measure the similarity between the two vectors. In this blog post, we will break down the concept of cosine similarity and implement it in TypeScript.
Note
I won’t explain how embeddings work in this blog post, but only how to use them.
Why Cosine Similarity Matters for Modern Web Development
When you build applications with any of these features, you directly work with vector mathematics:
- Semantic search: Finding relevant content based on meaning, not just keywords
- AI-powered recommendations: “Users who liked this also enjoyed…”
- Content matching: Identifying similar articles, products, or user profiles
- Natural language processing: Understanding and comparing text meaning
All of these require you to compare vectors, and cosine similarity offers one of the most effective methods to do so. Let me break down this concept from the ground up.
First, Let’s Understand Vectors
Vectors form the foundation of many AI operations. But what exactly makes a vector?
We express a vector in -dimensional space as:
Each stands for a component or coordinate in the -th dimension.
While we’ve only examined 2D vectors here, modern embeddings contain hundreds of dimensions.
What Is Cosine Similarity and How Does It Work?
Now that you understand vectors, let’s examine how to measure similarity between them using cosine similarity:
Cosine Similarity Explained
Cosine similarity measures the cosine of the angle between two vectors, showing how similar they are regardless of their magnitude. The value ranges from:
- +1: When vectors point in the same direction (perfectly similar)
- 0: When vectors stand perpendicular (no similarity)
- -1: When vectors point in opposite directions (completely dissimilar)
With the interactive visualization above, you can:
- Move both vectors by dragging the colored circles at their endpoints
- Observe how the angle between them changes
- See how cosine similarity relates to this angle
- Note that cosine similarity depends only on the angle, not the vectors’ lengths
Simple Explanation in Plain English
The cosine similarity formula measures how similar two vectors are by examining the angle between them, not their sizes. Here’s how it works in plain English:
-
What it does: It tells you if two vectors point in the same direction, opposite directions, or somewhere in between.
-
The calculation:
- First, multiply the corresponding elements of both vectors and add these products together (the dot product)
- Then, calculate how long each vector is (its magnitude)
- Finally, divide the dot product by the product of the two magnitudes
-
The result:
- If you get 1, the vectors point in exactly the same direction (perfectly similar)
- If you get 0, the vectors stand perpendicular to each other (completely unrelated)
- If you get -1, the vectors point in exactly opposite directions (perfectly dissimilar)
- Any value in between indicates the degree of similarity
-
Why it’s useful:
- It ignores vector size and focuses only on direction
- This means you can consider two things similar even if one is much “bigger” than the other
- For example, a short document about cats and a long document about cats would show similarity, despite their different lengths
-
In AI applications:
- We convert words, documents, images, etc. into vectors with many dimensions
- Cosine similarity helps us find related items by measuring how closely their vectors align
- This powers features like semantic search, recommendations, and content matching
Step-by-Step Example Calculation
Let me walk you through a manual calculation of cosine similarity between two simple vectors. This helps build intuition before we implement it in code.
Given two vectors: and
I’ll calculate their cosine similarity step by step:
Step 1: Calculate the dot product.
Step 2: Calculate the magnitude of each vector.
Step 3: Calculate the cosine similarity by dividing the dot product by the product of magnitudes.
Therefore, the cosine similarity between vectors
and is approximately 0.854, which shows that these vectors point in roughly the same direction.
Why Use Cosine Similarity?
Cosine similarity offers particular advantages in AI applications because:
- It ignores magnitude: You can find similarity between two documents even if one contains many more words than the other
- It handles high dimensions efficiently: It scales effectively to the hundreds or thousands of dimensions used in AI embeddings
- It captures semantic meaning: The angle between vectors often correlates well with conceptual similarity
Building a Cosine Similarity Function in TypeScript
Let me implement a cosine similarity function in TypeScript. This gives you complete control and understanding of the process.
/**
* Calculates the cosine similarity between two vectors
* @param vecA First vector
* @param vecB Second vector
* @returns A value between -1 and 1, where 1 means identical
*/
function function calculateCosineSimilarity(vecA: number[], vecB: number[]): number
Calculates the cosine similarity between two vectorscalculateCosineSimilarity(vecA: number[]
First vectorvecA: number[], vecB: number[]
Second vectorvecB: number[]): number {
// Verify vectors are of the same length
if (vecA: number[]
First vectorvecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length !== vecB: number[]
Second vectorvecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Vectors must have the same dimensions");
}
// Calculate dot product
let let dotProduct: number
dotProduct = 0;
for (let let i: number
i = 0; let i: number
i < vecA: number[]
First vectorvecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length; let i: number
i++) {
let dotProduct: number
dotProduct += vecA: number[]
First vectorvecA[let i: number
i] * vecB: number[]
Second vectorvecB[let i: number
i];
}
// Calculate magnitudes
let let magnitudeA: number
magnitudeA = 0;
let let magnitudeB: number
magnitudeB = 0;
for (let let i: number
i = 0; let i: number
i < vecA: number[]
First vectorvecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length; let i: number
i++) {
let magnitudeA: number
magnitudeA += vecA: number[]
First vectorvecA[let i: number
i] * vecA: number[]
First vectorvecA[let i: number
i];
let magnitudeB: number
magnitudeB += vecB: number[]
Second vectorvecB[let i: number
i] * vecB: number[]
Second vectorvecB[let i: number
i];
}
let magnitudeA: number
magnitudeA = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(let magnitudeA: number
magnitudeA);
let magnitudeB: number
magnitudeB = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(let magnitudeB: number
magnitudeB);
// Handle zero magnitude
if (let magnitudeA: number
magnitudeA === 0 || let magnitudeB: number
magnitudeB === 0) {
return 0; // or throw an error, depending on your requirements
}
// Calculate and return cosine similarity
return let dotProduct: number
dotProduct / (let magnitudeA: number
magnitudeA * let magnitudeB: number
magnitudeB);
}
A More Efficient Implementation
I can improve our implementation using array methods for a more concise, functional approach:
function function cosineSimilarity(vecA: number[], vecB: number[]): number
cosineSimilarity(vecA: number[]
vecA: number[], vecB: number[]
vecB: number[]): number {
if (vecA: number[]
vecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length !== vecB: number[]
vecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Vectors must have the same dimensions");
}
// Calculate dot product: A·B = Σ(A[i] * B[i])
const const dotProduct: number
dotProduct = vecA: number[]
vecA.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, a: number
a, i: number
i) => sum: number
sum + a: number
a * vecB: number[]
vecB[i: number
i], 0);
// Calculate magnitude of vector A: |A| = √(Σ(A[i]²))
const const magnitudeA: number
magnitudeA = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(vecA: number[]
vecA.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, a: number
a) => sum: number
sum + a: number
a * a: number
a, 0));
// Calculate magnitude of vector B: |B| = √(Σ(B[i]²))
const const magnitudeB: number
magnitudeB = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(vecB: number[]
vecB.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, b: number
b) => sum: number
sum + b: number
b * b: number
b, 0));
// Check for zero magnitude
if (const magnitudeA: number
magnitudeA === 0 || const magnitudeB: number
magnitudeB === 0) {
return 0;
}
// Calculate cosine similarity: (A·B) / (|A|*|B|)
return const dotProduct: number
dotProduct / (const magnitudeA: number
magnitudeA * const magnitudeB: number
magnitudeB);
}
Tip
Using Math.hypot() for Performance Optimization
You can optimize vector magnitude calculations using the built-in Math.hypot()
function, which calculates the square root of the sum of squares more efficiently:
function function cosineSimilarityOptimized(vecA: number[], vecB: number[]): number
cosineSimilarityOptimized(vecA: number[]
vecA: number[], vecB: number[]
vecB: number[]): number {
if (vecA: number[]
vecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length !== vecB: number[]
vecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Vectors must have the same dimensions");
}
// Calculate dot product: A·B = Σ(A[i] * B[i])
const const dotProduct: number
dotProduct = vecA: number[]
vecA.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, a: number
a, i: number
i) => sum: number
sum + a: number
a * vecB: number[]
vecB[i: number
i], 0);
// Calculate magnitudes using Math.hypot()
const const magnitudeA: number
magnitudeA = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.hypot(...values: number[]): number
Returns the square root of the sum of squares of its arguments.hypot(...vecA: number[]
vecA);
const const magnitudeB: number
magnitudeB = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.hypot(...values: number[]): number
Returns the square root of the sum of squares of its arguments.hypot(...vecB: number[]
vecB);
// Check for zero magnitude
if (const magnitudeA: number
magnitudeA === 0 || const magnitudeB: number
magnitudeB === 0) {
return 0;
}
// Calculate cosine similarity: (A·B) / (|A|*|B|)
return const dotProduct: number
dotProduct / (const magnitudeA: number
magnitudeA * const magnitudeB: number
magnitudeB);
}
This approach is not only more concise but can be significantly faster, especially for larger vectors, as Math.hypot()
is highly optimized by modern JavaScript engines.
Testing Our Implementation
Let’s see how our function works with some example vectors:
// Example 1: Similar vectors pointing in roughly the same direction
const const vecA: number[]
vecA = [3, 4];
const const vecB: number[]
vecB = [5, 2];
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log(`Similarity: ${cosineSimilarity(const vecA: number[]
vecA, const vecB: number[]
vecB).toFixed(3)}`);
// Output: Similarity: 0.857
// Example 2: Perpendicular vectors
const const vecC: number[]
vecC = [1, 0];
const const vecD: number[]
vecD = [0, 1];
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log(`Similarity: ${cosineSimilarity(const vecC: number[]
vecC, const vecD: number[]
vecD).toFixed(3)}`);
// Output: Similarity: 0.000
// Example 3: Opposite vectors
const const vecE: number[]
vecE = [2, 3];
const const vecF: number[]
vecF = [-2, -3];
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log(`Similarity: ${cosineSimilarity(const vecE: number[]
vecE, const vecF: number[]
vecF).toFixed(3)}`);
// Output: Similarity: -1.000
Mathematically, we can verify these results:
For Example 1:
For Example 2:
For Example 3:
Complete TypeScript Solution
Here’s a complete TypeScript solution that includes our cosine similarity function along with some utility methods:
class class VectorUtils
VectorUtils {
/**
* Calculates the cosine similarity between two vectors
*/
static VectorUtils.cosineSimilarity(vecA: number[], vecB: number[]): number
Calculates the cosine similarity between two vectorscosineSimilarity(vecA: number[]
vecA: number[], vecB: number[]
vecB: number[]): number {
if (vecA: number[]
vecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length !== vecB: number[]
vecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error(`Vector dimensions don't match: ${vecA: number[]
vecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length} vs ${vecB: number[]
vecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length}`);
}
const const dotProduct: number
dotProduct = vecA: number[]
vecA.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, a: number
a, i: number
i) => sum: number
sum + a: number
a * vecB: number[]
vecB[i: number
i], 0);
const const magnitudeA: number
magnitudeA = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(vecA: number[]
vecA.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, a: number
a) => sum: number
sum + a: number
a * a: number
a, 0));
const const magnitudeB: number
magnitudeB = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(vecB: number[]
vecB.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, b: number
b) => sum: number
sum + b: number
b * b: number
b, 0));
if (const magnitudeA: number
magnitudeA === 0 || const magnitudeB: number
magnitudeB === 0) {
return 0;
}
return const dotProduct: number
dotProduct / (const magnitudeA: number
magnitudeA * const magnitudeB: number
magnitudeB);
}
/**
* Calculates the dot product of two vectors
*/
static VectorUtils.dotProduct(vecA: number[], vecB: number[]): number
Calculates the dot product of two vectorsdotProduct(vecA: number[]
vecA: number[], vecB: number[]
vecB: number[]): number {
if (vecA: number[]
vecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length !== vecB: number[]
vecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error(`Vector dimensions don't match: ${vecA: number[]
vecA.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length} vs ${vecB: number[]
vecB.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length}`);
}
return vecA: number[]
vecA.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, a: number
a, i: number
i) => sum: number
sum + a: number
a * vecB: number[]
vecB[i: number
i], 0);
}
/**
* Calculates the magnitude (length) of a vector
*/
static VectorUtils.magnitude(vec: number[]): number
Calculates the magnitude (length) of a vectormagnitude(vec: number[]
vec: number[]): number {
return var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.sqrt(x: number): number
Returns the square root of a number.sqrt(vec: number[]
vec.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((sum: number
sum, v: number
v) => sum: number
sum + v: number
v * v: number
v, 0));
}
/**
* Normalizes a vector (converts to unit vector)
*/
static VectorUtils.normalize(vec: number[]): number[]
Normalizes a vector (converts to unit vector)normalize(vec: number[]
vec: number[]): number[] {
const const mag: number
mag = this.VectorUtils.magnitude(vec: number[]): number
Calculates the magnitude (length) of a vectormagnitude(vec: number[]
vec);
if (const mag: number
mag === 0) {
return var Array: ArrayConstructor
(arrayLength?: number) => any[] (+2 overloads)
Array(vec: number[]
vec.Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length).Array<any>.fill(value: any, start?: number, end?: number): any[]
Changes all array elements from `start` to `end` index to a static `value` and returns the modified arrayfill(0);
}
return vec: number[]
vec.Array<number>.map<number>(callbackfn: (value: number, index: number, array: number[]) => number, thisArg?: any): number[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map(v: number
v => v: number
v / const mag: number
mag);
}
/**
* Converts cosine similarity to angular distance in degrees
*/
static VectorUtils.similarityToDegrees(similarity: number): number
Converts cosine similarity to angular distance in degreessimilarityToDegrees(similarity: number
similarity: number): number {
// Clamp similarity to [-1, 1] to handle floating point errors
const const clampedSimilarity: number
clampedSimilarity = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.max(...values: number[]): number
Returns the larger of a set of supplied numeric expressions.max(-1, var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.min(...values: number[]): number
Returns the smaller of a set of supplied numeric expressions.min(1, similarity: number
similarity));
return var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.acos(x: number): number
Returns the arc cosine (or inverse cosine) of a number.acos(const clampedSimilarity: number
clampedSimilarity) * (180 / var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.PI: number
Pi. This is the ratio of the circumference of a circle to its diameter.PI);
}
}
The angular distance formula uses the inverse cosine function:
Where represents the angle in degrees between the two vectors.
Using Cosine Similarity in Real Web Applications
When you work with AI in web applications, you’ll often need to calculate similarity between vectors. For example:
-
Finding similar products:
function
function findSimilarProducts(product: Product, allProducts: Product[]): Product[]
findSimilarProducts(product: Product
product:type Product = /*unresolved*/ any
Product,allProducts: Product[]
allProducts:type Product = /*unresolved*/ any
Product[]):type Product = /*unresolved*/ any
Product[] { returnallProducts: Product[]
allProducts .Array<Product>.filter(predicate: (value: Product, index: number, array: Product[]) => unknown, thisArg?: any): Product[] (+1 overload)
Returns the elements of an array that meet the condition specified in a callback function.filter(p: Product
p =>p: Product
p.id !==product: Product
product.id) // Exclude the current product .Array<Product>.map<{ product: Product; similarity: any; }>(callbackfn: (value: Product, index: number, array: Product[]) => { product: Product; similarity: any; }, thisArg?: any): { product: Product; similarity: any; }[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map(p: Product
p => ({product: Product
product:p: Product
p,similarity: any
similarity: VectorUtils.cosineSimilarity(product: Product
product.embedding,p: Product
p.embedding) })) .Array<{ product: Product; similarity: any; }>.sort(compareFn?: ((a: { product: Product; similarity: any; }, b: { product: Product; similarity: any; }) => number) | undefined): { product: Product; similarity: any; }[]
Sorts an array in place. This method mutates the array and returns a reference to the same array.sort((
a,a: { product: Product; similarity: any; }
b) =>b: { product: Product; similarity: any; }
b.b: { product: Product; similarity: any; }
similarity: any
similarity -
a.a: { product: Product; similarity: any; }
similarity: any
similarity) // Sort by similarity (highest first) .Array<{ product: Product; similarity: any; }>.slice(start?: number, end?: number): { product: Product; similarity: any; }[]
Returns a copy of a section of an array. For both start and end, a negative index can be used to indicate an offset from the end of the array. For example, -2 refers to the second to last element of the array.slice(0, 5) // Get top 5 similar products .Array<{ product: Product; similarity: any; }>.map<Product>(callbackfn: (value: { product: Product; similarity: any; }, index: number, array: { product: Product; similarity: any; }[]) => Product, thisArg?: any): Product[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map(
result =>result: { product: Product; similarity: any; }
result.result: { product: Product; similarity: any; }
product: Product
product); } -
Semantic search:
function
function semanticSearch(queryEmbedding: number[], documentEmbeddings: DocumentWithEmbedding[]): SearchResult[]
semanticSearch(queryEmbedding: number[]
queryEmbedding: number[],documentEmbeddings: DocumentWithEmbedding[]
documentEmbeddings:type DocumentWithEmbedding = /*unresolved*/ any
DocumentWithEmbedding[]):type SearchResult = /*unresolved*/ any
SearchResult[] { returndocumentEmbeddings: DocumentWithEmbedding[]
documentEmbeddings .Array<DocumentWithEmbedding>.map<{ document: DocumentWithEmbedding; relevance: any; }>(callbackfn: (value: DocumentWithEmbedding, index: number, array: DocumentWithEmbedding[]) => { document: DocumentWithEmbedding; relevance: any; }, thisArg?: any): { document: DocumentWithEmbedding; relevance: any; }[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map(doc: DocumentWithEmbedding
doc => ({document: DocumentWithEmbedding
document:doc: DocumentWithEmbedding
doc,relevance: any
relevance: VectorUtils.cosineSimilarity(queryEmbedding: number[]
queryEmbedding,doc: DocumentWithEmbedding
doc.embedding) })) .Array<{ document: DocumentWithEmbedding; relevance: any; }>.filter(predicate: (value: { document: DocumentWithEmbedding; relevance: any; }, index: number, array: { document: DocumentWithEmbedding; relevance: any; }[]) => unknown, thisArg?: any): { document: DocumentWithEmbedding; relevance: any; }[] (+1 overload)
Returns the elements of an array that meet the condition specified in a callback function.filter(
result =>result: { document: DocumentWithEmbedding; relevance: any; }
result.result: { document: DocumentWithEmbedding; relevance: any; }
relevance: any
relevance > 0.7) // Only consider relevant results .Array<{ document: DocumentWithEmbedding; relevance: any; }>.sort(compareFn?: ((a: { document: DocumentWithEmbedding; relevance: any; }, b: { document: DocumentWithEmbedding; relevance: any; }) => number) | undefined): { document: DocumentWithEmbedding; relevance: any; }[]
Sorts an array in place. This method mutates the array and returns a reference to the same array.sort((
a,a: { document: DocumentWithEmbedding; relevance: any; }
b) =>b: { document: DocumentWithEmbedding; relevance: any; }
b.b: { document: DocumentWithEmbedding; relevance: any; }
relevance: any
relevance -
a.a: { document: DocumentWithEmbedding; relevance: any; }
relevance: any
relevance); }
Using OpenAI Embedding Models with Cosine Similarity
While the examples above used simple vectors for clarity, real-world AI applications typically use embedding models that transform text and other data into high-dimensional vector spaces.
OpenAI provides powerful embedding models that you can easily incorporate into your applications. These models transform text into vectors with hundreds or thousands of dimensions that capture semantic meaning:
// Example of using OpenAI embeddings with our cosine similarity function
async function function compareTextSimilarity(textA: string, textB: string): Promise<number>
compareTextSimilarity(textA: string
textA: string, textB: string
textB: string): interface Promise<T>
Represents the completion of an asynchronous operationPromise<number> {
// Get embeddings from OpenAI API
const const responseA: Response
responseA = await function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch('https://api.openai.com/v1/embeddings', {
RequestInit.method?: string | undefined
A string to set request's method.method: 'POST',
RequestInit.headers?: HeadersInit | undefined
A Headers object, an object literal, or an array of two-item arrays to set request's headers.headers: {
'Authorization': `Bearer ${var process: NodeJS.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
The `process.env` property returns an object containing the user environment.
See [`environ(7)`](http://man7.org/linux/man-pages/man7/environ.7.html).
An example of this object looks like:
```js
{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}
```
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other `Worker` threads.
In other words, the following example would not work:
```bash
node -e 'process.env.foo = "bar"' && echo $foo
```
While the following will:
```js
import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);
```
Assigning a property on `process.env` will implicitly convert the value
to a string. **This behavior is deprecated.** Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
```js
import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'
```
Use `delete` to delete a property from `process.env`.
```js
import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined
```
On Windows operating systems, environment variables are case-insensitive.
```js
import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1
```
Unless explicitly specified when creating a `Worker` instance,
each `Worker` thread has its own copy of `process.env`, based on its
parent thread's `process.env`, or whatever was specified as the `env` option
to the `Worker` constructor. Changes to `process.env` will not be visible
across `Worker` threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of `process.env` on a `Worker` instance operates in a case-sensitive manner
unlike the main thread.env.string | undefined
OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
RequestInit.body?: BodyInit | null | undefined
A BodyInit object or null to set request's body.body: var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.stringify({
model: string
model: 'text-embedding-3-large',
input: string
input: textA: string
textA
})
});
const const responseB: Response
responseB = await function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch('https://api.openai.com/v1/embeddings', {
RequestInit.method?: string | undefined
A string to set request's method.method: 'POST',
RequestInit.headers?: HeadersInit | undefined
A Headers object, an object literal, or an array of two-item arrays to set request's headers.headers: {
'Authorization': `Bearer ${var process: NodeJS.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
The `process.env` property returns an object containing the user environment.
See [`environ(7)`](http://man7.org/linux/man-pages/man7/environ.7.html).
An example of this object looks like:
```js
{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}
```
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other `Worker` threads.
In other words, the following example would not work:
```bash
node -e 'process.env.foo = "bar"' && echo $foo
```
While the following will:
```js
import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);
```
Assigning a property on `process.env` will implicitly convert the value
to a string. **This behavior is deprecated.** Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
```js
import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'
```
Use `delete` to delete a property from `process.env`.
```js
import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined
```
On Windows operating systems, environment variables are case-insensitive.
```js
import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1
```
Unless explicitly specified when creating a `Worker` instance,
each `Worker` thread has its own copy of `process.env`, based on its
parent thread's `process.env`, or whatever was specified as the `env` option
to the `Worker` constructor. Changes to `process.env` will not be visible
across `Worker` threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of `process.env` on a `Worker` instance operates in a case-sensitive manner
unlike the main thread.env.string | undefined
OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
RequestInit.body?: BodyInit | null | undefined
A BodyInit object or null to set request's body.body: var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.stringify({
model: string
model: 'text-embedding-3-large',
input: string
input: textB: string
textB
})
});
const const embeddingA: any
embeddingA = (await const responseA: Response
responseA.Body.json(): Promise<any>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json()).data[0].embedding;
const const embeddingB: any
embeddingB = (await const responseB: Response
responseB.Body.json(): Promise<any>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json()).data[0].embedding;
// Calculate similarity using our function
return VectorUtils.cosineSimilarity(const embeddingA: any
embeddingA, const embeddingB: any
embeddingB);
}
Warning
In a production environment, you should pre-compute embeddings for your content (like blog posts, products, or documents) and store them in a vector database (like Pinecone, Qdrant, or Milvus). Re-computing embeddings for every user request as shown in this example wastes resources and slows performance. A better approach: embed your content once during indexing, store the vectors, and only embed the user’s query when performing a search.
OpenAI’s latest embedding models like text-embedding-3-large
have up to 3,072 dimensions, capturing extremely nuanced semantic relationships between words and concepts. These high-dimensional embeddings enable much more accurate similarity measurements than simpler vector representations.
For more information on OpenAI’s embedding models, including best practices and implementation details, check out their documentation at https://platform.openai.com/docs/guides/embeddings.
Conclusion
Understanding vectors and cosine similarity provides practical tools that empower you to work effectively with modern AI features. By implementing these concepts in TypeScript, you gain a deeper understanding and precise control over calculating similarity in your applications. The interactive visualizations we’ve explored help you build intuition about these mathematical concepts, while the TypeScript implementation gives you the tools to apply them in real-world scenarios. Whether you build recommendation systems, semantic search, or content-matching features, the foundation you’ve gained here will help you implement more intelligent, accurate, and effective AI-powered features in your web applications.