Dot Product
Multiply two lists of numbers position by position and add up the result — a single number saying how much the two point the same way.
Why Does This Exist?
Every time a search over embeddings returns a result, a dot product decided it. Every attention score inside a transformer is a dot product. Every neuron in a dense layer computes one before it does anything else. It is, in raw arithmetic performed on GPUs, very likely the single most-executed operation in machine learning.
The reason it earns that position is that it answers a question no individual number can: do these two things point the same way? Two embeddings of 768 numbers each are impossible to compare by eye. One dot product collapses them to a single score, and that score behaves the way intuition wants — large when the vectors agree, zero when they are unrelated, negative when they oppose.
And it comes with a trap attached, which is the practical reason to read past the definition. Because the dot product mixes direction with magnitude, using it as a similarity score without controlling magnitude produces a retrieval system that quietly ranks by vector length. That bug is common, invisible in unit tests, and fixed by one line.
Think of It Like This
Pushing a trolley at an angle
You are pushing a supermarket trolley. The trolley only rolls forward, but your push comes in at an angle.
Push straight ahead and all of your effort goes into moving it. Push at forty-five degrees and some of your effort is wasted sideways into the handle. Push directly sideways and the trolley does not move at all — you are working hard and achieving nothing forward.
The dot product is exactly the useful part of your push: how much of your effort lies along the direction that matters. Push harder and it grows. Turn more sideways and it shrinks. Turn a full ninety degrees and it hits zero, which is what orthogonal means. Turn further and it goes negative, because you are now pulling the trolley backwards.
The analogy stops at dimensions: a trolley splits effort into two directions, while an embedding dot product splits agreement across hundreds of coordinates at once.
How It Actually Works
There are two formulas, they always agree, and each is useful for a different reason.
The computational one — multiply matching positions, add everything up:
In plain words: pair up the components, multiply each pair, total the results. Both vectors must have the same length , and the answer is always a single scalar — shapes and give shape .
The geometric one — length times length times the angle between them:
In plain words: the sizes of the two vectors, scaled down by how much they disagree in direction. The bars are the L2 length from norms, and is the angle between the vectors.
The second formula is where all the meaning lives. Because runs from down to :
- Same direction (, ) — the maximum possible value.
- At right angles (, ) — exactly zero. The vectors are orthogonal and share nothing.
- Opposite (, ) — maximally negative.
The magnitude problem, and cosine similarity
Look hard at that second formula, because it contains the trap. The dot product is — two length terms and one direction term. A long vector pointing roughly the wrong way can easily beat a short vector pointing exactly the right way.
If what you want is direction only, divide the lengths out:
That is cosine similarity, and it lands in regardless of how long either vector is. It is the right default for comparing embeddings, because embedding magnitude is usually an artifact of training rather than a meaning you want to rank by.
There is a shortcut production systems rely on. If you normalise every vector to length first — making each a unit vector — then and the plain dot product is the cosine. This is why vector databases store normalised embeddings: cosine semantics at dot-product speed.
Worked example
Take and .
Hand-computed componentwise: .
Now check it geometrically. Lengths are and . Since lies flat along the horizontal axis, the angle of above it has . So . The two formulas agree, as they must.
The projection is the piece the diagram draws: how far along direction reaches, which is . Equivalently . That is shadow on .
And the cosine similarity is — a strong match, though not a perfect one.
Code
The final line is the whole lesson: scaling changes the dot product and leaves cosine untouched.
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: # Divide out both magnitudes so only the angle survives. return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
a = np.array([4.0, 3.0]) # shape (2,)b = np.array([5.0, 0.0]) # shape (2,)
print(a @ b) # -> 20.0 the dot productprint(np.linalg.norm(a), np.linalg.norm(b)) # -> 5.0 5.0print(cosine_similarity(a, b)) # -> 0.8print(a @ b / np.linalg.norm(b)) # -> 4.0 projection of a onto b
# Length hijacks the raw dot product: 10*b agrees no better, but scores 10x.print(a @ (10 * b), cosine_similarity(a, 10 * b)) # -> 200.0 0.8Scaling by ten multiplies the dot product by ten to while cosine stays , because the angle never changed.
Watch Out For
Ranking by dot product on unnormalised embeddings
Embedding magnitudes are not uniform. Longer documents, more frequent tokens, and certain fine-tuning regimes all produce systematically longer vectors. Rank by raw dot product and those long vectors win more often — not because they are more relevant, but because they are longer.
What makes it hard to catch: the results are not garbage. They are plausible and mostly reasonable, just worse than they should be. No test fails. The usual symptom is a handful of documents that keep surfacing for unrelated queries.
The fix is one line at write time: normalise every vector to length before storage, then the dot product is cosine similarity and you keep the speed. Do it at ingestion and confirm it — np.linalg.norm(v) should be for everything in the index.
Accumulating a long dot product in low precision
A dot product over 4096 dimensions is 4096 multiplications summed into one accumulator, and in reduced precision that accumulator is the weak point.
float16 maxes out around 65504. Attention scores over a large hidden dimension can exceed that before any softmax normalises them, and the sum becomes inf. From there softmax produces NaN, and one NaN contaminates every parameter it reaches backward. This is a genuine failure mode of naive mixed-precision attention.
Even short of overflow, adding thousands of small float16 numbers loses low-order bits at every step. This is why attention divides scores by before softmax, and why kernels accumulate in float32 even when inputs are float16. If you hand-roll a kernel, accumulate wider than you store.
The Quick Version
- Multiply matching components and sum: two vectors of length give one scalar.
- Equivalently — two magnitude terms and one direction term.
- Positive means broadly aligned, zero means orthogonal, negative means opposed.
- Because magnitude is baked in, raw dot products rank partly by vector length.
- Divide by both lengths to get cosine similarity, which measures direction alone and lands in .
- Normalise vectors once at storage time and the dot product becomes cosine for free. This is what vector databases do.
- The projection of onto is , here .
- Accumulate long dot products in wider precision than you store them in.