Back to course overview
Module 1Embeddings fundamentals 11 min

Similarity search

Cosine similarity in practice: what the scores mean, why they're only comparable within a model, and top-k search in ten lines of numpy.

'Close together' needs a number. The standard one is cosine similarity — the angle between two vectors, ignoring their lengths: 1.0 means pointing identically (same meaning), 0 means unrelated. Most embedding APIs return normalized vectors (length 1), where cosine similarity is just the dot product — one multiply-and-sum per comparison, which is why this scales.

One piece of vocabulary you'll use constantly: k = how many results you keep, and 'top-k' = the k highest-scoring matches. top_k(query, docs, k=5) returns the five nearest chunks — the search you run before every answer.

search.pypython
import numpy as np

def top_k(query_vec: np.ndarray, doc_matrix: np.ndarray, k: int = 5):
    """doc_matrix: one row per chunk, vectors normalized.
    Returns (indices, scores) of the k most similar chunks."""
    scores = doc_matrix @ query_vec          # cosine sim via dot product
    idx = np.argsort(-scores)[:k]
    return idx, scores[idx]

# That's the entire search engine at HarborDocs scale. Genuinely.

Reading the scores like a practitioner

  • Scores are relative, not absolute. 0.61 isn't 'B-minus relevance' on a universal scale — score distributions differ per embedding model and per corpus. What's meaningful is ranking and gaps: the top hit at 0.71 with the next at 0.52 says 'clear winner'; five hits packed between 0.58 and 0.61 says 'several candidates, none decisive'.
  • The gap is a signal you'll reuse. A big drop-off after hit 2 → the question is well-covered by two chunks. No drop-off anywhere → the corpus may not contain the answer at all. Module 5 turns this into confidence scoring; Module 6 into an 'unanswerable' detector.
  • Calibrate thresholds empirically. If you want a 'don't even try' floor (below which retrieval is considered failed), pick it by looking at score distributions for known-good and known-bad questions on your corpus — never copy a threshold from a tutorial.

What similarity is not

Semantic similarity is relatedness, not relevance-for-answering. 'How do I cancel my order?' embeds close to a chunk about order cancellation policy — and also close to 'Customers may not cancel after shipment', which is related but answers the opposite direction. Similarity gets the right neighborhood; whether the neighbors actually answer the question is what reranking (Module 3) and grounding rules (Module 5) are for. Keep the layers straight and debugging stays sane.

Prompt to try

I have these five texts: a returns-policy paragraph, a shipping-times paragraph, a bench assembly guide, a marketing blurb about craftsmanship, and a careers-page paragraph. For each of these queries — 'can I send my bench back', 'how long until my stuff arrives', 'is the bench hard to put together' — predict the similarity ranking of the five texts, then explain which pair would be the closest *wrong* neighbor and why.

Prediction before measurement — you'll verify with real vectors in the lab. Building intuition for near-miss neighbors is the skill; they're where RAG errors are born.