Dot Product and Cosine Similarity
Multiply-and-add two lists. That number, scaled by lengths, is cosine. RAG ranks memory this way.
The dot product of two equal-length vectors is the sum of component times component: a1b1 + a2b2 + .... It is one number.
Algebra: “how much do these lists agree, slot by slot.” Geometry: u · v = |u| |v| cos(theta), where theta is the angle between them.
- If the dot product is positive, the arrows point into a shared half (acute angle).
- If it is zero, they are at a right angle (unrelated, in that geometry).
- If it is negative, they point somewhat opposite.
If you normalize both vectors to length 1, the dot product is cos(theta). That number lives in [-1, 1] and is called cosine similarity. RAG ranks chunks by this score (or a close cousin).
A wrong picture
A wrong picture is: “high cosine means the model agrees with the user” or “the chunk is true.” Cosine means nearby in embedding space. A confident wrong FAQ can sit right next to the query if they share words and topic. Retrieval finds related text, not correct text. Grounding and evals are later jobs.
Another wrong picture is mixing dot product on raw vectors with cosine in the same index. If one document vector is longer because the chunk was longer, raw dot product rewards length. Cosine ignores length and keeps direction. Many APIs already return length-1 vectors; then cosine and dot product match. Silent mismatch is a top production RAG bug.
A third wrong picture is: “distance and cosine always rank the same.” They do not, unless you normalized. Do not mix conventions. If you normalize, use dot product. If you do not, use cosine (normalize inside the formula) or a distance you have tested.
The formula in words
Dot product: multiply matching slots, add the products. Same length required.
Cosine: dot product, then divide by (length of first times length of second). If either length is 0, stop — undefined.
If both lengths are 1, skip the divide: cosine equals the dot product.
Tiny numeric: u = [1, 0], v = [0.6, 0.8]. Dot is 10.6 + 00.8 = 0.6. Lengths are 1 and 1, so cosine is 0.6. w = [0, 1] is at right angles to u: dot 0, cosine 0. r = [-1, 0] is opposite: cosine -1.
u lies on the x-axis. v is the 3-4-5 direction. Multiply matching slots and add: the dot is 0.6.
Two unit arrows: u and vWhy not only distance?
You can rank by distance too: closer points win. Cosine ignores length and keeps direction. That matters when one document vector is longer because the chunk was longer, not because it is more relevant.
Tied scores: two chunks can share a cosine. Break the tie with recency or a trusted source. Geometry should not be the only policy.
Score cutoff: top-k always returns k neighbors, even if every cosine is 0.2. Pair ranking with a threshold from the clip lesson. A winner at 0.82 and a winner at 0.21 are different confidence stories.
Rank a tiny memory bank
An agent stores three memories as 3-d vectors. A new observation arrives. We score every memory and sort. That is retrieval without a vector database: a loop, a sort, a top-k.
Run to execute this in your browser. Nothing is sent to a server.
The query [0.2, 0.85, 0.05] is billing-ish: large middle slot, like “refunds take 5-7 days” [0.1, 0.9, 0.1]. You should see refunds first with cosine near 0.99, then terse answers, then on-call last. If it does not, the vectors are badly chosen — which is how you debug a real embedder: look at neighbors, not at ads.
Change query toward [0.0, 0.1, 1.0] and watch the on-call memory rise. Ranking is geometry plus a sort key. Print the scores, not only the texts.
Hand-check one pair if you want: query dot refunds is 0.20.1 + 0.850.9 + 0.05*0.1 = 0.02 + 0.765 + 0.005 = 0.79. Lengths are a bit over 0.87 and a bit over 0.91. Divide and you land near 0.99. The formula is multiply-and-add, then scale by lengths.
If mag(u) * mag(v) is 0, this code divides by zero. Guard it the way normalize did: refuse zero vectors before you rank.
How agents use this
Tool-using agents retrieve memories, docs, and old traces with this score. If top-k is 4 and the right paragraph is 5th, the model never sees it. That failure is not “the LLM is dumb.” It is a k / embedding / cutoff failure you can measure with (query, must-include-chunk) pairs.
When you log retrieval, log the scores, not only the texts.
- Tokens: the generator never sees a chunk that lost the cosine sort. Improving the prompt poem does not fix a broken neighborhood.
- Ranking: this is ranking. Metadata filters (only
label == billing) run before or after this score. Filters change the candidate set. Cosine ranks whatever is left. - Loss: a linear ranker on embeddings is often trained so that gold chunks get higher dots than distractors. Same multiply-and-add, with a slope on
W. - Sampling: retrieval is usually deterministic given the index (argmax / top-k). Do not confuse it with token sampling. Mixing temperature into retrieval is a different product choice.
If two cosines tie, break the tie with recency or a trusted source. If the whole top-k sits in a blob of similar scores, that is high-entropy retrieval (entropy lesson): ask a clarifying question instead of pretending rank-1 is destiny.
Tip:If two cosines tie, break the tie with recency or a trusted source. Geometry should not be the only policy.
Check your understanding