Embeddings and Vector Space, Illustrated
The first search box I built over a help center used a database and a LIKE query. It looked great in the demo, because I typed the exact phrases I had written the articles around. Then real users showed up. Someone typed “the app won’t let me back in” and got nothing, because the article that answered them was titled “Troubleshooting login failures” and shared exactly zero words with what they typed.
That is the whole problem in one sentence. Keyword search matches strings. People search by meaning. “Back in” and “login,” “cancel” and “end your subscription,” “it crashed” and “the process exited unexpectedly” mean the same thing to a human and look like unrelated noise to a string match.
Embeddings are how you close that gap. The trick underneath is small and, once you see it, kind of beautiful: turn each piece of text into a list of numbers, positioned so that things that mean similar things end up near each other. Then “find related text” becomes “find nearby points,” which is just geometry. That one idea powers semantic search, retrieval for RAG, recommendations, deduplication, and clustering. The specific models keep getting better every few months. The idea has been stable for years and is not going anywhere.
Meaning as a position
Start with a picture you can actually hold in your head. Imagine a flat map, and imagine placing every word on it so that related words land close together. Fruits cluster in one corner. Vehicles cluster in another. Royalty gathers somewhere else. The distance between two points on the map means something: close is “similar,” far is “unrelated.”
On a 2D map you can only fit so much before everything collides. There is not enough room to express that “bank” (river) and “bank” (money) are different, that “hot” relates to both temperature and spice, that a laptop is closer to a phone than to a banana but not by much. Two coordinates run out fast.
So real embeddings do not use two numbers. They use hundreds or thousands. A modern embedding model might output a vector of 768, 1024, or 3072 numbers per input. You cannot draw that, and you should not try. But every intuition from the 2D map carries straight up: each input is a point in that high-dimensional space, similar meanings sit close, and “close” is something you compute. The map is real, you just can’t see it.
Meaning, not spelling
Here is the part that makes embeddings worth the trouble. They track what text means, not how it is spelled. Those two things come apart constantly, and keyword search only ever sees the spelling.
“Car” and “automobile” share not a single letter, yet any decent embedding model puts them almost on top of each other, because they mean the same thing. Meanwhile “apple” and “Apple” share every letter, and a good model pulls them apart once there is context, because a fruit and a phone company are not the same thing at all. String matching gets both of these exactly backwards.
Where the numbers come from
An embedding model is a close cousin of the chat models from what a language model actually does. Same transformer machinery underneath. The difference is the last step. A chat model ends in a head that predicts the next token. An embedding model ends in a head that squashes the entire input down into one fixed-length vector, and it was trained so that texts meaning similar things come out with similar vectors, and texts meaning different things come out far apart.
You do not need to know the training details to use it. What you do need is the shape of the thing. You hand the model a string, it hands you back an array of floats.
const v = await embed("Ending your subscription");
v.length; // 1024 <- this model always returns exactly 1024 numbers
v.slice(0, 4); // [0.0213, -0.0447, 0.1106, -0.0067]
Three properties of that call matter more than anything else you will read here.
The length is fixed. This model returns 1024 numbers for the word “hi” and 1024 numbers for a 500-word paragraph. The dimension count is a property of the model, not of your input. Pick a model and every vector it ever gives you is the same length, which is exactly what lets you store them all in one table and compare them.
The same text gives the same vector. Embed “Ending your subscription” today and again next week through the same model, and you get the same numbers (near enough to treat as identical). That is why the two-lane picture above works: you embed your documents once, offline, store the vectors, and never pay to embed them again. Only fresh queries get embedded at search time, and a query is short. Embedding is usually far cheaper per token than a chat completion, and you pay the document side exactly once.
Same model on both sides, always. This is the rule people trip over first. The geometry only means something if the query and the documents were placed in the same space by the same model. Embed your documents with one model and your queries with another and you are comparing coordinates from two different maps. The numbers still compute, the search still returns something, and the results are quietly garbage. Change your embedding model and you must re-embed every document you already stored. There is no shortcut.
Closeness, measured
The map says “close means similar.” Time to make “close” a number. You have two vectors, and you want a single score for how aligned they are. The near-universal choice for text is cosine similarity: the cosine of the angle between the two vectors.
Why the angle and not plain straight-line distance? Because the direction of an embedding carries the meaning, and its length mostly carries stuff you do not care about, like how long the text was. Two documents about the same topic point the same way even if one is a tweet and the other is an essay. Cosine looks only at direction, so it is not fooled by length.
In code it is about six lines, and there is nothing magic in them:
function cosine(a, b) {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]; // how aligned
na += a[i] * a[i]; // length of a, squared
nb += b[i] * b[i]; // length of b, squared
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
Many embedding models already hand you normalized vectors, meaning every vector has length 1. When that is true, Math.sqrt(na) and Math.sqrt(nb) are both 1, and cosine similarity collapses into the plain dot product, a[i] * b[i] summed up. That is why you will see production code skip the division entirely and just do a dot product: the normalization was baked in ahead of time. The mechanics of picking a metric and finding the nearest vectors fast get their own treatment in similarity and nearest neighbours.
Semantic search, start to finish
Put the pieces together and you have semantic search, and it is shorter than you would guess. Embed the documents once. Embed the query. Score the query against every document with cosine. Return the top few. That is the entire algorithm.
Here it is as JavaScript. embed is whatever provider or local model you use; the logic around it is yours and it is tiny.
// once, offline: embed every document and keep the vectors next to it
const index = [];
for (const doc of docs) {
index.push({ doc, vec: await embed(doc.text) });
}
// per search: embed the query, score everything, take the top k
async function search(query, k = 5) {
const q = await embed(query);
return index
.map(({ doc, vec }) => ({ doc, score: cosine(q, vec) }))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}
That loop is honest and it works, and you should understand it before reaching for anything fancier. It is also O(n) per query: it compares the query against every stored vector, one at a time. Fine for a few thousand documents. Painful at a million, and hopeless at a billion. Past a certain size you stop scanning and start using a vector index (an approximate nearest-neighbour structure) inside a database, which is what vector search with pgvector is about. Same geometry, just found fast.
Play with the geometry directly. Below is a toy space with a handful of words at hand-picked positions. Click any word and its three nearest neighbours light up, measured by actual distance. Watch how the clusters behave, and notice where the two “apple” points land.
Nothing in that demo calls a model. The positions are hand-assigned so you can see the mechanism cleanly. A real embedding model is just the thing that assigns those coordinates for you, from text, in a space you cannot draw.
The facts that will bite you
The concept is simple. The operational details are where people lose a day. Four of them, in rough order of how often they cause pain.
There is always a token limit, so long text gets chunked
An embedding model reads its input as tokens, and it has a maximum it will accept. As of 2026 that ceiling varies a lot by model: some cap out around 512 tokens, plenty sit near 8,000, and a few newer ones stretch to 32,000 or beyond. Whatever the number, feeding a whole 40-page PDF into one embedding call is not the move. Even when a model technically accepts a long input, squashing an entire document into a single vector blurs it. One vector cannot be “close” to a hundred different topics at once, so a query that matches page 30 gets drowned out by the other 39 pages.
The fix is chunking: split the document into smallish passages, embed each one, and store them as separate rows that point back at their source. Retrieval then finds the specific passage that answers the query, not the whole document. Chunking well (where to cut, how much overlap, keeping related sentences together) is fiddly enough to deserve its own article, chunking text for retrieval.
Fixed dimensions mean storage is predictable, and it adds up
Every vector from a given model is the same length, so the storage math is refreshingly simple, and also a little alarming at scale. A 1024-dimension vector stored as 32-bit floats is 1024 x 4 = 4096 bytes, about 4 KB, per item. That is nothing for one row. Multiply by a billion and it is 4 terabytes, before you have stored a single word of the actual text or built a single index.
This is why quantization has become standard practice. You trade a little precision for a lot of space by storing each number in fewer bits.
There is a related trick called Matryoshka embeddings (named after the nesting dolls). Models trained this way front-load the most important information into the earliest dimensions, so you can chop a 1024-vector down to 256 and keep most of the quality, no re-embedding needed. Between quantization and truncation you can often cut storage and search cost by 4 to 30 times while giving up a few percent of accuracy. Whether that trade is worth it depends entirely on your scale and your recall needs, so treat those percentages as things to measure on your own data, not gospel.
The vector is not the text
Store the vector and you have stored a fingerprint of the meaning, not the words. You cannot reverse a vector back into the sentence it came from. So your vector store always keeps the original text (or a pointer to it) right next to the vector, exactly like the { doc, vec } pairs in the search loop above. The vector is for finding; the text is for showing and for feeding to a model afterward.
Everything is versioned to the model
Say it once more because it is the quiet killer: a stored vector only makes sense in the space of the exact model that produced it. Upgrade to a newer, better embedding model and every vector already in your database is now speaking a different language than your fresh queries. There is no partial migration. You re-embed the whole corpus, or you keep both models running side by side during a cutover. Budget for this when you choose a model, because you will want to upgrade eventually.
What embeddings quietly get wrong
Embeddings are not the model “understanding” your text in any deep sense. They are a similarity geometry, learned from patterns in training data, and that framing tells you exactly where they fail.
They are built to reward meaning overlap, which means they are weak precisely where meaning is not the point. Search for the order id INV-4471-Q3 and semantic similarity is worse than useless: that string has no “meaning” to be near, and the model will happily hand you five other invoice-shaped strings that are close in vector space and wrong. Rare product codes, exact error strings, people’s names, a specific function name in a codebase: these are cases where you want the literal characters, and an embedding will smear them into whatever looks meaning-adjacent.
That mirror-image failure is the whole reason hybrid search exists: run a keyword search and an embedding search together, then merge and re-rank the two result lists so exact matches and meaning matches both get a fair shot. In practice a hybrid setup beats either half on its own for most real corpora, and it is covered in hybrid search and reranking. The lesson to carry forward is not “embeddings are flawed.” It is “embeddings are one lens, they blur exact tokens, and the fix is to pair them with a lens that does not.”
A couple of smaller cautions worth holding onto. Embeddings can be confidently wrong about negation: “safe for cats” and “not safe for cats” can land unnervingly close, because most of the words match and the model weights that heavily. And embeddings inherit whatever associations were in their training data, biases included, so two phrases can sit closer or farther than you would like for reasons that have nothing to do with your use case. Neither is a dealbreaker. Both are reasons to test retrieval against real queries instead of trusting that “the vectors will just work.”
Summary
- An embedding turns a piece of text into a fixed-length vector of numbers, positioned so that similar meanings land close together and unrelated things land far apart. Search, RAG, recommendations, and clustering all sit on top of this one idea, which has been stable for years even as the models keep improving.
- The vectors come from an embedding model, a cousin of chat models with a different last step: it pools the whole input into one vector trained so related texts point the same way.
- Distance is measured with cosine similarity, the angle between two vectors, because direction carries the meaning and length mostly does not. With normalized vectors, cosine is just the dot product.
- Semantic search is short: embed documents once and store them, embed the query, score with cosine, return the closest few. The naive scan is fine for thousands of items; past that you need a vector index.
- Use the same model (and version) for documents and queries, or the geometry is meaningless. Change models and you re-embed everything.
- Embedding inputs have a token limit, so long text is chunked into passages. Vectors are cheap to store individually but add up fast at scale, which is why quantization and Matryoshka truncation exist.
- Embeddings track meaning, not spelling, so they miss exact keywords, rare ids, and error codes, and they can fumble negation. Pairing them with keyword search (hybrid search) covers the gap.