What you'll learn
Quick Answer
A vector embedding is a fixed-length list of numbers that represents the meaning of a piece of text (or an image, or audio). An embedding model is trained so that inputs with similar meaning get vectors pointing in similar directions. You compare two embeddings with cosine similarity: near 1 means very similar, near 0 means unrelated. This is what powers semantic search, recommendations, and the retrieval step in RAG.
What an embedding is
Feed a sentence to an embedding model and it returns something like [0.021, -0.044, 0.109, ...] - a list of floating-point numbers, always the same length for a given model. That length is commonly a few hundred to a few thousand values; two widely used text models produce 1536 and 3072 numbers respectively.
Each number is a coordinate in a high-dimensional space. The training objective is what makes it useful: the model is optimized so that texts meaning similar things land near each other in that space, and unrelated texts land far apart. The individual numbers are not human-readable - there is no "dimension 7 = formality" - but distances and directions between whole vectors carry meaning. The same idea applies to images and audio: any embedding model maps its inputs into a space where nearness means similarity.
Cosine similarity
The standard way to compare two embeddings is the cosine of the angle between them: the dot product divided by the product of their lengths. It ranges from -1 to 1. With real embedding models, similar texts score close to 1, loosely related texts land in the middle, and unrelated texts sit near 0 (negative scores are rare in practice for modern text models).
Cosine measures direction, not magnitude, so a long document and a short phrase about the same topic can still score high. Many embedding APIs return vectors already normalized to length 1, in which case the denominator is 1 and cosine similarity is just the dot product - which is why vector databases can compare millions of vectors so fast.
Worked example with real numbers
Here are four toy vectors, hand-built over six made-up dimensions - roughly [pet, feline, canine, finance, currency, market]. This code runs in plain Node, no libraries:
const vectors = {
cat: [0.9, 0.9, 0.1, 0.0, 0.0, 0.0],
kitten: [0.9, 0.85, 0.1, 0.0, 0.0, 0.05],
dog: [0.9, 0.1, 0.9, 0.0, 0.0, 0.0],
bank: [0.0, 0.0, 0.0, 0.9, 0.8, 0.6],
};
function dot(a, b) {
return a.reduce(function (sum, ai, i) { return sum + ai * b[i]; }, 0);
}
function magnitude(a) {
return Math.sqrt(dot(a, a));
}
function cosineSimilarity(a, b) {
return dot(a, b) / (magnitude(a) * magnitude(b));
}
console.log("cat vs kitten:", cosineSimilarity(vectors.cat, vectors.kitten).toFixed(3));
console.log("cat vs dog: ", cosineSimilarity(vectors.cat, vectors.dog).toFixed(3));
console.log("cat vs bank: ", cosineSimilarity(vectors.cat, vectors.bank).toFixed(3));
console.log("dog vs bank: ", cosineSimilarity(vectors.dog, vectors.bank).toFixed(3));Actual output:
cat vs kitten: 0.999
cat vs dog: 0.607
cat vs bank: 0.000
dog vs bank: 0.000"cat" and "kitten" point almost the same way (0.999). "cat" and "dog" share the pet dimension but differ on feline versus canine, so they land partway (0.607). "cat" and "bank" have no overlap and score 0.000. A real embedding model does this over hundreds or thousands of learned dimensions instead of six hand-set ones, but the comparison math is exactly what you just ran.
What embeddings are used for
- Semantic search. Embed every document once, embed the query, return the nearest documents. It matches on meaning, so "how do I reset my password" finds an article titled "Recovering account access" even with no shared keywords.
- Clustering and deduplication. Group texts whose vectors are close together; flag near-duplicate support tickets or catalogue entries.
- Recommendations. Surface items whose embeddings are near ones a user already liked.
- Classification and routing. Embed the input and compare it to labelled examples, or to a short description of each category, and pick the nearest.
- Retrieval for RAG, which is semantic search whose results are pasted into an LLM prompt.
The common thread: any time the task is "find things that mean something similar to this", embeddings turn it into nearest-neighbour math that scales to millions of items.
How you actually get embeddings
In practice you call an embedding endpoint - hosted by a model provider, or a small open model you run yourself - with a string (or a batch of strings) and get back the vectors. Embedding calls are much cheaper and faster than chat calls, often a tiny fraction of a cent per thousand tokens, so embedding a whole document set is usually affordable.
Two habits save pain later. Cache embeddings keyed by a hash of the input text so you never pay to embed the same chunk twice, and re-embed only what changed. And store the model name and version alongside every vector - the day you upgrade the embedding model, every old vector becomes incomparable with new ones, and you will want to know exactly which rows need reprocessing.
Gotchas
- Embeddings from different models are not comparable. Index with one model and query with another and the numbers are meaningless together. Re-embed everything when you change models.
- Similarity scores are relative, not absolute. One model might put genuinely related texts at 0.35 and near-identical ones at 0.55; another spreads the same pairs across 0.7 to 0.95. Calibrate a threshold on your own data - do not copy a "0.8 means similar" rule from a blog post.
- Cosine similarity captures topic, not truth or stance. "The drug is effective" and "the drug is not effective" embed very close together - same topic, opposite meaning.
- Long inputs are silently truncated at the model's token limit before embedding. A 50-page document embedded whole is really just its first few pages; chunk it first.
