TAI BUI
Back to curriculum
BuildPython

Embeddings & Vector Representations

Embeddings turn text into vectors so that meaning, not spelling, decides what counts as similar. Search, retrieval, and reranking all sit on that one idea, and this lesson builds the whole pipeline in plain numpy before pointing it at a real model.

TIME: ~75 minutesPREREQUISITES: Phase 11, Lesson 01 (Prompt Engineering)

Learning Objectives

  • Generate text embeddings using API providers and open-source models, and compute cosine similarity between them
  • Explain why embeddings solve the vocabulary mismatch problem that keyword search cannot handle
  • Build a semantic search index that retrieves documents by meaning rather than exact keyword match
  • Evaluate embedding quality using retrieval benchmarks (precision@k, recall) and choose the right embedding model for your task

The Problem

You have 10,000 support tickets. A customer writes "my payment didn't go through." You need to find similar past tickets. Keyword search finds tickets containing "payment" and "didn't go through." It misses "transaction failed," "charge was declined," and "billing error." These tickets describe the exact same problem in different words.

This is the vocabulary mismatch problem. Human language has dozens of ways to say the same thing. Keyword search treats each word as an independent symbol with no meaning. It cannot know that "declined" and "didn't go through" refer to the same concept.

You need a representation of text where meaning, not spelling, determines similarity. You need a way to place "my payment didn't go through" and "transaction was declined" close together in some mathematical space, while pushing "my payment arrived on time" far away despite sharing the word "payment."

That representation is an embedding.

Warm-up0/3
1. Which prompting technique places worked input-output examples directly in the prompt so the model imitates the pattern?Warm-up
A three-line prompt: two review lines each followed by an arrow and a label, and a third review line followed by an arrow and a question mark.
Typed example prompt, not captured from a model.
2. An LLM prompt plus its answer must fit inside the context window. In what unit is that window measured?Warm-up
A horizontal bar split into your prompt, the answer and room left, ending at a fixed limit line. A question mark asks what unit the limit is measured in.
Schematic with no real sizes.
3. How much memory does a single 32-bit floating-point number occupy?Warm-up
The number 0.023 with an arrow to a row of 32 small cells labelled 32 bits, and a question mark asking how many bytes it occupies.
Schematic: 32 cells for the 32 bits of one float32.
Check: The Problem0/1
1. What problem do embeddings solve that keyword search cannot?Explain
A new ticket, my payment didn't go through, with dashed lines to four past tickets, each marked with a question mark.
Ticket texts from the lesson.

The Concept

What Is an Embedding?

An embedding is a dense vector of floating-point numbers that represents the meaning of text. The word "dense" matters: every dimension carries information, unlike sparse representations (bag-of-words, TF-IDF) where most dimensions are zero.

"The cat sat on the mat" becomes something like [0.023, -0.041, 0.087, ..., 0.012], a list of 768 to 3072 numbers depending on the model. These numbers encode meaning. You never inspect them directly. You compare them.

The Word2Vec Breakthrough

In 2013, Tomas Mikolov and colleagues at Google published Word2Vec. The core insight: train a neural network to predict a word from its neighbors (or neighbors from a word), and the hidden layer weights become meaningful vector representations.

The famous result:

king - man + woman = queen

Vector arithmetic on word embeddings captures semantic relationships. The direction from "man" to "woman" is roughly the same as the direction from "king" to "queen." This was the moment the field realized that geometry could encode meaning.

Word2Vec produced 300-dimensional vectors. Each word got one vector regardless of context. "Bank" in "river bank" and "bank account" had the same embedding. This limitation drove the next decade of research.

From Words to Sentences

Word embeddings represent single tokens. Production systems need to embed entire sentences, paragraphs, or documents. Four approaches emerged:

Averaging: take the mean of all word vectors in the sentence. Cheap, lossy, and decent for short text. Word order is lost, so "dog bites man" and "man bites dog" get identical embeddings.

CLS token: transformer models (BERT, 2018) output a special [CLS] token embedding that represents the entire input. Better than averaging but the [CLS] token was trained for next-sentence prediction, not similarity.

Contrastive learning: train the model explicitly to push similar pairs together and dissimilar pairs apart. Sentence-BERT (Reimers & Gurevych, 2019) used this approach and became the foundation for modern embedding models. Given "How do I reset my password?" and "I need to change my password," the model learns these should have nearly identical vectors.

Instruction-tuned embeddings: the latest approach. Models like E5 and GTE accept a task prefix ("search_query:", "search_document:") that tells the model what kind of embedding to produce. This lets one model serve multiple tasks.

Mermaid diagram

Modern Embedding Models

The market has settled into a handful of production-grade options (MTEB scores as of early 2026, MTEB v2):

ModelProviderDimensionsMTEBContextCost / 1M tokens
Gemini Embedding 2Google3072 (Matryoshka)67.7 (retrieval)8192$0.15
embed-v4Cohere1024 (Matryoshka)65.2128K$0.12
voyage-4Voyage AI1024/2048 (Matryoshka)66.832K$0.12
text-embedding-3-largeOpenAI3072 (Matryoshka)64.68192$0.13
text-embedding-3-smallOpenAI1536 (Matryoshka)62.38192$0.02
BGE-M3BAAI1024 (dense+sparse+ColBERT)63.0 multilingual8192Open-weight
Qwen3-EmbeddingAlibaba4096 (Matryoshka)66.932KOpen-weight
Nomic-embed-v2Nomic768 (Matryoshka)63.18192Open-weight

MTEB (Massive Text Embedding Benchmark) v2 covers 100+ tasks across retrieval, classification, clustering, reranking, and summarization. Higher is better. By 2026, open-weight models (Qwen3-Embedding, BGE-M3) match or beat closed hosted models on most axes. Gemini Embedding 2 leads pure retrieval; Voyage/Cohere lead specific domains (finance, law, code). Benchmark on your own queries before committing.

Similarity Metrics

Given two embedding vectors, three ways to measure how similar they are:

Cosine similarity: the cosine of the angle between two vectors. Ranges from -1 (opposite) to 1 (identical direction). Ignores magnitude, so a 10-word sentence and a 500-word document can score 1.0 if they point the same direction. This is the default for 90% of use cases.

cosine_sim(a, b) = dot(a, b) / (||a|| * ||b||)

Dot product: the raw inner product of two vectors. Identical to cosine similarity when vectors are normalized (unit length). Faster to compute. OpenAI's embeddings are normalized, so dot product and cosine give the same ranking.

dot(a, b) = sum(a_i * b_i)

Euclidean (L2) distance: straight-line distance in the vector space. Smaller means more similar. Sensitive to magnitude differences. Use when the absolute position in space matters, not just the direction.

L2(a, b) = sqrt(sum((a_i - b_i)^2))

When to use which:

MetricUse whenAvoid when
Cosine similarityComparing texts of different lengths; most retrieval tasksMagnitude carries information
Dot productEmbeddings are already normalized; maximum speedVectors have varying magnitudes
Euclidean distanceClustering; spatial nearest-neighbor problemsComparing documents of wildly different lengths

Vector Databases and HNSW

A brute-force similarity search compares the query against every stored vector. At 1 million vectors with 1536 dimensions, that is 1.5 billion multiply-add operations per query. Too slow.

Vector databases solve this with Approximate Nearest Neighbor (ANN) algorithms. The dominant algorithm is HNSW (Hierarchical Navigable Small World):

  1. Build a multi-layer graph of vectors
  2. Top layers are sparse, with long-range connections between distant clusters
  3. Bottom layers are dense, with fine-grained connections between nearby vectors
  4. Search starts at the top layer, greedily descending to refine
  5. Returns approximate top-k results in O(log n) time instead of O(n)

HNSW trades a small accuracy loss (typically 95-99% recall) for massive speed gains. At 10 million vectors, brute force takes seconds. HNSW takes milliseconds.

Mermaid diagram

Production options:

DatabaseTypeBest forMax scale
PineconeManaged SaaSZero-ops productionBillions
WeaviateOpen sourceSelf-hosted, hybrid search100M+
QdrantOpen sourceHigh performance, filtering100M+
ChromaDBEmbeddedPrototyping, local dev1M
pgvectorPostgres extensionAlready using Postgres10M
FAISSLibraryIn-process, research1B+

Chunking Strategies

Documents are too long to embed as single vectors. A 50-page PDF covers dozens of topics, so its embedding becomes an average of everything, similar to nothing specific. You split documents into chunks and embed each one.

Fixed-size chunking: split every N tokens with M-token overlap. Simple and predictable. Works well when documents have no clear structure. A 512-token chunk with 50-token overlap: chunk 1 is tokens 0-511, chunk 2 is tokens 462-973.

Sentence-based chunking: split at sentence boundaries, grouping sentences until reaching the token limit. Each chunk is at least one complete sentence. Better than fixed-size because you never cut a thought in half.

Recursive chunking: try splitting at the largest boundary first (section headers). If still too large, try paragraph boundaries. Then sentence boundaries. Then character limits. This is LangChain's RecursiveCharacterTextSplitter and it works well for mixed-format corpora.

Semantic chunking: embed each sentence, then group consecutive sentences whose embeddings are similar. When the embedding similarity drops below a threshold, start a new chunk. Expensive (requires embedding every sentence individually) but produces the most coherent chunks.

StrategyComplexityQualityBest for
Fixed-sizeLowDecentUnstructured text, logs
Sentence-basedLowGoodArticles, emails
RecursiveMediumGoodMarkdown, HTML, mixed docs
SemanticHighBestCritical retrieval quality

The sweet spot for most systems: 256-512 token chunks with 50-token overlap.

Bi-Encoders vs Cross-Encoders

A bi-encoder embeds the query and documents independently, then compares vectors. Fast: you embed the query once and compare against pre-computed document embeddings. This is what you use for retrieval.

A cross-encoder takes the query and a document as a single input and outputs a relevance score. Slow, because it processes each query-document pair through the full model. But far more accurate because it can attend across query and document tokens simultaneously.

The production pattern: bi-encoder retrieves top-100 candidates, cross-encoder reranks them to top-10. This is the retrieve-then-rerank pipeline.

Mermaid diagram

Reranking models: Cohere Rerank 3.5 ($2 per 1000 queries), BGE-reranker-v2 (free, open source), Jina Reranker v2 (free, open source).

Matryoshka Embeddings

Traditional embeddings are all-or-nothing. A 1536-dimensional vector uses 1536 floats. You cannot truncate to 256 dimensions without retraining.

Matryoshka Representation Learning (Kusupati et al., 2022) fixes this. The model is trained so that the first N dimensions capture the most important information, like a Russian nesting doll. Truncating a 1536-d Matryoshka embedding to 256 dimensions loses some accuracy but remains functional.

OpenAI's text-embedding-3-small and text-embedding-3-large support Matryoshka truncation via the dimensions parameter. Requesting 256 dimensions instead of 1536 cuts storage by 6x with roughly 3-5% accuracy loss on MTEB benchmarks.

Binary Quantization

A 1536-dimensional embedding stored as float32 uses 6,144 bytes. Multiply by 10 million documents: 61 GB just for vectors.

Binary quantization converts each float to a single bit: positive values become 1, negative values become 0. Storage drops from 6,144 bytes to 192 bytes, a 32x reduction. Similarity is computed using Hamming distance (count differing bits), which CPUs can do in a single instruction.

The accuracy hit is around 5-10% on retrieval recall. The common pattern: binary quantization for the first-pass search over millions of vectors, then rescore the top-1000 with full-precision vectors. This gets you 95%+ of full-precision accuracy at 32x less memory.

Check: The Concept0/31
1. What did Word2Vec (2013) show about word vectors?Define
A two-dimensional plot with six labelled word points: man, woman, king, queen, car and apple.
Illustrative 2-D positions, typed. Real Word2Vec vectors have 300 dimensions.
2. What is an embedding?Define
A sentence, The cat sat on the mat, goes into a box labelled embedding model, and a short list of decimal numbers comes out.
Numbers copied from the lesson's example, abbreviated.
3. What does cosine similarity measure between two embedding vectors?Define
Two arrows from the origin in a 2-D plane, a pointing to (3, 1) and b pointing to (1, 2), with the label cosine_sim(a, b) equals a question mark.
Toy vectors, typed. Real embeddings have hundreds of dimensions.
4. How does HNSW make vector search fast at scale?Define
A scatter of about ninety grey points, one orange triangle marked query, and three green points nearest to it.
Schematic with random 2-D points; a real index holds millions of high-dimensional vectors.
5. What is a bi-encoder?Define
Two parallel paths: the query reset my password and a document each go through their own encoder box and produce a query vector and a document vector, which are then compared.
Typed diagram of one model type, with no measured values.
6. What is a cross-encoder?Define
The query reset my password and a document both feed into a single model box, which outputs one score.
Typed diagram of one model type, with no measured values.
7. What makes Matryoshka embeddings different from traditional embeddings?Define
A long horizontal bar for 1536 dimensions, labelled dim 0 at the left end and dim 1535 at the right end.
Schematic of one vector, no real values.
8. How does binary quantization shrink a vector's storage?Define
A row of four decimal numbers and an ellipsis labelled 1536 numbers, an arrow labelled binary quantization, and a question mark for what is stored.
Values typed for illustration.
9. What is chunking?Define
A bar for token positions 0 to 973 with two spans below it, tokens 0 to 511 and tokens 462 to 973, each pointing to its own embedding vector.
Spans as given in the lesson; the vectors are placeholders.
10. What is a vector database?Define
A query, my payment didn't go through, goes into a box named Store Z that lists four ticket texts, and a question mark marks what comes back.
Ticket texts from the lesson; the layout is typed.
11. What is contrastive learning?Define
A plane with three text points, two password questions far apart and a payment text. A dashed green line joins the password questions and a dashed red line joins the first password question and the payment text.
Illustrative positions, typed. Pair labels come from the lesson's examples.
12. What is MTEB?Define
Horizontal bars for five embedding models with scores from 62.3 to 66.9 in a column headed MTEB (v2, early 2026).
Scores copied from the lesson's model table. Bars start at 55, not zero.
13. Which sentence-embedding approach uses a special transformer output token that was trained for next-sentence prediction, not similarity?Define
Five input tokens, a special first token, sentence A, a separator, sentence B and a separator, feed a transformer, and the special first position's output feeds a box asking whether B is the next sentence.
Typed diagram of the pretraining task named in the stem.
14. Which chunking strategy tries section headers first, then paragraphs, then sentences, then character limits?Define
A staircase of four boxes labelled first section headers, second paragraphs, third sentences and fourth character limits, each indented further than the last.
Order taken from the question stem.
15. What is the typical dimensionality of modern text embedding models?Define
A sentence goes into an embedding model, which outputs a list of decimal numbers that trails off with dots; a bracket asks how many numbers are in the list.
Typed schematic; values abbreviated.
16. Which tool in the vector database table is a Postgres extension for teams already running Postgres?Define
A web app connected to an existing Postgres database, and a dashed box below stating a new need to store embeddings and find nearest neighbors.
Typed schematic.
17. Why did Word2Vec give 'bank' in 'river bank' and 'bank account' the same vector?Explain
Two phrases, river bank and bank account, each go through a Word2Vec lookup box and produce a 300-number vector for bank, with an equals sign between the two vectors.
Typed diagram; the 300 dimensions come from the lesson.
18. Why do 'dog bites man' and 'man bites dog' get identical embeddings under the averaging approach?Explain
Two rows of three words, dog bites man and man bites dog, each passing through a box that averages word vectors and ending in a question mark.
Typed diagram.
19. When does dot product give the same ranking as cosine similarity?Explain
A table scoring three toy document vectors against the query (1, 0), with a dot product column and a cosine column.
Toy 2-D vectors, scores computed with numpy.
20. Why is a 50-page PDF embedded as one vector a poor retrieval target?Explain
A grid of 50 page outlines with an arrow to a single vector, and a separate query vector below it that is compared with that vector.
Schematic, typed.
21. How do production systems usually combine bi-encoders and cross-encoders?Explain
Four boxes: all documents on the left, a bi-encoder and a cross-encoder in the middle with no arrows between them, and a final top 10 on the right.
Typed schematic; the top 10 is from the lesson's example.
22. Why is a cross-encoder more accurate than a bi-encoder but too slow to scan a whole corpus?Explain
The query reset my password and document 1 feed one cross-encoder box that outputs a score, with a dashed box below describing a corpus of N documents.
Typed diagram.
23. Which claim about open-weight embedding models such as Qwen3-Embedding and BGE-M3 does the lesson support?Explain
Two boxes: five hosted embedding models on the left and three open-weight models on the right, listed by name only.
Names and grouping from the lesson's model table.
24. Why pair binary quantization with rescoring the top 1000 using full-precision vectors?Explain
A left-to-right pipeline: millions of 1-bit vectors, pass 1 using Hamming distance, the top 1000, pass 2 at full precision, then results.
Typed pipeline from the question stem.
25. You store 10 million 1536-d embeddings as binary vectors. About how much memory do the vectors need?Apply
Rows of small squares standing for stored vectors, with the line 10,000,000 vectors by 1536 dimensions by 1 bit each, and a question mark for the memory needed.
Schematic; counts from the stem.
26. A brute-force index holds 2 million 1536-d vectors. Roughly how many multiply-add operations does one query need?Apply
A query vector of 1536 numbers pointing at a block of squares standing for 2,000,000 stored vectors, and a question mark for the number of multiply-add operations per query.
Schematic; counts from the stem.
27. A team slices the first 256 entries off embeddings from an ordinary model, and recall collapses. What was the wrong assumption?Analyze
An ordinary embedding model produces 1536 numbers, the code vec[:256] keeps the first 256, and these go into an index; below it says recall collapses.
Typed schematic; the slice length is from the stem.
28. Search quality is fine on 10,000 vectors, but at 10 million each query takes seconds. Which component is the cause?Analyze
A five-step pipeline from documents to top-k results, and two rows: 10,000 vectors with quality fine, and 10,000,000 vectors where each query takes seconds.
Typed schematic; sizes and symptoms are from the stem.
29. A pipeline runs a cross-encoder over all documents for every query. Results are accurate but far too slow. Which change fixes it?Analyze
A query and a cross-encoder box that runs on every document in the collection, with the note accuracy good, speed far too slow.
Typed schematic.
30. You must index 20 million 1536-d float32 vectors (about 123 GB) in 8 GB of RAM and keep recall high. What fits?Design
Two horizontal bars: the float32 index of 20 million 1536-dimensional vectors at about 123 GB, and the RAM available at 8 GB, which is far shorter.
Bars to scale; 20,000,000 x 6,144 bytes = 122.88 GB.
31. A leaderboard ranks model A above model B on MTEB. You are choosing a model for your own support tickets. What do you do?Design
A leaderboard with Model A ranked first and Model B second on MTEB, next to a box for the reader's own support-ticket search where the better model is a question mark.
Typed schematic; no scores are shown.

Build It

We build a semantic search engine from scratch. No vector database. No external embedding API. Pure Python with numpy for the math.

Step 1: Text Chunking

python
def chunk_text(text, chunk_size=200, overlap=50):
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunk = " ".join(words[start:end])
        chunks.append(chunk)
        start += chunk_size - overlap
    return chunks
 
 
def chunk_by_sentences(text, max_chunk_tokens=200):
    sentences = text.replace("\n", " ").split(".")
    sentences = [s.strip() + "." for s in sentences if s.strip()]
    chunks = []
    current_chunk = []
    current_length = 0
    for sentence in sentences:
        sentence_length = len(sentence.split())
        if current_length + sentence_length > max_chunk_tokens and current_chunk:
            chunks.append(" ".join(current_chunk))
            current_chunk = []
            current_length = 0
        current_chunk.append(sentence)
        current_length += sentence_length
    if current_chunk:
        chunks.append(" ".join(current_chunk))
    return chunks

Step 2: Building Embeddings from Scratch

We implement a simple dense embedding using TF-IDF with L2 normalization. This is not a neural embedding, but it follows the same contract: text in, fixed-size vector out, similar texts produce similar vectors.

python
import math
import numpy as np
from collections import Counter
 
class SimpleEmbedder:
    def __init__(self):
        self.vocab = []
        self.idf = []
        self.word_to_idx = {}
 
    def fit(self, documents):
        vocab_set = set()
        for doc in documents:
            vocab_set.update(doc.lower().split())
        self.vocab = sorted(vocab_set)
        self.word_to_idx = {w: i for i, w in enumerate(self.vocab)}
        n = len(documents)
        self.idf = np.zeros(len(self.vocab))
        for i, word in enumerate(self.vocab):
            doc_count = sum(1 for doc in documents if word in doc.lower().split())
            self.idf[i] = math.log((n + 1) / (doc_count + 1)) + 1
 
    def embed(self, text):
        words = text.lower().split()
        count = Counter(words)
        total = len(words) if words else 1
        vec = np.zeros(len(self.vocab))
        for word, freq in count.items():
            if word in self.word_to_idx:
                tf = freq / total
                vec[self.word_to_idx[word]] = tf * self.idf[self.word_to_idx[word]]
        norm = np.linalg.norm(vec)
        if norm > 0:
            vec = vec / norm
        return vec

Step 3: Similarity Functions

python
def cosine_similarity(a, b):
    dot = np.dot(a, b)
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return float(dot / (norm_a * norm_b))
 
 
def dot_product(a, b):
    return float(np.dot(a, b))
 
 
def euclidean_distance(a, b):
    return float(np.linalg.norm(a - b))

Step 4: Vector Index with Brute-Force Search

python
class VectorIndex:
    def __init__(self):
        self.vectors = []
        self.texts = []
        self.metadata = []
 
    def add(self, vector, text, meta=None):
        self.vectors.append(vector)
        self.texts.append(text)
        self.metadata.append(meta or {})
 
    def search(self, query_vector, top_k=5, metric="cosine"):
        scores = []
        for i, vec in enumerate(self.vectors):
            if metric == "cosine":
                score = cosine_similarity(query_vector, vec)
            elif metric == "dot":
                score = dot_product(query_vector, vec)
            elif metric == "euclidean":
                score = -euclidean_distance(query_vector, vec)
            else:
                raise ValueError(f"Unknown metric: {metric}")
            scores.append((i, score))
        scores.sort(key=lambda x: x[1], reverse=True)
        results = []
        for idx, score in scores[:top_k]:
            results.append({
                "text": self.texts[idx],
                "score": score,
                "metadata": self.metadata[idx],
                "index": idx
            })
        return results
 
    def size(self):
        return len(self.vectors)

Step 5: The Semantic Search Engine

python
class SemanticSearchEngine:
    def __init__(self, chunk_size=200, overlap=50):
        self.embedder = SimpleEmbedder()
        self.index = VectorIndex()
        self.chunk_size = chunk_size
        self.overlap = overlap
 
    def index_documents(self, documents, source_names=None):
        all_chunks = []
        all_sources = []
        for i, doc in enumerate(documents):
            chunks = chunk_text(doc, self.chunk_size, self.overlap)
            all_chunks.extend(chunks)
            name = source_names[i] if source_names else f"doc_{i}"
            all_sources.extend([name] * len(chunks))
        self.embedder.fit(all_chunks)
        for chunk, source in zip(all_chunks, all_sources):
            vec = self.embedder.embed(chunk)
            self.index.add(vec, chunk, {"source": source})
        return len(all_chunks)
 
    def search(self, query, top_k=5, metric="cosine"):
        query_vec = self.embedder.embed(query)
        return self.index.search(query_vec, top_k, metric)
 
    def search_with_scores(self, query, top_k=5):
        results = self.search(query, top_k)
        return [
            {
                "text": r["text"][:200],
                "source": r["metadata"].get("source", "unknown"),
                "score": round(r["score"], 4)
            }
            for r in results
        ]

Step 6: Comparing Similarity Metrics

python
def compare_metrics(engine, query, top_k=3):
    results = {}
    for metric in ["cosine", "dot", "euclidean"]:
        hits = engine.search(query, top_k=top_k, metric=metric)
        results[metric] = [
            {"score": round(h["score"], 4), "preview": h["text"][:80]}
            for h in hits
        ]
    return results
Check: Build It0/5
1. Why does SemanticSearchEngine call embedder.fit on all chunks before adding any vector to the index?Explain
Three numbered boxes: chunk every document into all_chunks, call embedder.fit on all_chunks, then embed and add each chunk to the index.
Order taken from SemanticSearchEngine.index_documents in the lesson.
2. chunk_text runs on a 500-word text with chunk_size=200 and overlap=50. How many chunks come back?Apply
A bar for a 500-word text, the call chunk_text(text, chunk_size=200, overlap=50), and a question mark for how many chunks come back.
Typed schematic.
3. chunk_by_sentences runs with max_chunk_tokens=200 on three sentences of 90 words each. What does it return?Apply
Three equal boxes labelled sentence 1, sentence 2 and sentence 3, each 90 words, and a question mark for the chunks returned.
Typed schematic.
4. A query contains only words that were never in the fitted corpus. What does cosine_similarity return between embed(query) and a chunk vector?Apply
A box of fourteen vocabulary words from three fitted chunks, a query made of two words, and the expression cosine_similarity of the query vector and a chunk vector equals a question mark.
Vocabulary captured from the lesson's SimpleEmbedder fitted on three typed chunks.
5. VectorIndex.search runs with metric='euclidean' and scores sorted high to low. Which stored vector comes first?Apply
A 2-D plot with the query at (1, 1) and three stored vectors v0 at (5, 4), v1 at (2, 1.5) and v2 at (8, 1), plus the call to VectorIndex.search with the euclidean metric and a question mark for the first result.
Toy vectors; the order added is shown as added #1 to #3.

Use It

With a production embedding API, the architecture stays identical. Only the embedder changes:

python
from openai import OpenAI
 
client = OpenAI()
 
def openai_embed(texts, model="text-embedding-3-small", dimensions=None):
    kwargs = {"model": model, "input": texts}
    if dimensions:
        kwargs["dimensions"] = dimensions
    response = client.embeddings.create(**kwargs)
    return [item.embedding for item in response.data]

Matryoshka truncation with OpenAI (same model, fewer dimensions, lower storage):

python
full = openai_embed(["semantic search query"], dimensions=1536)
compact = openai_embed(["semantic search query"], dimensions=256)

The 256-d vector uses 6x less storage. For 10 million documents, that is 10 GB vs 61 GB. The accuracy loss is roughly 3-5% on standard benchmarks.

For reranking with Cohere:

python
import cohere
 
co = cohere.ClientV2()
 
results = co.rerank(
    model="rerank-v3.5",
    query="What is the refund policy?",
    documents=["Full refund within 30 days...", "No refunds after 90 days..."],
    top_n=3
)

For local embeddings with no API dependency:

python
from sentence_transformers import SentenceTransformer
 
model = SentenceTransformer("BAAI/bge-small-en-v1.5")
embeddings = model.encode(["semantic search query", "another document"])

The VectorIndex class from our build works with any of these. Swap the embedding function, keep the search logic.

Check: Use It0/3
1. You request dimensions=256 from openai_embed with text-embedding-3-small, then call it again without dimensions. What do the two calls return?Apply
The signature of openai_embed with model text-embedding-3-small and dimensions defaulting to None, then two calls, one with dimensions=256 and one without, and question marks for the vector lengths.
Code copied from the lesson.
2. Your support search built on TF-IDF scores 0.0000 between 'refund' and 'money back' tickets. What is the right fix?Analyze
A table of three tickets scored against the query refund: 0.4107 for the ticket containing refund, and 0.0000 for the money-back ticket and for a password ticket.
Captured by running the lesson's SimpleEmbedder and cosine_similarity on three typed tickets.
3. You search private contracts in mixed Markdown and HTML that cannot leave your servers. Which design fits?Design
A dashed box labelled your servers holding Markdown and HTML contract snippets, separated by a blocked line from an area labelled outside.
Contract snippets typed for illustration.

Ship It

This lesson produces:

  • outputs/prompt-embedding-advisor.md: a prompt for choosing embedding models and strategies for specific use cases
  • outputs/skill-embedding-patterns.md: a skill that teaches agents how to use embeddings effectively in production
Author's Note

I'd run embeddings.py and read the similarity comparison (STEP 3 in its printed output) before anything else. SimpleEmbedder is TF-IDF over a vocabulary built from the corpus, so it only sees spelling. "Payment transaction was declined by the bank" and "My credit card charge did not go through" share no words, and the script prints a cosine of 0.0000 for the pair. That number is the vocabulary mismatch problem from The Problem, measured. It is also the reason a neural embedding exists.

The two production tricks in the file are simulations, and they behave like it. truncate_embedding slices the first N entries of a vector whose dimensions are the alphabetically sorted vocabulary, so cutting it drops words, not the least important directions. Matryoshka training is what makes real truncation work, and nothing here does that. In the same way, binarize is vec > 0 on non-negative TF-IDF weights, so it keeps only which words appear. On the sample corpus the "API rate limits" query shares 1 of 5 top results between cosine and Hamming search. I wouldn't read that as what binary quantization does to a real embedding.

The part that carries over is the shape. VectorIndex scans every stored vector on every query, which is the cost HNSW exists to avoid, and it takes any vector you hand it. Swap SimpleEmbedder for one of the models in Use It and the search logic stays as written. That swap is where I'd start the exercises.

Exercises

  1. Metric comparison: run the same 5 queries against the sample documents using cosine similarity, dot product, and euclidean distance. Record the top-3 results for each. For which queries do the metrics disagree? Why?

  2. Chunk size experiment: index the sample documents with chunk sizes of 50, 100, 200, and 500 words. For each, run 5 queries and record the top-1 similarity score. Plot the relationship between chunk size and retrieval quality. Find the point where larger chunks start hurting.

  3. Matryoshka simulation: build a SimpleEmbedder that produces 500-d vectors. Truncate to 50, 100, 200, and 500 dimensions. Measure how retrieval recall degrades at each truncation. This simulates Matryoshka behavior without needing the real training trick.

  4. Binary quantization: take the embeddings from the search engine, convert them to binary (1 if positive, 0 if negative), and implement Hamming distance search. Compare the top-10 results against full-precision cosine similarity. Measure the overlap percentage.

  5. Sentence-based chunking: replace fixed-size chunking with chunk_by_sentences. Run the same queries and compare retrieval scores. Does respecting sentence boundaries improve the results?

Key Terms

TermWhat people sayWhat it actually means
Embedding"Text to numbers"A dense vector where geometric proximity encodes semantic similarity
Word2Vec"The OG embedding"2013 model that learned word vectors by predicting context words; proved vector arithmetic encodes meaning
Cosine similarity"How similar are two vectors"Cosine of the angle between vectors; 1 = identical direction, 0 = orthogonal, -1 = opposite
HNSW"Fast vector search"Hierarchical Navigable Small World graph -- multi-layer structure enabling O(log n) approximate nearest neighbor search
Bi-encoder"Embed separately, compare fast"Encodes query and document independently into vectors; enables pre-computation and fast retrieval
Cross-encoder"Slow but accurate reranker"Processes query-document pair jointly through the full model; higher accuracy, no pre-computation
Matryoshka embeddings"Truncatable vectors"Embeddings trained so the first N dimensions capture the most important information, enabling variable-size storage
Binary quantization"1-bit embeddings"Converting float vectors to binary (sign bit only) for 32x storage reduction with Hamming distance search
Chunking"Split docs for embedding"Breaking documents into 256-512 token segments so each can be independently embedded and retrieved
Vector database"Search engine for embeddings"Data store optimized for storing vectors and performing approximate nearest neighbor search at scale
Contrastive learning"Train by comparison"Training approach that pushes similar pair embeddings together and dissimilar pair embeddings apart
MTEB"The embedding benchmark"Massive Text Embedding Benchmark -- 56 datasets across 8 tasks; standard for comparing embedding models

Further Reading

  • Mikolov et al., "Efficient Estimation of Word Representations in Vector Space" (2013): the Word2Vec paper that started the embedding revolution with the king-queen analogy
  • Reimers & Gurevych, "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks" (2019): how to train bi-encoders for sentence-level similarity, foundation of modern embedding models
  • Kusupati et al., "Matryoshka Representation Learning" (2022): the technique behind variable-dimension embeddings that OpenAI adopted for text-embedding-3
  • Malkov & Yashunin, "Efficient and Robust Approximate Nearest Neighbor using Hierarchical Navigable Small World Graphs" (2018): the HNSW paper, the algorithm behind most production vector search
  • OpenAI Embeddings Guide (platform.openai.com/docs/guides/embeddings): practical reference for text-embedding-3 models including Matryoshka dimension reduction
  • MTEB Leaderboard (huggingface.co/spaces/mteb/leaderboard): live benchmark comparing all embedding models across tasks and languages
  • Muennighoff et al., "MTEB: Massive Text Embedding Benchmark" (EACL 2023): the benchmark defining 8 task categories (classification, clustering, pair classification, reranking, retrieval, STS, summarization, bitext mining) that the leaderboard reports; read before trusting any single MTEB score.
  • Sentence Transformers documentation: canonical reference for bi-encoder vs cross-encoder, pooling strategies, and the ingest-split-embed-store RAG pipeline this lesson implements. [ { "question": "What problem do embeddings solve that keyword search cannot?", "options": [ "Embeddings are faster", "Embeddings capture semantic meaning, matching 'payment didn't go through' with 'charge was declined' even though they share no words", "Embeddings use less storage", "Embeddings work offline" ], "correct": 1, "explanation": "Keyword search treats words as independent symbols. Embeddings map text to high-dimensional vectors where semantic similarity = geometric proximity. Texts with the same meaning cluster together regardless of word choice.", "stage": "pre" }, { "question": "What does cosine similarity measure between two embedding vectors?", "options": [ "The Euclidean distance", "The angle between the vectors, indicating how similar their directions are regardless of magnitude", "The sum of their components", "The number of matching dimensions" ], "correct": 1, "explanation": "Cosine similarity = dot(A,B) / (|A|*|B|). It ranges from -1 (opposite) to 1 (identical direction). Two texts with the same meaning will have vectors pointing in nearly the same direction, giving cosine similarity near 1.", "stage": "pre" }, { "question": "What is the typical dimensionality of modern text embedding models?", "options": [ "2-10 dimensions", "50-100 dimensions", "768-3072 dimensions", "100,000+ dimensions" ], "correct": 2, "explanation": "Modern embedding models (OpenAI text-embedding-3, BGE, E5) produce vectors with 768 to 3072 dimensions. Higher dimensions capture more nuance but cost more to store and search.", "stage": "post" }, { "question": "Why should you evaluate embedding quality using retrieval benchmarks rather than just inspecting similarity scores?", "options": [ "Similarity scores are always wrong", "Absolute similarity values vary by model; what matters is whether relevant documents rank higher than irrelevant ones (precision@k, recall)", "Retrieval benchmarks are faster", "Similarity scores don't use cosine distance" ], "correct": 1, "explanation": "A cosine similarity of 0.85 might mean 'very similar' for one model and 'somewhat similar' for another, since raw similarity scores aren't calibrated the same way across models. What matters is whether relevant documents actually rank above irrelevant ones, which is what retrieval benchmarks like MTEB measure directly.", "stage": "post" }, { "question": "When would you use a local/open-source embedding model instead of an API-based one?", "options": [ "Local models are always better", "When you need data privacy, offline operation, lower cost at scale, or domain-specific fine-tuning", "Local models produce higher quality embeddings", "API models don't support batching" ], "correct": 1, "explanation": "API embeddings (OpenAI, Cohere) are easy but send your data externally. Local models (BGE, E5, Nomic) keep data private, eliminate per-call costs at scale, and can be fine-tuned on domain-specific data.", "stage": "post" }, { "question": "What breakthrough did Word2Vec (Mikolov et al., 2013) demonstrate about word embeddings?", "options": [ "That embeddings must always have exactly 768 dimensions", "That keyword search outperforms semantic search for short queries", "That embeddings require labeled training data from crowdworkers", "That vector arithmetic on word embeddings captures semantic relationships, illustrated by king minus man plus woman equaling queen" ], "correct": 3, "explanation": "The lesson credits Word2Vec with the famous king - man + woman = queen result, showing the direction from man to woman roughly matches the direction from king to queen in vector space.", "stage": "check" }, { "question": "Of the four sentence-embedding approaches the lesson covers, which one uses a special transformer output token that was originally trained for next-sentence prediction rather than similarity?", "options": [ "Averaging", "CLS token", "Contrastive learning", "Instruction-tuned embeddings" ], "correct": 1, "explanation": "The lesson notes the CLS-token approach from transformer models like BERT is 'better than averaging but the [CLS] token was trained for next-sentence prediction, not similarity.'", "stage": "check" }, { "question": "How does HNSW (Hierarchical Navigable Small World) make vector search fast at scale, according to the lesson?", "options": [ "It builds a multi-layer graph where sparse top layers allow long-range jumps and dense bottom layers refine the search, returning approximate results in O(log n) instead of O(n)", "It deletes older vectors once the index reaches 1 million entries", "It compresses every vector to a single bit before comparing them", "It re-trains the embedding model on the query at search time" ], "correct": 0, "explanation": "The lesson describes HNSW as a multi-layer graph: searches start at sparse top layers and descend to dense bottom layers, trading a small accuracy loss for O(log n) search instead of brute-force O(n).", "stage": "check" }, { "question": "Per the lesson's vector database comparison table, which option is a Postgres extension best suited for teams already running Postgres?", "options": [ "Pinecone", "ChromaDB", "pgvector", "FAISS" ], "correct": 2, "explanation": "The lesson's table lists pgvector as a 'Postgres extension' best for teams 'already using Postgres,' scaling to roughly 10 million vectors.", "stage": "post" }, { "question": "Which chunking strategy does the lesson describe as trying section headers first, then paragraph boundaries, then sentence boundaries, then character limits?", "options": [ "Fixed-size chunking", "Sentence-based chunking", "Semantic chunking", "Recursive chunking" ], "correct": 3, "explanation": "The lesson defines recursive chunking as trying 'the largest boundary first (section headers)' and falling back through paragraphs, sentences, and character limits, citing LangChain's RecursiveCharacterTextSplitter as an example.", "stage": "check" }, { "question": "What production pattern does the lesson describe for combining bi-encoders and cross-encoders?", "options": [ "A bi-encoder retrieves a larger candidate set (for example, top-100), then a cross-encoder reranks it down to a smaller final set (for example, top-10)", "Use only cross-encoders, since bi-encoders are obsolete", "Run both independently and average their scores", "Use a bi-encoder for reranking after a cross-encoder narrows the candidates" ], "correct": 0, "explanation": "The lesson calls this the 'retrieve-then-rerank pipeline': a fast bi-encoder retrieves top-100 candidates, then a slower but more accurate cross-encoder reranks them down to top-10.", "stage": "check" }, { "question": "What makes Matryoshka embeddings different from traditional fixed embeddings, per the lesson?", "options": [ "They can only be generated by open-source models", "They double the vector's dimensionality automatically at query time", "They are trained so the first N dimensions carry the most important information, so truncating the vector still produces a usable, smaller embedding without retraining", "They eliminate the need for a similarity metric entirely" ], "correct": 2, "explanation": "The lesson compares Matryoshka embeddings to a nesting doll: the model is trained so the first N dimensions capture the most important information, letting a 1536-d vector truncate to 256 dimensions with only 3-5% accuracy loss.", "stage": "post" }, { "question": "How does binary quantization reduce vector storage, according to the lesson?", "options": [ "It deletes half of the stored vectors at random", "It stores vectors as compressed JPEG images", "It merges every two dimensions into one using averaging", "It converts each float in the embedding to a single bit (positive becomes 1, negative becomes 0), cutting a 6,144-byte vector down to 192 bytes, a 32x reduction" ], "correct": 3, "explanation": "The lesson describes binary quantization as converting each float to a single bit, shrinking a 1536-dimension float32 vector from 6,144 bytes to 192 bytes, searched afterward using Hamming distance.", "stage": "post" }, { "question": "According to the lesson, when does dot product similarity produce the same ranking as cosine similarity?", "options": [ "Only when using Euclidean distance instead", "When the vectors are normalized to unit length, as OpenAI's embeddings are, so dot product gives the same ranking as cosine similarity but computes faster", "Only when comparing exactly two vectors at a time", "Never; they always produce different rankings" ], "correct": 1, "explanation": "The lesson states dot product is 'identical to cosine similarity when vectors are normalized (unit length)' and notes OpenAI's embeddings are normalized, so the two metrics give the same ranking while dot product is faster to compute.", "stage": "check" }

Code

Ships With