AI & Machine Learning Cheatsheet
RAG and Vector Search
Use this AI & Machine Learning reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
What is RAG?
Retrieval-Augmented Generation (RAG) solves a key LLM limitation: hallucination and knowledge cutoffs. Instead of relying solely on parameters baked during pre-training, the model retrieves relevant documents at query time and uses them as context.
Basic RAG flow:
Query → Embed query → Vector search → Top-k relevant chunks
→ Augment prompt with chunks → LLM generates answer grounded in retrieved contextBenefits: - Up-to-date knowledge without retraining - Citations and verifiability - Reduced hallucination on factual questions - Domain customization without fine-tuning - Cheaper than full fine-tuning for knowledge updates
Vector Embeddings
A vector embedding maps text (or images, audio) to a dense vector in a latent space where semantic similarity ≈ geometric closeness.
from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer("BAAI/bge-large-en-v1.5") # 1024-dim embeddings # Embed documents docs = [ "Python is a high-level programming language", "Photosynthesis converts sunlight to glucose", "Neural networks are inspired by the brain" ] embeddings = model.encode(docs, normalize_embeddings=True) # shape: (3, 1024) # Semantic similarity via dot product (after L2 normalization = cosine sim) query_emb = model.encode(["What is Python?"], normalize_embeddings=True) scores = (query_emb @ embeddings.T)[0] print(f"Similarities: {scores}")
Choosing an Embedding Model
| Model | Dim | Use Case | Notes |
|---|---|---|---|
all-MiniLM-L6-v2 | 384 | Fast, general | Good baseline |
BAAI/bge-large-en-v1.5 | 1024 | English retrieval | Strong MTEB score |
text-embedding-3-small | 1536 | OpenAI API | Good quality/cost |
text-embedding-3-large | 3072 | OpenAI API | Best OpenAI embedding |
voyage-large-2 | 1536 | Voyage AI API | Top retrieval benchmarks |
nomic-embed-text-v1.5 | 768 | Open, long context | 8192 token window |
multilingual-e5-large | 1024 | Multilingual | 100+ languages |
MTEB (Massive Text Embedding Benchmark) is the standard leaderboard for embedding models.
Vector Similarity Search
Distance Metrics
| Metric | Formula | Use When |
|---|---|---|
| Cosine similarity | (a·b)/(‖a‖‖b‖) | Normalized embeddings; most common |
| Dot product | a·b | Already L2-normalized (equals cosine) |
| Euclidean (L2) | √Σ(aᵢ−bᵢ)² | Un-normalized embeddings |
| Manhattan (L1) | Σ|aᵢ−bᵢ| | Sparse features |
After L2 normalization, cosine similarity = dot product. Always normalize embeddings before indexing for consistency.
Exact vs. Approximate Nearest Neighbor (ANN)
Exact k-NN: check all n vectors. O(nd) per query — fine for n < 100k.
ANN (Approximate Nearest Neighbor): trade a little accuracy for orders-of-magnitude faster search. Used when n > 100k.
FAISS (Facebook AI Similarity Search)
FAISS is the most popular open-source vector search library. Multiple index types with different speed/accuracy tradeoffs.
import faiss import numpy as np d = 1024 # embedding dimension n = 100000 # number of documents embeddings = np.random.randn(n, d).astype("float32") faiss.normalize_L2(embeddings) # L2 normalize in-place # Flat index (exact, brute force) — best for <100k index = faiss.IndexFlatIP(d) # Inner Product = cosine after normalization index.add(embeddings) # Search query = np.random.randn(1, d).astype("float32") faiss.normalize_L2(query) distances, indices = index.search(query, k=10) # IVF index (approximate, for millions of vectors) quantizer = faiss.IndexFlatIP(d) index_ivf = faiss.IndexIVFFlat(quantizer, d, 1000, faiss.METRIC_INNER_PRODUCT) # nlist=1000 index_ivf.train(embeddings) # cluster centroids index_ivf.add(embeddings) index_ivf.nprobe = 20 # how many clusters to check at query time dist, idx = index_ivf.search(query, k=10) # HNSW (best single-machine ANN, no training needed) index_hnsw = faiss.IndexHNSWFlat(d, 32) # 32 = M parameter index_hnsw.add(embeddings) dist, idx = index_hnsw.search(query, k=10)
FAISS Index Types
| Index | Method | Recall | Speed | Memory |
|---|---|---|---|---|
IndexFlatIP/L2 | Exact | 100% | Slow (brute force) | High (full vectors) |
IndexIVFFlat | IVF clusters | 95–99% | Fast | High |
IndexIVFPQ | IVF + Product Quantization | 90–97% | Very fast | Low |
IndexHNSWFlat | Hierarchical Navigable Small World | 95–99% | Very fast | Medium |
IndexIVFSQ | IVF + Scalar Quantization | 95–99% | Fast | Low |
Product Quantization (PQ): compress vectors by splitting into sub-vectors, quantize each sub-vector separately → 16–64× compression with acceptable recall loss.
Vector Databases
Purpose-built for storing, indexing, and querying embedding vectors at scale.
| Database | Type | Key Features |
|---|---|---|
| Chroma | Open-source, embedded | Zero-setup, great for prototypes |
| FAISS | Library | Best raw performance, no persistence |
| Pinecone | Managed cloud | Serverless, fully managed |
| Weaviate | Open-source + cloud | Hybrid search, multi-modal |
| Qdrant | Open-source + cloud | Rust-based, fast, filtering |
| Milvus | Open-source | Distributed, production-scale |
| pgvector | Postgres extension | Familiar SQL + vector |
| Redis Vector | Redis extension | In-memory, low latency |
# Chroma (minimal setup) import chromadb client = chromadb.PersistentClient(path="./chroma_db") collection = client.get_or_create_collection("documents", metadata={"hnsw:space": "cosine"}) # Add documents collection.add( documents=docs, embeddings=embeddings.tolist(), ids=[f"doc_{i}" for i in range(len(docs))], metadatas=[{"source": "wiki", "year": 2024} for _ in docs] ) # Query results = collection.query( query_embeddings=query_emb.tolist(), n_results=5, where={"year": {"$gte": 2023}}, # metadata filtering include=["documents", "distances", "metadatas"] )
Building a RAG System
1. Indexing Pipeline
from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.document_loaders import PyPDFLoader from sentence_transformers import SentenceTransformer import chromadb # Load documents loader = PyPDFLoader("document.pdf") pages = loader.load() # Chunk text (important: chunk size matches embedding model context) splitter = RecursiveCharacterTextSplitter( chunk_size=512, # chars per chunk chunk_overlap=50, # overlap for continuity separators=["\n\n", "\n", ". ", " ", ""] ) chunks = splitter.split_documents(pages) # Embed and store embed_model = SentenceTransformer("BAAI/bge-large-en-v1.5") texts = [c.page_content for c in chunks] embeddings = embed_model.encode(texts, normalize_embeddings=True) collection = client.get_or_create_collection("docs") collection.add( documents=texts, embeddings=embeddings.tolist(), ids=[f"chunk_{i}" for i in range(len(texts))], metadatas=[{"page": c.metadata.get("page", 0)} for c in chunks] )
2. Retrieval and Generation
from openai import OpenAI openai_client = OpenAI() def rag_query(question: str, n_results: int = 5) -> str: # Embed the question q_emb = embed_model.encode([question], normalize_embeddings=True) # Retrieve relevant chunks results = collection.query(query_embeddings=q_emb.tolist(), n_results=n_results) context = "\n\n---\n\n".join(results["documents"][0]) # Generate answer response = openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Answer the question using only the provided context. " "If the context doesn't contain the answer, say 'I don't know'."}, {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"} ], temperature=0 ) return response.choices[0].message.content answer = rag_query("What is the company's revenue growth target?")
Advanced RAG Techniques
Query Rewriting / HyDE
HyDE (Hypothetical Document Embeddings): generate a hypothetical answer to the query, embed it, search with the embedding of the hypothetical answer (often closer to real documents).
def hyde_retrieval(query: str) -> list: # Generate hypothetical answer hyp_answer = openai_client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": f"Write a short paragraph that would answer: {query}"}] ).choices[0].message.content # Embed the hypothetical answer hyp_emb = embed_model.encode([hyp_answer], normalize_embeddings=True) return collection.query(query_embeddings=hyp_emb.tolist(), n_results=5)
Hybrid Search
Combine dense (vector) and sparse (BM25 keyword) search via reciprocal rank fusion or a learned re-ranker:
from rank_bm25 import BM25Okapi # Sparse BM25 index tokenized = [doc.split() for doc in texts] bm25 = BM25Okapi(tokenized) def hybrid_search(query, alpha=0.5, k=10): # Dense retrieval q_emb = embed_model.encode([query], normalize_embeddings=True) dense_res = collection.query(query_embeddings=q_emb.tolist(), n_results=k) # Sparse BM25 retrieval sparse_scores = bm25.get_scores(query.split()) sparse_top = np.argsort(sparse_scores)[::-1][:k] # Reciprocal Rank Fusion scores = {} for rank, idx in enumerate(dense_res["ids"][0]): scores[idx] = scores.get(idx, 0) + alpha / (rank + 60) for rank, idx in enumerate(sparse_top): doc_id = f"chunk_{idx}" scores[doc_id] = scores.get(doc_id, 0) + (1-alpha) / (rank + 60) return sorted(scores, key=scores.get, reverse=True)[:k]
Re-ranking
After initial retrieval, re-rank top-k results with a more expensive cross-encoder model:
from sentence_transformers import CrossEncoder reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") query = "What is photosynthesis?" candidates = retrieved_docs[:20] # initial retrieval pairs = [[query, doc] for doc in candidates] scores = reranker.predict(pairs) ranked = sorted(zip(scores, candidates), reverse=True) top5 = [doc for _, doc in ranked[:5]]
Chunking Strategies
| Strategy | Description | When to Use |
|---|---|---|
| Fixed-size | Split every N characters | Simple baseline |
| Recursive text splitter | Split on paragraphs → sentences → words | General text |
| Sentence splitter | Split on sentence boundaries | Coherent context |
| Semantic chunking | Split when embedding similarity drops | Best coherence |
| Document-specific | Tables, code blocks, sections | Structured docs |
| Small-to-big | Store small chunks, retrieve parent | Precision + context |
Contextual Compression
Extract only the relevant parts of retrieved chunks for the final prompt, reducing noise and token cost.
RAG Evaluation
| Metric | Measures | Tool |
|---|---|---|
| Context Recall | Are relevant docs retrieved? | RAGAS |
| Context Precision | Are retrieved docs relevant? | RAGAS |
| Answer Faithfulness | Is answer grounded in context? | RAGAS |
| Answer Relevancy | Does answer address the question? | RAGAS |
| MRR / nDCG@k | Ranking quality of retrieval | Manual |
from ragas import evaluate from ragas.metrics import (faithfulness, answer_relevancy, context_recall, context_precision) from datasets import Dataset # Prepare evaluation dataset data = Dataset.from_dict({ "question": questions, "answer": generated_answers, "contexts": retrieved_contexts, "ground_truth": reference_answers }) result = evaluate(data, metrics=[faithfulness, answer_relevancy, context_recall, context_precision]) print(result)
Agentic RAG
Move beyond single-retrieval to agents that can decide when and what to retrieve:
- Iterative RAG: retrieve → answer → decide if more retrieval needed → retrieve again
- Self-RAG: model generates special tokens to trigger retrieval, critique retrieved docs
- FLARE: generate tentative next sentence; if uncertain, retrieve to improve it
- Tool-use RAG: LLM decides to call search, calculator, code interpreter as needed
# Simple iterative RAG with OpenAI tools tools = [{ "type": "function", "function": { "name": "search_documents", "description": "Search the knowledge base for relevant information", "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"] } } }] response = openai_client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, tool_choice="auto" )