What is an embedding?

An embedding is a list of numbers (a vector) that represents a word, sentence, or document. The trick: similar meanings get similar numbers.

"I love cats"
->
Embedding Model
->
[0.31, 0.82, -0.14, ..., 0.67]
hundreds to thousands of numbers

Think of it like GPS coordinates for meaning. Just like latitude/longitude tell you where a place is on Earth, an embedding tells you where a piece of text sits in "meaning space."

Before embeddings

Computers compared text mainly by matching exact words. "doctor" and "physician" were much harder to connect — methods like thesauri and TF-IDF helped, but required manual effort or missed semantic nuance.

With embeddings

"doctor" and "physician" get similar vectors because they appear in similar contexts during training. The computer now "knows" they mean similar things.


Embedding Space (2D visualization)

Real embeddings have hundreds to thousands of dimensions (384-3072 is typical). Here's a simplified 2D view — similar words cluster together.

Royalty Animals Medicine Food Programming

Hover over points to see labels. Click to add your own word.

Note: This shows proximity (Euclidean closeness), not cosine angle. In practice, most embedding models L2-normalize their vectors, which makes cosine similarity and Euclidean distance directly equivalent — close points = small angle from origin. This is a 2D simplification; real high-dimensional spaces have hundreds of dimensions that are not individually interpretable.


Measuring Similarity: Cosine vs Dot Product

Once you have vectors, how do you tell if two are "close"? Three common methods.

Cosine Similarity

Measures the angle between two vectors. Normalizes away magnitude — focuses on direction.

cos(A, B) = (A . B) / (|A| * |B|)
Range: -1 (opposite) to +1 (identical)

Best when: vector magnitudes vary, you want pure semantic direction.

Real-world example:
A short tweet "great coffee" and a long review "I absolutely loved the amazing artisan coffee, the rich aroma was incredible" should match as similar even though one has much stronger embedding magnitude. Cosine ignores that length difference — only direction (topic) matters.
Used by: Most semantic search systems (Pinecone, Weaviate default), Sentence-BERT, OpenAI recommends for text-embedding-3.

Dot Product

Multiplies matching dimensions and sums them up. Bigger vectors get higher scores even if they point the same direction.

dot(A, B) = sum(a_i * b_i)
Range: -inf to +inf

Best when: vectors are already L2-normalized (then it equals cosine).

Real-world example:
In a recommendation system, you want magnitude to matter. A user who is "very interested" in cooking (large vector) should get a stronger match to cooking content than someone who is "mildly interested" (small vector pointing the same way). Dot product captures both relevance and intensity.
Used by: FAISS inner product index, attention mechanism inside transformers (Q*K^T is a dot product), MIPS (Maximum Inner Product Search).

Euclidean Distance

Measures the straight-line distance between two points in vector space. Smaller = more similar.

d(A, B) = sqrt(sum((a_i - b_i)^2))
Range: 0 (identical) to +inf

Best when: absolute position matters, not just direction.

Real-world example:
In anomaly detection, you embed normal system logs into a cluster. A new log is anomalous if it's far away from the cluster in absolute terms — even if it points in a similar direction. Euclidean measures that "how far from the group" distance directly.
Used by: K-means clustering, DBSCAN, KNN classifiers, pgvector (Postgres) default, scikit-learn distance metrics.

Note: Some models intentionally encode information in vector magnitude (e.g., confidence or specificity). Cosine discards this. The right metric depends on how the embedding model was trained — always match the metric to the model's documentation.

Visual Intuition: Drag the vectors!

Drag the tips of vector A (red) and B (blue) to see how each metric changes. The three panels show what each metric actually measures geometrically.

Cosine Similarity
0.00
Measures the angle
Dot Product
0.00
Angle + magnitude
Euclidean Distance
0.00
Tip-to-tip distance
Try this: Make both vectors point the same direction but different lengths. Cosine stays at 1.0 (same direction = identical meaning), while dot product changes with length.
Try this: Make one vector very long and the other short but same direction. Dot product is huge. Now point the long one 90 degrees away — dot product drops to zero.
Try this: Move both tips close together (small distance = similar). Now keep them close but move both far from origin — Euclidean stays small even though they're far from zero.

How each metric behaves in 3 scenarios

These graphs show the key differences. Each scenario keeps one thing constant and varies another — watch how the metrics respond differently.

Scenario 1: Same direction, vary magnitude

B points same direction as A. X-axis: B's length increases.

Scenario 2: Same magnitude, vary angle

Both vectors length=1. X-axis: angle between them (0-180).

Scenario 3: Both move together (translate)

Both tips stay close but move far from origin.

--- Cosine --- Dot Product (scaled) --- Euclidean Distance (scaled)
Cosine is flat — it doesn't care about magnitude at all. Same direction = 1.0 regardless of length.
All three react to angle — but cosine and dot go negative for opposite directions while euclidean just keeps growing.
Euclidean is constant — if tips stay the same distance apart, it doesn't matter where they are. Cosine and dot change because direction from origin changes.

Numeric Calculator: Compare two vectors


L2 Normalization

Why engineers shrink all vectors to the same length before comparing them.

The Problem Without Normalization

Imagine two documents about "cats." One has a vector with large magnitudes [5.0, 3.0, 4.0] and another with small magnitudes [0.5, 0.3, 0.4]. They point in the same direction (same meaning!), but dot product gives wildly different scores.

Doc A (loud) [5.0, 3.0, 4.0] |norm| = 7.07
Doc B (quiet) [0.5, 0.3, 0.4] |norm| = 0.71

Doc A dominates just because its numbers are bigger, not because it's more relevant.

After L2 Normalization

Divide each vector by its length. Now every vector has length = 1. Only the direction (meaning) matters.

Doc A (normalized) [0.71, 0.42, 0.57] |norm| = 1.0
Doc B (normalized) [0.71, 0.42, 0.57] |norm| = 1.0

Now identical! Same direction = same meaning. Dot product and cosine give the same result.

Caveat: Normalization is a design choice, not magic. Whether it helps depends on how your embedding model was trained and how your vector index computes similarity. Some models intentionally encode useful information in magnitude — normalizing it away can hurt performance. Always check the model's recommended metric.


Token Embeddings: The First Step Inside Every LLM

Before a model can do anything — attention, reasoning, generation — it must convert each token into a vector. That's what the token embedding layer does.

What happens when you type "The cat sat"

Raw Text
"The cat sat"
Tokenizer
["The", "cat", "sat"]
Token IDs
[464, 3797, 3332]
Embedding Lookup
table[464] ->
table[3797] ->
table[3332] ->
Vectors
[0.12, -0.34, ...]
[0.87, 0.21, ...]
[0.44, 0.73, ...]
Step 1 Step 2 Step 3 Step 4 Step 5

The Embedding Table: A Giant Lookup Dictionary

The core of a token embedding layer is a table — one row per token in the vocabulary, one column per dimension. For GPT-2, that's 50,257 rows (tokens) x 768 columns (dimensions).

Embedding table (simplified to 5 dims):
Tokend0d1d2d3d4
ID 0 "!"0.02-0.150.330.07-0.21
ID 1 "\""0.110.08-0.220.140.31
...50,257 rows total
ID 464 "The"0.12-0.340.560.89-0.12
ID 3797 "cat"0.870.21-0.430.150.68
...

Given token ID 464 ("The"), the model just looks up row 464 and returns that 768-dimensional vector. No computation — just a table lookup. These values are learned during training.

Key facts about token embeddings

Static at input — The embedding table gives each token the same initial vector regardless of context. "bank" gets the same vector whether it means riverbank or savings bank. Context comes later from the attention layers.
Position gets added — After looking up the token embedding, a positional encoding is added (element-wise sum). So the model sees token_embedding + position_embedding as its input to layer 1.
Sometimes shared with the output layer — In some models (GPT-2, T5, Gemma), the same embedding table is reused in reverse at the output: the model's final hidden state is multiplied by the embedding table to produce next-token probabilities. This is called "weight tying." Other models (LLaMA family, Mistral) use separate input and output matrices for better quality.
Significant memory cost — GPT-2 Small: 50,257 x 768 = ~38M params (~31% of total!), tied input/output so counted once. LLaMA 3 8B: 128,256 x 4,096 = ~525M per matrix, and since LLaMA uses untied input and output embeddings that's ~1,051M total (~13%). LLaMA 3 70B: ~1,051M per matrix → ~2,101M total (~3%). The fraction shrinks as models get larger — embeddings are a fixed cost that gets amortized.

Token Embeddings vs Contextual Embeddings: The Journey Through the Model

Token embeddings are just the starting point. As they pass through each transformer layer, they get transformed by attention and feed-forward networks. By the time they exit the final layer, they're completely different — they're now contextual representations that encode the meaning of that token in this specific sentence.

Token Embedding
(Layer 0 — input)
"bank" = same vector
every time, no context
Context-free
->
Hidden States
(Layers 1-N — processing)
Attention mixes info from
other tokens each layer
Gaining context...
->
Contextual Embedding
(Final layer — output)
"bank" near "river" =
different from "bank" + "loan"
Fully contextual

Example: "river bank" vs "bank account"

Layer 0 (input): "bank" = [0.44, 0.12, -0.33, ...] (identical in both sentences!)
Layer 6: Starting to diverge as attention mixes "river" or "account" context into "bank"
Layer 12 (final): Completely different vectors — the model has resolved the ambiguity

Why this matters for search

Token embeddings (Layer 0) are bad for search — they're context-free, so "bank" always matches "bank" regardless of meaning.
Sentence embeddings pool the final-layer contextual outputs into one vector — they capture the meaning of the whole sentence, not just individual words. This is what search engines use.

Sentence & Document Embeddings

Taking the contextual outputs from the final layer and compressing them into a single vector for the whole text.

Sentence Embeddings

One vector for the whole sentence/paragraph. Created by pooling the final-layer token outputs:

[CLS] pooling — Use the special first-token output (BERT-style)
Mean pooling — Average all token outputs (most common)
Last-token — Use the final token's output (decoder models)

Use for: Semantic search, RAG, clustering, duplicate detection

Document Embeddings

One vector for an entire document. Two approaches:

Direct encoding — Feed the whole document through a long-context model (Longformer, Jina Embeddings v3, nomic-embed at 8K)
Chunk-and-combine — Split into chunks, embed each separately, then combine (mean, weighted average, or max pooling). Lossy but works for any length.

Use for: Document retrieval, recommendations, topic clustering


Dense vs Sparse Representations

Two fundamentally different ways to represent text — and why modern systems use both.

Dense Embeddings

Every dimension has a value — 768 numbers, all meaningful

Strength: Captures semantic meaning. "doctor" and "physician" are close even with zero word overlap.
Weakness: Can retrieve conceptually related but wrong content. May miss exact terms.

Sparse Retrieval (TF-IDF vectors, BM25 ranking)

TF-IDF produces explicit sparse vectors. BM25 is a ranking function (not a vector) that shares the same keyword-matching philosophy.

Strength: Exact keyword matching. Won't miss "product-ID-12345."
Weakness: No semantic understanding. "doctor" and "physician" are completely unrelated.

Hybrid Retrieval: Best of Both

User Query
->
Dense Search (semantic)
in parallel
Sparse Search (keyword / BM25)
->
Merge & Rerank

Both searches run simultaneously on the same query. Dense finds semantically relevant docs ("physician" matches "doctor"); sparse ensures exact terms aren't missed ("product-ID-12345"). Results are merged and reranked before returning.


Bi-Encoder vs Cross-Encoder

Two architectures for comparing text — one is fast, the other is accurate. Production uses both.

Bi-Encoder (the fast librarian)

Query
->
Encoder
->
Vector
Doc
->
Encoder
->
Vector

Encodes query and document separately. Document vectors can be pre-computed and stored in a vector database. Comparison = fast vector similarity.

Speed: Milliseconds over millions of docs

Accuracy: Moderate (no cross-attention)

Cross-Encoder (the careful reviewer)

Query + Doc
->
Full Attention
->
Score

Processes query and document together through the full model. Every word can attend to every other word. Much richer comparison but can't pre-compute.

Speed: Slow (must run model per pair)

Accuracy: High (full cross-attention)

The Production Pattern: Two-Stage Retrieval

1M documents
->
Bi-Encoder
recall top 100
->
Cross-Encoder
rerank to top 10
->
Final results

Hubness & Anisotropy

Two problems that can silently break your vector search even when the embeddings look fine.

Hubness

Some vectors become "hubs" — they show up as nearest neighbors for way too many queries, even unrelated ones. Like a popular kid who gets picked for every team.

Query: "best pizza in Seattle" -> Hub doc
Query: "quantum physics intro" -> Hub doc
Query: "cat grooming tips" -> Hub doc
Same doc keeps showing up!

Red hub point is nearest neighbor to many unrelated queries (arrows).

Anisotropy

Vectors crowd into a narrow cone instead of spreading evenly across the space. Like everyone at a party standing in one corner.

All cosine similarities are 0.85-0.99
Everything looks "similar" to everything!
Hard to distinguish truly relevant from irrelevant.

Left: healthy (spread out). Right: anisotropic (crowded into a cone).

Fixes: Better negative sampling during training, L2-normalization, post-hoc whitening transforms, adding a reranking stage, or switching to a better-trained embedding model.


Embedding Dimension: The Trade-off

More dimensions = richer meaning but higher cost. How to pick the right size.

768
ModelDimensionsStorage per 1M docsUse case
text-embedding-3-small512-1536~2-6 GBGeneral search, budget-friendly
text-embedding-3-large256-3072~1-12 GBHigh-fidelity retrieval
all-MiniLM-L6-v2384~1.5 GBFast, lightweight search
BERT base768~3 GBGeneral NLP tasks
E5-large-v21024~4 GBHigh-quality retrieval

Evaluating Embeddings

How do you know if your embeddings are actually good before putting them in production?

For Retrieval

Recall@K — Of the relevant docs, how many are in the top K results?
Precision@K — Of the top K results, how many are actually relevant?
MRR — How high does the first correct result rank?
nDCG — Are the best results at the very top? Accounts for graded relevance.
MAP — Mean Average Precision across multiple queries.

For Clustering

Cluster purity — Do clusters contain only one topic?
Silhouette score — Are clusters well-separated? Range: -1 to 1.
Adjusted Rand Index — How well do clusters match ground-truth labels?
V-measure — Harmonic mean of homogeneity and completeness.
Manual spot-check — Do neighbors make sense to a human?

For Similarity / Classification

STS benchmark — Spearman correlation with human similarity judgments.
Linear probe accuracy — Train a linear classifier on frozen embeddings.
kNN accuracy — Classify by majority vote of k nearest neighbors.
MTEB score — Massive Text Embedding Benchmark — tests across 56+ tasks.

The golden rule:

Never evaluate embeddings in isolation. Test them inside the full pipeline they'll power — the same embedding model can perform very differently depending on the chunking strategy, query rewriting, reranking, and downstream task. MTEB is the standard leaderboard for comparing embedding models across tasks.


The Full Semantic Search Pipeline

How embeddings fit into a real retrieval system from end to end.

Indexing Phase (offline, once)

Documents
->
Chunk
->
Embed each chunk
->
Vector Database
(FAISS, Pinecone, etc.)

Query Phase (online, per request)

User Query
->
Embed query
->
Vector search
(ANN lookup)
->
Rerank
(cross-encoder)
->
LLM generates
answer + citations

How Are Embeddings Trained?

Embeddings don't appear by magic. Models learn them through clever training objectives.

Contrastive Learning

The most common approach for modern embedding models. The idea is simple:

1. Take a sentence (the "anchor")
2. Pair it with a similar sentence (the "positive")
3. Pair it with an unrelated sentence (the "negative")
Train the model to push anchor+positive vectors closer together and anchor+negative vectors further apart.

This is how Sentence-BERT (Reimers & Gurevych, 2019) made sentence-level similarity search practical — by training BERT with contrastive objectives instead of using raw BERT outputs.

Negative Sampling

The quality of the "negative" examples matters enormously. Two strategies:

Random negatives: Grab any random sentence from the batch. Easy but the model learns to distinguish obvious differences, not subtle ones.
Hard negatives: Pick sentences that are similar but NOT relevant (e.g., same topic, wrong answer). Forces the model to learn fine-grained distinctions. Much better embeddings, but harder to construct training data.

Poor negative sampling is one of the main causes of hubness and anisotropy problems.


Domain Mismatch: Not All Embeddings Are Interchangeable

An embedding model trained on Wikipedia won't necessarily work well for legal contracts or medical records.

The Problem

Embedding models learn from their training data. A general model understands everyday language but may not capture the specialized meaning of domain-specific terms — placing them near common-language meanings instead of domain-relevant ones.

General model vs. what you need:
"stat" ~ "statistics" (general meaning)
"stat" ~ "urgent/immediately" (medical context: "give meds stat")
"discharge" ~ "release/emit" (general meaning)
"discharge" ~ "patient leaving hospital" (clinical context)
A general model may retrieve docs about electrical discharge when you're searching for patient discharge summaries.

The Fix

Match your embedding model to your domain:

1. Evaluate on YOUR data — never trust generic benchmarks alone
2. Fine-tune — retrain the last layers on domain-specific pairs
3. Use domain-specific models — e.g., PubMedBERT for medical, CodeBERT for code
4. Test retrieval + generation — good retrieval doesn't guarantee good downstream LLM answers

How Image Embeddings Work

Before we get to multimodal, let's understand how a model turns raw pixels into a vector — the same way tokenization turns text into vectors.

From pixels to vector: step by step

Raw Image
224x224 pixels
3 channels (RGB)
= 150,528 numbers
Patch Split
Cut into 16x16 patches
= 196 patches
(like "tokens")
Patch Embeddings
Each patch -> vector
via linear projection
+ position encoding
Transformer
Self-attention across
all 196 patches
(same as text!)
Image Vector
Pool/[CLS] output
= one vector
for the whole image

The key insight: patches are tokens

The Vision Transformer (ViT) treats image patches exactly like text tokens. A 224x224 image split into 16x16 patches gives 196 "tokens." Each gets embedded, position-encoded, and fed through transformer layers — the same architecture as GPT or BERT.

Each colored square = one 16x16 patch = one "token"

Before ViT: CNNs produced image embeddings too

Before Vision Transformers (2020), Convolutional Neural Networks (CNNs) like ResNet were the standard. They slide small filters across the image, building up feature maps layer by layer — edges -> textures -> parts -> objects.

The final layer's output (before the classification head) is the image embedding. ResNet-50 produces a 2048-dim vector. That vector captures "what's in this image" in a way that similar images get similar vectors.

ViT replaced CNNs for most tasks because transformers scale better and transfer-learn more effectively, but the core idea — compress an image into a fixed-size vector — is the same.


CLIP: How Multimodal Models Are Actually Trained

CLIP (Contrastive Language-Image Pretraining) by OpenAI taught models to understand both text and images by training them together. Here's exactly how.

The Training Setup

CLIP was trained on 400 million (image, text) pairs scraped from the internet — photos with their captions, alt-text, titles, etc. The training objective is elegantly simple:

Image Encoder
(ViT or ResNet)
photo of a dog -> [0.3, -0.1, ...]
Match?
~
cosine similarity
in shared space
Text Encoder
(Transformer)
"a golden retriever" -> [0.3, -0.1, ...]

Contrastive training: the NxN matrix trick

In each training batch of N (image, text) pairs, CLIP computes the similarity between every image and every text — an NxN matrix. The diagonal (matching pairs) should be high; everything else should be low.

Similarity matrix (batch of 4)
"dog on
beach"
"red
car"
"sunset
sky"
"coffee
cup"
dog pic 0.92 0.05 0.12 0.03
car pic 0.08 0.89 0.11 0.06
sunset pic 0.14 0.07 0.95 0.09
coffee pic 0.02 0.04 0.06 0.91
Diagonal = matching pairs (push high)
Off-diagonal = non-matching (push low)
Step 1: Encode all N images through the image encoder -> N image vectors
Step 2: Encode all N texts through the text encoder -> N text vectors
Step 3: Compute NxN cosine similarity matrix between all image-text pairs
Step 4: Loss = cross-entropy on both rows (image->text) and columns (text->image). Matching pairs should be highest in their row AND column.

With batch size of 32,768 (CLIP's actual setting), each image is contrasted against 32,767 wrong captions. This massive scale is what makes the learned space so robust.

What CLIP's shared space looks like

After training, text and images live in the same 512/768-dim vector space. You can compare any text to any image (or image to image, or text to text) using cosine similarity.

Circles = images. Diamonds = text. Same color = matching pair. They cluster together in the shared space.

Zero-shot classification (no training needed!)

CLIP can classify images it has never seen during training. You just embed the class names as text and find which one is closest to the image:

Image of a dog -> embed -> [0.3, -0.1, ...]

"a photo of a dog" -> [0.3, -0.1, ...] cos=0.92
"a photo of a cat" -> [0.1, 0.5, ...] cos=0.31
"a photo of a car" -> [-0.2, 0.8, ...] cos=0.08

Winner: dog (no fine-tuning needed)

Why this was a breakthrough

Before CLIP, image classifiers needed labeled datasets per task. Want to classify dogs vs cats? Train on a dog/cat dataset. Want to classify cars? Train on a car dataset.

CLIP changed this: one model, trained once on image-text pairs, can do any classification task just by changing the text prompts. No retraining. This is the image equivalent of what GPT-3 did for text with few-shot prompting.


Beyond CLIP: The Multimodal Embedding Zoo

CLIP opened the door. Now there are specialized models for every modality combination.

ModelModalitiesTraining approachKey use case
CLIP Text + Image Contrastive (NxN matching) Zero-shot image classification, text-to-image search
SigLIP Text + Image Sigmoid loss (per-pair, not NxN) Same as CLIP but scales to larger batches more efficiently
ALIGN Text + Image Contrastive on 1.8B noisy pairs Shows scale > curation for multimodal learning
ImageBind Text + Image + Audio + Depth + Thermal + IMU Bind all modalities to images as anchor Cross-modal retrieval (audio -> image, text -> depth, etc.)
CLAP Text + Audio Contrastive (like CLIP for audio) Audio search, music recommendation, sound classification
ONE-PEACE Text + Image + Audio Shared transformer backbone Unified multimodal understanding

CLIP vs GPT-4V: Encode-only vs Encode+Decode

CLIP can only compare images and text (which is closer?). To actually reason about an image and generate an answer, you need to wire a vision encoder into an LLM decoder. Here's the key architectural difference:

CLIP (encode only — no decoder)

Image
->
ViT Encoder
->
1 vector
Text
->
Text Encoder
->
1 vector
compare with cosine similarity

Output: a similarity score (0-1). Can answer "is this a dog or cat?" by comparing to text labels. Cannot generate text, explain reasoning, or answer open-ended questions.

GPT-4V / LLaVA (encoder + LLM decoder)

Image
->
ViT
(frozen CLIP)
->
Projection
MLP bridge
->
LLM Decoder
GPT / LLaMA
->
Text
answer

The CLIP vision encoder is reused (often frozen) but its output goes through a projection layer into an LLM that can reason and generate. Can answer "what breed is this dog and why do you think so?"

Inside the LLM decoder: image patches become tokens

The critical insight: the vision encoder doesn't produce one vector for the whole image. It produces one vector per patch — hundreds of them. These get projected and concatenated with text tokens, so the LLM "reads" the image the same way it reads text.

What the LLM actually sees as input:
[ img_patch_1 img_patch_2 ... img_patch_576 | What breed is this dog ? ]
576 image tokens (from ViT + projection) + 6 text tokens (from tokenizer) = 582 total
Step 1: Encode

Image (384x384) -> ViT splits into 24x24 = 576 patches of 16x16 pixels. Each patch becomes a 1024-dim vector. The ViT is often a frozen CLIP encoder — reusing what CLIP already learned about images.

Step 2: Project

A learned MLP maps each 1024-dim patch vector to the LLM's hidden size (e.g., 4096-dim). This is the "bridge" — often the only new component trained when building a multimodal LLM.

Step 3: Attend + Generate

The LLM's self-attention runs over all 582 tokens. Text tokens attend to image patches ("breed" attends to the fur texture patches) and generate: "This is a golden retriever."

This is why image inputs cost more — a single 384x384 image consumes 576 tokens. Higher resolution = more patches = more tokens = higher cost and latency.

Audio and video work the same way

Audio Embeddings

Audio is converted to a spectrogram (a 2D image of frequency vs time), then processed like an image through a ViT or CNN. Models like Whisper, CLAP, and AudioMAE produce audio vectors that can be compared to text or other audio.

Waveform -> Mel spectrogram (2D) -> Patch/CNN encoder -> Audio vector

Video Embeddings

Sample frames at regular intervals (e.g., 1 fps), embed each frame independently with ViT, then aggregate across time. Some models (ViViT, VideoMAE) process 3D patch tubes (space + time) in a single pass.

Sample frames -> Embed each -> Temporal attention -> Video vector