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.
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 floatsSimilarity 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 matchThe 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-4821may 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 likevoyage-code-3mitigate 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.