GenAI Course · Module 9 · Session 34

Embeddings & Vector Databases

⏱ 14 min read🎯 Intermediate🖼 2 diagrams🧪 1 live demo🐍 Python code

Embeddings turn text into numbers so that similar meanings land close together. A vector database stores millions of them and finds the nearest ones in milliseconds. In this session, let us learn how to choose both, and see a real speed test of three index types.

In this session

  • How to choose an embedding model: dimension, domain, languages, MTEB
  • Cosine similarity in one picture
  • ChromaDB, Pinecone, FAISS and pgvector compared
  • Flat vs IVF vs HNSW indexes, with a benchmark we actually ran

01Choosing an embedding model

An embedding model reads a chunk and outputs a vector, for example 1,024 numbers. Questions to ask:

ModelDimensionsNotes
OpenAI text-embedding-3-small1,536cheap, good general quality, API
OpenAI text-embedding-3-large3,072higher quality; dimensions can be shortened
BGE-M3 (BAAI)1,024open, multilingual (100+ languages), long inputs
multilingual-e5-large1,024open, strong multilingual retrieval
all-MiniLM-L6-v2384tiny and fast; fine for prototypes
Golden rule: the same embedding model must be used for indexing and for queries. Changing models means re-embedding everything.

02What an embedding space looks like

dim 1dim 2dim 3…dim 1024FD withdrawal0.42-0.100.33…0.07FD interest0.39-0.050.21…0.11Savings rules-0.120.300.02…-0.20Card fees-0.310.44-0.18…0.25Question0.40-0.080.30…0.09
Each chunk becomes a row of numbers. The question’s row (amber) is closest to the FD rows (purple), so those chunks are retrieved. Illustrative values.

03Cosine similarity

To compare two vectors, RAG systems usually use cosine similarity: 1 means same direction (same meaning), 0 means unrelated. The retriever returns the chunks with the highest similarity to the question.

Demo · Similarity to the question “How do I break my FD early?”

04Vector databases compared

OptionTypeBest forWatch out
ChromaDBopen source, runs locally or as a serverlearning, prototypes, small appsnot built for very large production loads
Pineconefully managed cloud (serverless)production without running infrastructurepaid; data leaves your network
FAISSlibrary from Meta (in-memory)fast research and custom systemsno built-in server, filtering or persistence
pgvectorPostgreSQL extensionteams already on Postgres; SQL filters + vectors togethertune indexes as data grows
OthersQdrant, Weaviate, Milvus, OpenSearchlarge scale, hybrid search featuresmore moving parts
pgvector_example.sql
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE chunks (id bigserial PRIMARY KEY, text text, department text, embedding vector(1024));
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);

-- top 5 chunks for retail banking, nearest first (<=> is cosine distance)
SELECT text FROM chunks
WHERE department = 'retail_banking'
ORDER BY embedding <=> '[0.012, -0.031, ...]'::vector
LIMIT 5;

05Index types: Flat, IVF and HNSW

Comparing the query with every vector (Flat) is exact but slow at scale. Approximate indexes trade a tiny bit of accuracy for big speed gains.

IVF (clusters)• groups vectors into clusters• searches only nearest clusters• nprobe = clusters to check• less memory, needs trainingHNSW (graph)• links each vector to neighbours• hops through the graph• efSearch = search width• very fast, more memory
Two popular approximate indexes.
📮

IVF is like PIN codes. To find a house, first go to the right PIN code area, then search only there. HNSW is like asking neighbours for directions: each hop takes you closer to the house.

We ran this benchmark on 100,000 clustered 384-dimensional vectors (CPU):

index_benchmark.py
import time, numpy as np, faiss

d, n = 384, 100_000                                  # 384-d vectors, like all-MiniLM-L6-v2
rng = np.random.default_rng(0)
centers = rng.standard_normal((500, d))              # real embeddings form clusters of topics
xb = (centers[rng.integers(0, 500, n)] + 0.3 * rng.standard_normal((n, d))).astype("float32")
xq = (centers[rng.integers(0, 500, 200)] + 0.3 * rng.standard_normal((200, d))).astype("float32")
faiss.normalize_L2(xb); faiss.normalize_L2(xq)       # cosine similarity = inner product

flat = faiss.IndexFlatIP(d); flat.add(xb)

hnsw = faiss.IndexHNSWFlat(d, 32, faiss.METRIC_INNER_PRODUCT)
hnsw.hnsw.efSearch = 64; hnsw.add(xb)

quant = faiss.IndexFlatIP(d)
ivf = faiss.IndexIVFFlat(quant, d, 256, faiss.METRIC_INNER_PRODUCT)
ivf.train(xb); ivf.add(xb); ivf.nprobe = 8

_, truth = flat.search(xq, 10)
for name, idx in [("Flat (exact)", flat), ("IVF", ivf), ("HNSW", hnsw)]:
    t = time.perf_counter(); _, I = idx.search(xq, 10); ms = (time.perf_counter() - t) * 1000 / len(xq)
    recall = np.mean([len(set(a) & set(b)) / 10 for a, b in zip(I, truth)])
    print(f"{name:13s} {ms:5.2f} ms/query   recall@10 = {recall:.2f}")
Flat (exact) 3.90 ms/query recall@10 = 1.00 IVF 0.21 ms/query recall@10 = 1.00 HNSW 0.06 ms/query recall@10 = 0.97

HNSW was about 60× faster than exact search while still finding 97% of the true top-10. Timings depend on your machine; the pattern holds.

IndexSpeedAccuracyMemoryUse when
Flatslow at scaleexactlowunder ~100K vectors, or as ground truth
IVFfasthigh (tune nprobe)lowlarge data, memory limited
HNSWfastesthigh (tune efSearch)higherlow-latency production search

06Summary

07Quick quiz

1. What must stay the same between indexing and querying?

2. MTEB is…

3. In our benchmark, HNSW vs Flat was…

4. You already run PostgreSQL and want SQL filters with vectors. Pick…

5. IVF searches…

08References

Created by Ajeetkumar