Embeddings and Vector Search
Encode text, pick a metric, size the vectors, choose an index and fix bad recall. Python, NumPy and FAISS.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Retrieve, then rerank#
From Lesson 11.04, Lesson 05.22, Lesson 05.14
Encode, search, truncate#
encoder = SentenceTransformer("BAAI/bge-small-en-v1.5")384 dimembeddings = encoder.encode(corpus, normalize_embeddings=True)unit vectorsq_emb = encoder.encode([query], normalize_embeddings=True)sims = (embeddings @ q_emb.T).flatten()dot equals cosineorder = np.argsort(-sims)[:top_k]best firstdef truncate(vectors, dim):Matryoshka cut out = vectors[:, :dim] return out / np.linalg.norm(out, axis=1, keepdims=True)renormalizeemb_256 = truncate(emb, 256)index = faiss.IndexFlatL2(dimension)exact searchindex.add(embeddings)distances, indices = index.search(query_vectors, k=5)From Lesson 05.14, Lesson 05.22, Lesson 02.06
Tasks#
| Task | Code |
|---|---|
| Truncate one vector | truncate_matryoshka(vec, |
| Binarize a vector | binarize(vec) |
| Search by Hamming distance | engine.index.search(query_vec, |
| Fuse dense and sparse ranks | rrf_fuse([dense_ranked[:5], |
| Load BGE-M3 | BGEM3FlagModel("BAAI/ |
| Run an analogy | analogy(vocab, |
| Vector for an unseen word | fasttext_vector(word, |
| Evaluate on MTEB tasks | evaluation.run(encoder, |
From Lesson 05.22, 05.22 code/main.py, 11.04 code/embeddings.py, Lesson 05.03, Lesson 05.04
Similarity math#
- Cosine
- \cos(a, b) = \frac{a \cdot b}{\lVert a \rVert \, \lVert b \rVert}. Range -1 to 1. Ignores length.
- Cosine distance
- 1 - \cos(a, b). Range 0 to 2.
- Dot product
- \sum_i a_i b_i. Equals cosine on unit vectors.
- L2
- \sqrt{\sum_i (a_i - b_i)^2}. Smaller is closer.
- Unit vectors
- \lVert a - b \rVert^2 = 2 - 2 \cos(a, b).
- Hamming
- count of different bits after binarize.
- MaxSim
- best doc token per query token, then sum.
- RRF
- \sum \frac{1}{k + \text{rank} + 1} over rankings, rank from 0.
From Lesson 11.04, 11.04 code/embeddings.py, Lesson 01.14, Lesson 04.20, Lesson 05.22, 05.22 code/main.py
HNSW search#
From Lesson 11.04, Lesson 01.14
Choose an index#
| Command | What it does |
|---|---|
IndexFlatIP | Exact inner product. Cosine on unit vectors. About 100k vectors |
IndexFlatL2 | Exact L2, no training. Up to about 1M vectors |
IndexIVFFlat | K cells, searches the closest few. Needs training. About 10M |
IndexIVFPQ | IVF plus product quantization. 100M and more |
IndexHNSW | Graph index. Fastest for many queries. Large index |
| LSH | Random hash projections. Near-duplicate detection |
| KD-trees, ball trees | Exact. Low and medium dimension only |
From Lesson 04.20, Lesson 01.14, Lesson 02.06
Size and compression#
- float32, 1536 dim
- 6,144 bytes per vector. 61 GB for 10 million.
- 3,072 dim
- 12 KB per vector at full precision.
- Binary
- 192 bytes per vector, 32x smaller. 5 to 10 percent recall loss.
- Rescore
- binary first pass, then full precision on the top 1000.
- Matryoshka 256 of 1536
- 6x less storage, about 3 to 5 percent loss.
- Safe cut
- 1,536 to 256 is usually safe. 1,536 to 64 is not.
From Lesson 11.04, Lesson 05.22
Defaults in the code#
| Setting | Default | Note |
|---|---|---|
chunk_size | 200 | Words per chunk, chunk_text |
overlap | 50 | Words shared by two chunks |
top_k | 5 | Results per search |
metric | "cosine" | Also dot, euclidean, hamming |
RRF k | 60 | rrf_fuse |
hash_embed dim | 256 | Hash trick demo embedder |
n_min | 3 | FastText n-grams, n_max 6 |
x_max | 100 | GloVe weighting, alpha 0.75 |
From 11.04 code/embeddings.py, 05.22 code/main.py, Lesson 05.04
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| bge recall 3 to 5 points low | No query prefix | Prepend the prefix from the model card |
| Truncated vectors rank badly | No renormalize after the cut | Divide by the norm again |
| Recall falls after truncation | Cut too far, or no Matryoshka training | Validate on your eval set |
| Nearest neighbors change | Cosine and L2 conventions mixed | Match the training metric |
| Query and doc do not match | Asymmetric model | Check the model card |
cosine: dim mismatch | Vectors of different size | One model and one dim for both |
| Long docs match nothing | One vector averages all topics | Chunk 256 to 512 tokens, 50 overlap |
| Unseen word has no vector | Word2Vec and GloVe have no fallback | Use FastText n-grams |
From Lesson 05.22, 05.22 code/main.py, Lesson 04.20, Lesson 11.04, Lesson 05.03, Lesson 05.04
Abbreviations#
- ANN
- Approximate Nearest Neighbor. Small accuracy loss, large speed gain.
- HNSW
- Hierarchical Navigable Small World. Multi-layer graph index.
- IVF
- Inverted file index. Searches a few clusters only.
- PQ
- Product quantization. Search in compressed space.
- LSH
- Locality-sensitive hashing. Random hash projections.
- MTEB
- Massive Text Embedding Benchmark. 56 tasks at launch, 100+ in v2.
- BEIR
- 18 zero-shot retrieval tasks.
- OOV
- Out of vocabulary. A word not seen in training.
Questions#
- Cosine or dot product?
- On L2-normalized vectors the choice does not matter.
- Why ANN?
- Brute force at 1M by 1536 is 1.5 billion multiply-adds per query.
- Why rerank?
- A cross-encoder reads query and doc together. It is slower but more accurate.
- Why RRF, not a score sum?
- BM25 is unbounded. Cosine is in -1 to 1. Ranks compare.
- Which number for a RAG retriever?
- Recall@k. No right passage, no answer.
From Lesson 11.04, Lesson 01.14, Lesson 19.65, Lesson 05.14, Lesson 04.20
Tips#
- Always set
normalize_embeddings=True. - Benchmark on your own queries. Leaderboard rank is not enough.
- Swap a model only if it wins on your domain by more than 3 points.
- Report recall@1, @5 and @10 side by side.
- Measure p99 latency. MTEB scores hide it.
- For a failing query, diff the sparse and dense rankings.