Vectors, Dot Products & Cosine Similarity
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This closing post is deliberately practical: it lists the six mistakes that most often corrupt cosine-similarity results. The framing is critical — almost none of these throw an error. They return a confident, wrong number, so your search quietly degrades and you blame the embedding model instead of the math around it.
The value of naming them is preventive. Once you've seen the skipped-normalization trap or the distance-versus-similarity flip stated explicitly, you start catching them in your own pipelines before they ship. Fixing these six dramatically improves the reliability of any similarity or retrieval system you build.
Mistake one is using a raw dot product when you actually wanted cosine similarity. The raw dot product is sensitive to magnitude, so a long, verbose vector scores higher simply for being big, regardless of direction. A frequently occurring or wordy document can outrank a near-perfect match purely because its embedding has greater length.
The fix is to normalize the vectors to unit length, or equivalently to use cosine similarity, whenever you care about meaning rather than size. This is the single most common similarity bug, and it's especially insidious because the dot product still returns plausible-looking numbers — they're just ranked by the wrong criterion.
This snippet exposes the magnitude problem concretely. The query [1,1] has a perfect-direction match in short = [1,1], yet the raw dot product gives short a score of 2.0 while the off-topic long = [5,0] scores 5.0. The longer, less-aligned vector wins on raw dot product alone.
The lesson is that raw dot product conflates 'aligned' with 'large,' and large often reflects irrelevant factors like length or token count. Cosine similarity would correctly rank the perfectly-aligned short vector first because it divides out magnitude. Whenever ranking quality surprises you, check whether you're accidentally ranking by size.
Mistake two is confusing similarity with distance, which flips your sort order. Cosine similarity is high (near 1) for good matches, so you sort descending. Cosine distance is defined as 1 minus cosine similarity, so it's low (near 0) for good matches, and you sort ascending. Get the direction wrong and your best results land at the bottom of the list.
The defense is to always confirm which quantity your library or vector database returns before you rank. Some return similarity, some return distance, and the names aren't always obvious. A quick check — 'should a perfect match be near 1 or near 0?' — settles it and prevents a subtle, results-destroying error.
The compare diagram puts similarity and distance side by side so the inversion is unmistakable. For cosine similarity: 1 means identical, 0 means unrelated, you sort descending, and bigger is better. For cosine distance: 0 means identical, 1 means unrelated, you sort ascending, and smaller is better.
Seeing the two columns mirror each other reinforces the diagnostic. When integrating any similarity API, locate which of these two behaviors it follows before writing the ranking code. The visual pairing makes it hard to forget that the two metrics demand opposite sort directions.
Mistake three is the zero vector. Cosine similarity divides by the product of the vectors' lengths, and a zero vector has length zero — so the computation divides by zero, producing NaN or raising an error. Zero vectors arise more often than you'd expect: empty or whitespace-only text, an all-zero embedding from a model edge case, or a feature that got dropped to zeros.
If a zero vector slips into your index unguarded, it can poison rankings silently, propagating NaNs that sort unpredictably. The defense is to guard the division explicitly, or to filter out zero-length vectors before indexing. This is a small check that prevents a confusing class of failures.
This snippet shows the standard guard: compute both norms, and divide by the maximum of their product and a tiny epsilon (1e-8) so you never divide by exactly zero. Calling safe_cosine with a zero vector returns 0.0 instead of crashing or returning NaN.
The epsilon technique is a common numerical-stability pattern you'll see throughout ML — adding a tiny floor to a denominator to avoid division by zero. It's mathematically harmless for normal inputs (the epsilon is negligible next to real norms) and protective for degenerate ones. Wrapping your cosine in a guard like this makes a pipeline robust to messy real-world data.
Mistake four comes in two flavors. The loud version: cosine requires equal-length vectors, so comparing a 384-dimensional query against 768-dimensional documents throws an error immediately. That one is annoying but self-announcing. The quiet version is more dangerous: embeddings produced by two different models occupy entirely different vector spaces, so comparing them is meaningless even if the dimensions happen to match.
The defense is to embed everything — queries and documents alike — with a single, consistent model. Mixing models, or upgrading your embedding model without re-embedding the existing index, silently destroys similarity quality because the vectors no longer live in a comparable space. One model, one space.
Mistake five is forgetting to normalize before indexing. If you store raw vectors and your search assumes the dot product equals cosine similarity, every score is silently length-weighted — you're back to mistake one, but baked into your whole index. The results look plausible and are subtly wrong across the board.
The fix is to normalize vectors to unit length once, at insert time, so all later searches are pure cosine. Many vector databases either expect pre-normalized vectors for their cosine mode or normalize internally — know which, because assuming the wrong one reintroduces the magnitude bias everywhere at once.
Mistake six is reading a vector's magnitude as importance or confidence. A longer embedding vector is not a 'stronger' signal or a more relevant result; magnitude usually reflects incidental factors like token count or model artifacts rather than meaning. Cosine similarity deliberately discards magnitude for exactly this reason.
The trap is to look at raw vector norms and infer significance from them, or to skip normalization because 'bigger seems more important.' That's precisely the information cosine throws away on purpose. Treat direction as the carrier of meaning and length as noise to be normalized out, unless you have a specific, well-justified reason to do otherwise.
These habits collectively neutralize all six mistakes. Normalize vectors before comparing so you're truly computing cosine. Confirm whether your metric is similarity (sort high) or distance (sort low). Guard against zero vectors with an epsilon. Use one model so all vectors share a space and dimensionality. And treat direction, not length, as the meaning.
None of these require advanced math — they're disciplines. Adopting them early is what separates a similarity pipeline that mysteriously underperforms from one you can trust and debug quickly. They compound: each habit makes the others easier to maintain and your retrieval results steadily more reliable.
The teaser closes the day and bridges to the next. With the concept (Post 1), the motivation (Post 2), the mechanics (Post 3), a hands-on build (Post 4), and the pitfalls (Post 5) all covered, you have a working command of vectors, dot products, and cosine similarity. The next day builds on this foundation to go deeper into the math that powers machine learning.