Cosine Similarity

The Workhorse of Semantic Search

Cosine similarity measures the angle between two vectors: 1 = same direction (same meaning), 0 = unrelated, -1 = opposite. In practice text embeddings mostly give values between 0 and 1, and what matters is the ranking: which documents are closest to the query.

Cosine and Normalization

Example

Normalize once when you store vectors; then similarity is a cheap dot product at query time:

import math

def normalize(v):
    n = math.sqrt(sum(x * x for x in v))
    return [x / n for x in v]

def cosine(a, b):
    return sum(x * y for x, y in zip(a, b)) / (math.sqrt(sum(x*x for x in a)) * math.sqrt(sum(y*y for y in b)))

docs = {"refund policy": [3, 1, 0, 0], "delivery times": [0, 4, 1, 0], "size chart": [0, 0, 1, 5]}
query = [2, 1, 0, 0]

stored = {k: normalize(v) for k, v in docs.items()}
q = normalize(query)
for name in docs:
    fast = sum(x * y for x, y in zip(q, stored[name]))
    print(f"{name:15} cosine={cosine(query, docs[name]):.3f}  dot-of-normalized={fast:.3f}")

Output

refund policy   cosine=0.990  dot-of-normalized=0.990
delivery times  cosine=0.434  dot-of-normalized=0.434
size chart      cosine=0.000  dot-of-normalized=0.000

Exercise

Exercise: Write top_k(query_vec, vectors, k) that returns the k names with the highest cosine similarity, best first.

Show answer
def top_k(query_vec, vectors, k=2):
    scored = [(cosine(query_vec, v), name) for name, v in vectors.items()]
    scored.sort(reverse=True)
    return [name for _, name in scored[:k]]

print(top_k([2, 1, 0, 0], docs, k=2))   # ['refund policy', 'delivery times']