Skip to content
AI360Xpert
Math

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.

The dot product measures how much of one vector lies along another: drop a perpendicular from the tip of a onto the line of b and the shadow it casts is the projection, four units long here
The dot product measures how much of one vector lies along another: drop a perpendicular from the tip of a onto the line of b and the shadow it casts is the projection, four units long here

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 terms of raw arithmetic performed on the planet's 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, it is invisible in unit tests, and it is 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.

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:

ab=i=1naibia \cdot b = \sum_{i=1}^{n} a_i b_i

In plain words: pair up the components, multiply each pair, total the results. Both vectors must have the same length nn, and the answer is always a single scalar — shapes (n,)(n,) and (n,)(n,) give shape ()().

The geometric one — length times length times the angle between them:

ab=abcosθa \cdot b = \lVert a \rVert \, \lVert b \rVert \cos\theta

In plain words: the sizes of the two vectors, scaled down by how much they disagree in direction. The \lVert \cdot \rVert bars are the L2 length from norms, and θ\theta is the angle between the vectors.

The second formula is where all the meaning lives. Because cosθ\cos\theta runs from 1 down to 1-1:

  • Same direction (θ=0\theta = 0, cosθ=1\cos\theta = 1) — the maximum possible value.
  • At right angles (θ=90°\theta = 90°, cosθ=0\cos\theta = 0) — exactly zero. The vectors are orthogonal and share nothing.
  • Opposite (θ=180°\theta = 180°, cosθ=1\cos\theta = -1) — maximally negative.

The magnitude problem, and cosine similarity

Look hard at that second formula, because it contains the trap. The dot product is abcosθ\lVert a \rVert \lVert b \rVert \cos\thetatwo 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:

cosθ=abab\cos\theta = \frac{a \cdot b}{\lVert a \rVert \, \lVert b \rVert}

That is cosine similarity, and it lands in [1,1][-1, 1] 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 worth knowing, because production systems rely on it. If you normalise every vector to length 1 first — making each a unit vector — then ab=1\lVert a \rVert \lVert b \rVert = 1 and the plain dot product is the cosine. This is why vector databases store normalised embeddings: it buys cosine semantics at dot-product speed.

Worked example

Take a=[4,3]a = [4, 3] and b=[5,0]b = [5, 0].

Componentwise: ab=(4×5)+(3×0)=20a \cdot b = (4 \times 5) + (3 \times 0) = 20.

Now check it geometrically. The lengths are a=16+9=5\lVert a \rVert = \sqrt{16 + 9} = 5 and b=5\lVert b \rVert = 5. Since bb lies flat along the horizontal axis, the angle of aa above it has cosθ=4/5=0.8\cos\theta = 4/5 = 0.8. So abcosθ=5×5×0.8=20\lVert a \rVert \lVert b \rVert \cos\theta = 5 \times 5 \times 0.8 = 20. The two formulas agree, as they must.

The projection is the piece the diagram draws: how far along bb's direction aa reaches, which is acosθ=5×0.8=4\lVert a \rVert \cos\theta = 5 \times 0.8 = 4. Equivalently ab/b=20/5=4a \cdot b / \lVert b \rVert = 20/5 = 4. That is aa's shadow on bb.

And the cosine similarity is 20/(5×5)=0.820 / (5 \times 5) = 0.8 — a strong match, though not a perfect one.

Show Me the Code

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 with a no better, but scores 10x.print(a @ (10 * b), cosine_similarity(a, 10 * b))  # -> 200.0 0.8

That final line is the whole lesson. Scaling b by ten multiplies the dot product by ten while the cosine similarity does not budge, because the two vectors still point at exactly the same angle to each other.

Watch Out For

Ranking by dot product on unnormalised embeddings

This is the most common retrieval bug there is, and it does not announce itself.

Embedding magnitudes are not uniform. Longer documents, more frequent tokens, and certain fine-tuning regimes all produce systematically longer vectors. If you rank candidates by raw dot product, those long vectors win more often — not because they are more relevant, but because they are longer. Your search quietly becomes a popularity contest.

What makes it hard to catch: the results are not garbage. They are plausible, related, mostly reasonable, and just noticeably 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 1 before you store it, then the dot product is cosine similarity and you keep the speed. Do it at ingestion, not at query time, and confirm it — np.linalg.norm(v) should be 1.0 for everything in the index.

Accumulating a long dot product in low precision

A dot product over 4,096 dimensions is 4,096 multiplications summed into one accumulator, and in reduced precision that accumulator is the weak point.

float16 maxes out around 65,504. Attention scores over a large hidden dimension can exceed that before any softmax gets a chance to normalise them, and the sum becomes inf. From there the softmax produces NaN, and one NaN contaminates every parameter it reaches on the backward pass. This is a genuine failure mode of naive mixed-precision attention, not a hypothetical.

Even short of overflow, precision degrades: adding 4,096 small float16 numbers loses low-order bits at every step, and the error accumulates with the count. This is why the transformer's attention divides scores by dk\sqrt{d_k} before the softmax, and why kernels accumulate in float32 even when their inputs are float16. If you hand-roll an attention or similarity kernel, accumulate wider than you store.

The Quick Version

  • Multiply matching components and sum: two vectors of length nn give one scalar.
  • Equivalently abcosθ\lVert a \rVert \lVert b \rVert \cos\theta — 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 [1,1][-1, 1].
  • Normalise vectors once at storage time and the dot product becomes cosine for free. This is what vector databases do.
  • The projection of aa onto bb is ab/ba \cdot b / \lVert b \rVert.
  • Accumulate long dot products in wider precision than you store them in.

Related concepts