Building RAG Systems

Lesson 6 of 23

Embeddings

What an embedding encodes

An embedding model maps a span of text to a fixed-dimensional dense vector — a list of floating-point numbers, typically 256 to 3,072 dimensions. Two texts with similar meaning produce vectors close together in this space; unrelated texts produce distant vectors.

The model learns this mapping from large text corpora, typically with a contrastive objective: pull matching (query, passage) pairs closer, push non-matching pairs apart. The resulting vector encodes a compressed representation of the text's meaning at the granularity of the input span — not individual words, not syntax, but the aggregate semantics.

This property — semantic proximity as geometric proximity — is the mechanism that makes vector search work.

Embedded chunks cluster by meaning in vector space: similar topics land close together regardless of their wording
Embedded chunks cluster by meaning in vector space: similar topics land close together regardless of their wording

Model landscape

The embedding space has consolidated around a few families. Key specs to compare:

  • text-embedding-3-small (OpenAI) — 1,536 dimensions, 8,191-token context. Good cost/quality ratio. Supports Matryoshka dimension reduction (truncate to 256 or 512 dims with modest quality loss).
  • text-embedding-3-large (OpenAI) — 3,072 dimensions, 8,191-token context. Higher quality, 2x storage cost.
  • all-MiniLM-L6-v2 (Sentence Transformers) — 384 dimensions, 256-token context. Open-source, runs locally, fast. Short context is the main constraint.
  • bge-large-en-v1.5 (BAAI) — 1,024 dimensions, 512-token context. Strong open-source retrieval model.
  • voyage-3 (Voyage AI) — 1,024 dimensions, 32,000-token context. Long-context embeddings with a code-aware variant.
  • embed-v4.0 (Cohere) — 1,024 dimensions, 512-token context. Separates input types (query vs. document).

Higher dimensions capture more information but cost more to store and search. OpenAI's text-embedding-3-small supports Matryoshka dimensionality reduction — you can request fewer dimensions at embedding time:

python
from openai import OpenAI

client = OpenAI()

response = client.embeddings.create(
    model="text-embedding-3-small",
    input="How does retrieval-augmented generation work?",
    dimensions=512,  # truncate from default 1536
)
vector = response.data[0].embedding  # list of 512 floats

Similarity metrics

Three standard metrics for comparing vectors a and b of dimension d:

Cosine similarity

cos(a,b)=(ab)(ab)\cos(a, b) = \frac{(a \cdot b)}{(\|a\| \cdot \|b\|)}

. Ranges from -1 to 1. Ignores magnitude, compares direction only. This is the default for most embedding models because they produce normalized vectors (unit length), where cosine similarity equals the dot product.

Dot product (inner product)

ab=aibia \cdot b = \sum a_{i} \cdot b_{i}. For normalized vectors, identical to cosine similarity. For unnormalized vectors, magnitude matters — a longer vector scores higher regardless of direction.

Euclidean (L2) distance

ab=(aibi)2\|a - b\| = \sqrt{\sum (a_{i} - b_{i})^{2}}

. Lower is more similar. For normalized vectors, L2 distance is a monotonic function of cosine similarity: L22=2(1cos)\text{L2}^{2} = 2(1 - \cos), so the rankings are identical.

Which to use: Check your model's documentation. Most models (OpenAI, Sentence Transformers) output normalized vectors and recommend cosine similarity. Using the wrong metric doesn't produce an error — it produces silently degraded retrieval.

The query-document coupling

A hard constraint: the same embedding model must encode both the document chunks (at index time) and the query (at query time). A chunk embedded with text-embedding-3-small lives in a 1,536-dimensional space that is incompatible with a query embedded by bge-large-en-v1.5 — different dimensions, different learned spaces. Mixing models produces random results, not errors.

This means switching embedding models requires re-embedding the entire corpus. At scale, this is a significant cost. Choose your model before indexing production data, and treat model migration as a planned operation.

python
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("all-MiniLM-L6-v2")

# Index time: embed chunks
chunks = [
    "PostgreSQL supports JSON columns with GIN indexing.",
    "The refund policy allows returns within 30 days.",
    "Vector similarity search finds nearest neighbors in embedding space.",
]
chunk_vectors = model.encode(chunks, normalize_embeddings=True)

# Query time: same model
query = "How does vector search work?"
query_vector = model.encode([query], normalize_embeddings=True)[0]

# Cosine similarity = dot product for normalized vectors
similarities = np.dot(chunk_vectors, query_vector)
# [0.18, 0.05, 0.71] — chunk 2 is the clear match

The query "How does vector search work?" embeds close to the chunk about vector similarity search despite sharing only the word "vector." The model captures semantic overlap, not just lexical overlap. This is the core capability of dense retrieval — and also the source of its failure modes.

When embeddings fail

Dense embeddings encode meaning, not symbols. This creates systematic blind spots:

  • Exact identifiers. A query for error code ERR-4821 may not embed close to a chunk containing that code. The model treats it as an opaque token with little semantic content. Keyword search handles this — which is why hybrid retrieval exists (Lesson 9).
  • Negation. "This method does NOT support batch processing" and "This method supports batch processing" embed very similarly. The model captures the topic but not the negation.
  • Domain-specific terminology. A general-purpose model may not distinguish "transformer" (ML architecture) from "transformer" (electrical device). Domain-specific models like voyage-code-3 mitigate this.
  • Input truncation. Text exceeding the model's max tokens is silently truncated. A 2,000-token chunk embedded with all-MiniLM-L6-v2 (256-token limit) loses 87% of its content. Always verify chunk size fits the embedding model's context window.

These are structural limitations of dense retrieval, not bugs. The Retrieval section covers the techniques (hybrid search, reranking) that compensate.

Everything above is the working subset. If embeddings are where your pipeline is stuck — or if you simply want to know why any of this works — our Embeddings, End to End track takes the same ground to the bottom: the history from one-hot to contrastive bi-encoders with every formula, what makes an embedding space good, MTEB read honestly, the retrieval metrics that decide, and embeddings for images, audio and graphs.

← Previous