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:

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) = (a . b) / (||a|| * ||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)

a . b = sum(a_i * 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

||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: 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.

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.

Key takeaways

  • An embedding maps text to a dense vector where semantic similarity corresponds to geometric proximity — the mechanism behind vector search.
  • Choose the similarity metric your model was trained for (usually cosine for normalized vectors); the wrong metric silently degrades retrieval.
  • The same model must encode both chunks and queries — mixing models produces random results, not errors.
  • Dense embeddings fail on exact identifiers, negation, and domain terms; hybrid retrieval compensates for these gaps.
← Previous