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.
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.
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.