You shall know a word
by the company it keeps.
— J.R. Firth, 1957
This single sentence, written sixty-six years before ChatGPT,
is the theoretical foundation of every embedding in existence.
01 — The Problem Token IDs Don't Solve
Yesterday we learned that tokenization converts text into numbers. Your message becomes a list of integers before the model ever processes it. "cat" → 9246. "dog" → 10077. "kitten" → 19772.
But here is the problem: those numbers are arbitrary. 9246 and 10077 are just entries in a lookup table — they carry no relationship to each other. The number 9246 is no closer to 10077 than it is to 45,000. Yet "cat" and "dog" are very closely related — both are household pets, both are animals, both appear in similar sentences. "cat" and "kitten" are even more closely related: one is literally the younger form of the other.
Raw token IDs are useless for capturing this. They are the equivalent of naming things with random serial numbers. So we need a different kind of representation — one where similar words produce similar numbers. Not a single number per word, but a list of hundreds of numbers — a vector — arranged so that words with similar meanings cluster together in space.
How do we represent words as numbers in a way that similar words produce similar numbers? So that the distance between "cat" and "dog" is small, the distance between "cat" and "politics" is large, and the direction from "king" to "queen" is the same direction as from "man" to "woman"? This is the problem word embeddings solve.
cat → 9246 (just a serial number)
dog → 10077 (no relationship to 9246)
politics → 45231 (equally "different" from cat as dog is)
Embedding vector (captures meaning):
cat → [ 0.82, -0.13, 0.45, 0.71, -0.22, ... ] (300 numbers)
dog → [ 0.79, -0.17, 0.41, 0.68, -0.19, ... ] (300 numbers — nearly identical!)
politics → [-0.12, 0.63, -0.38, -0.04, 0.77, ... ] (very different direction)
The vector for "cat" and the vector for "dog" are nearly parallel — they point in almost the same direction in 300-dimensional space. "Politics" points in a completely different direction. Now similarity is measurable, and arithmetic on concepts becomes possible.
02 — The Distributional Hypothesis: Meaning From Context
Before the algorithms, we need the theoretical foundation. Why should training on text co-occurrence produce vectors that capture meaning? The answer comes from linguistics, not computer science.
In 1957, linguist J.R. Firth wrote: "You shall know a word by the company it keeps." This is the distributional hypothesis — the idea that words with similar meanings tend to appear in similar contexts.
This is the magic of large-scale text. You don't need a linguist to manually write that "cat" and "kitten" are related — you just need to observe that they appear in the same contexts millions of times. The mathematical structure of co-occurrence is the structure of meaning, at least to a very good first approximation.
The distributional hypothesis is the theoretical justification for all count-based and prediction-based embedding methods. It doesn't capture everything — it conflates polysemous words ("bank" the institution vs "bank" the river), is sensitive to corpus domain, and misses logical operators like negation. But it captures an extraordinary amount of semantic structure from raw co-occurrence statistics alone, which is why every major embedding approach since 2013 has been built on some form of this idea.
03 — Why One-Hot Encoding Fails
Before Word2Vec, the dominant approach was one-hot encoding. Understanding why it fails makes the embedding solution much clearer.
Vocabulary: [cat, dog, kitten, politics, run]
cat → [1, 0, 0, 0, 0]
dog → [0, 1, 0, 0, 0]
kitten → [0, 0, 1, 0, 0]
politics→ [0, 0, 0, 1, 0]
Cosine similarity between any two different words:
cos(cat, dog) = 0.0 ← orthogonal, no relationship captured
cos(cat, kitten) = 0.0 ← same distance as cat↔politics!
cos(cat, politics) = 0.0 ← indistinguishable from all others
Every word is equally far from every other word. "cat" is as different from "dog" as it is from "galaxy." The representation carries zero semantic information.
The solution is to learn dense, low-dimensional representations: vectors of 100–1000 numbers (not 50,000), where similar words have similar vectors. This is exactly what Word2Vec introduced in 2013.
04 — Word2Vec: Learning Meaning From Prediction
In 2013, Tomas Mikolov and colleagues at Google published Word2Vec — a shockingly simple neural network that produced astonishingly good word representations. The central insight was counterintuitive: don't try to count co-occurrences. Train a network to predict words from their context, and read off the learned weights as the embeddings.
Word2Vec comes in two symmetric flavours:
The Architecture
The network is almost embarrassingly simple: input → one hidden layer (no activation) → output softmax. What matters is the task, not the architecture.
Input: one-hot vector for centre word → shape [V]
(e.g. "cat" = [0,0,...,1,...,0])
Hidden: W_in ∈ ℝ^(V×d) ← THIS IS THE EMBEDDING MATRIX
x = W_in[word_id] ← just a row lookup, no multiplication
shape: [d] (e.g. d = 300)
Output: W_out ∈ ℝ^(d×V) → softmax over vocab → P(context word)
Loss: Cross-entropy between predicted distribution and actual context words
After training: W_out is discarded. W_in rows are the word embeddings.
By training to predict context words from a centre word, the hidden layer weights are forced to encode what contexts each word appears in. Two words that consistently appear in similar contexts will converge toward similar weight vectors — because similar vectors produce similar predictions, and similar predictions incur similar losses on similar training examples.
The Training Trick: Negative Sampling
Computing softmax over a 50,000-word vocabulary is expensive — you normalise over all 50,000 outputs for every training step. Word2Vec uses negative sampling to avoid this: instead of predicting the full distribution, train a binary classifier. For each (centre, context) pair, ask "is this a real pair or a randomly sampled fake?"
Maximise: log σ(v_centre · v_context) ← real pair scores high
+ Σ log σ(−v_centre · v_noise_k) ← k random words score low
σ = sigmoid: σ(x) = 1/(1+e^−x)
k = noise samples per real pair (typically 5–20)
Instead of normalising over 50,000 words → only 1+k comparisons per step
Speedup: 1,000–10,000× for large vocabularies
The Famous Analogy Arithmetic
Once trained on a large corpus, something remarkable emerges. You can do arithmetic on meaning:
The direction from "man" to "woman" in embedding space consistently encodes the concept of gender — because gender-correlated contexts are consistent across the corpus. The direction from "France" to "Paris" consistently encodes "capital city of" — because capital city naming patterns are consistent across thousands of sentences. Arithmetic on vectors captures arithmetic on the underlying conceptual relationships. The same mechanism that reveals the king→queen relationship also reveals the doctor→nurse gender bias, which is a direct mirror of the bias in the training text.
What Word2Vec Gets Wrong
05 — GloVe: Global Vectors for Word Representation
Word2Vec is trained locally — it looks at one (centre, context) pair at a time. Stanford researchers Pennington, Socher, and Manning argued in 2014 that this throws away global statistical information available in the full corpus. Their approach: start from the full co-occurrence matrix and factorize it directly.
The Co-Occurrence Matrix
Before training, GloVe counts how often every word pair appears within a fixed window across the entire corpus, building a matrix X where X[i][j] = times word j appears in the context of word i.
The GloVe Objective
J = Σᵢⱼ f(Xᵢⱼ) · (vᵢᵀ vⱼ + bᵢ + bⱼ − log Xᵢⱼ)²
vᵢ, vⱼ = embedding vectors for words i and j
bᵢ, bⱼ = scalar bias terms per word
Xᵢⱼ = co-occurrence count of words i and j
f(Xᵢⱼ) = weighting function — down-weights very frequent pairs
Goal: make the dot product of two word vectors ≈ log(co-occurrence count)
Weighting function (prevents "the", "is", "a" from dominating):
f(x) = (x/x_max)^α if x < x_max, else 1.0
α = 0.75, x_max = 100 (empirically tuned in the paper)
GloVe vs Word2Vec
| Property | Word2Vec | GloVe |
|---|---|---|
| Learning approach | Prediction (local windows) | Matrix factorisation (global stats) |
| Statistical scope | Local — one window at a time | Global — full corpus co-occurrence |
| Memory during training | Low — streams one window | Higher — stores V×V matrix (sparse) |
| Analogy quality | ✓ Excellent | ✓ Excellent (often slightly better) |
| Loss interpretability | Prediction probability | Dot product ≈ log co-occurrence |
| Polysemy | ✗ One vector per word | ✗ One vector per word |
In practice, both produce embeddings of comparable quality for most downstream tasks. Many practitioners simply try both and pick whichever works better for their specific use case. The shared limitation — one vector per word regardless of context — is what both leave for the next generation to solve.
06 — The Polysemy Problem and Contextual Embeddings
Both Word2Vec and GloVe assign one fixed vector to each word type, regardless of context. But many words have completely different meanings in different sentences.
The solution is context-dependent embeddings: instead of one fixed vector per word type, compute a different vector for each word occurrence based on the surrounding sentence. This is what contextual embeddings do — and it's the bridge from Word2Vec/GloVe to the modern transformer era.
ELMo: The First Contextual Embeddings (2018)
ELMo (Embeddings from Language Models, AllenNLP 2018) was the first widely adopted contextual approach. It runs each sentence through a bidirectional LSTM and uses the resulting hidden states as word representations. The same word "bank" in two different sentences produces two different ELMo vectors — because the hidden states encode the sentence history.
Forward LSTM: h→ₖ = context of words 1 ... k (left context)
Backward LSTM: h←ₖ = context of words k ... N (right context)
ELMo(wₖ) = γ · Σⱼ sⱼ · hⱼₖ
sⱼ = learned weights per LSTM layer (sum to 1)
γ = task-specific scaling factor
"bank" in a financial sentence → different hidden states → different vector
than "bank" in a river sentence ✓ — polysemy finally handled
ELMo was a major advance, but was quickly superseded by BERT (2018), which replaces LSTMs with transformers — attending to the full sentence simultaneously at every layer rather than processing left-to-right and right-to-left in two separate passes. Day 26 covers BERT and GPT in depth.
07 — Sentence Transformers: Embedding Whole Sentences
Word-level embeddings answer "what does this word mean here?" But many real applications need a different answer: "how similar is the meaning of sentence A to sentence B?" — and they need it to be fast enough to search millions of documents.
This is the problem sentence transformers solve, and it's the core technology behind semantic search, RAG retrieval, duplicate detection, and recommendation systems.
The Naive Approach and Why It Fails
sentence_vec = (1/N) · Σᵢ word_vec(wᵢ)
Problem 1: "A dog bit a man" ≈ "A man bit a dog"
Same words → same average → identical vector
Completely different meaning — invisible to mean pooling
Problem 2: "Not good" → vector ≈ average(not, good)
Negation is completely lost — the result sits between positive and negative
Problem 3: Stopwords dominate long texts
"the", "a", "is" appear so often they dilute semantically rich words
Why Raw BERT Doesn't Scale to Semantic Search
BERT produces its best sentence representation by reading two texts jointly — the query and the document are concatenated and passed through together, so attention flows between them inside the model. This is called a cross-encoder. But it means you can't pre-compute document embeddings: every time a new query arrives, you must run BERT on every (query, document) pair. For 10,000 documents at ~50ms per pass → 8 minutes per query. Completely unusable in production.
Sentence-BERT: The Siamese Solution (2019)
Nils Reimers and Iryna Gurevych (UKP Lab) published Sentence-BERT (SBERT) in 2019. The solution: fine-tune BERT with a siamese network architecture using sentence pair similarity tasks, so the resulting embeddings are directly comparable by cosine similarity — without joint encoding.
Training data: sentence pairs with similarity labels
"A cat sits on a mat." ↔ "A kitten rests on a rug." → similar (1)
"A cat sits on a mat." ↔ "The market crashed today." → different (0)
BERT(sentence_A) → mean_pool(hidden states) → u ∈ ℝ^768
BERT(sentence_B) → mean_pool(hidden states) → v ∈ ℝ^768
Training loss (classification variant):
concat(u, v, |u−v|) → linear → softmax → similarity label
At inference:
similarity(A, B) = cosine(u, v) = (u·v) / (||u|| · ||v||)
Pre-compute all document embeddings once → store in vector DB
Query: embed once → single dot-product search over all documents
Speedup vs raw BERT cross-encoder: ~9,000× for 10,000 documents
How This Powers RAG (Day 29 Preview)
The most widely used sentence transformer families: all-MiniLM-L6-v2 (80 MB, 384-dim, fast, great for English), all-mpnet-base-v2 (slower, higher quality), paraphrase-multilingual-mpnet-base-v2 (50 languages). For cutting-edge quality: text-embedding-3-large (OpenAI, 3072-dim) and embed-english-v3.0 (Cohere). All available via the sentence-transformers Python library and HuggingFace Hub. For domain-specific work: BioBERT (medical), LegalBERT (legal), FinBERT (financial).
08 — Cosine Similarity: Measuring Meaning With Angles
Throughout this post we've talked about "similar vectors" without precisely defining similarity. The standard measure in embedding space is cosine similarity.
cos(u, v) = (u · v) / (||u|| × ||v||)
u · v = Σᵢ uᵢvᵢ ← dot product
||u|| = √(Σᵢ uᵢ²) ← L2 norm of u
||v|| = √(Σᵢ vᵢ²) ← L2 norm of v
Range and interpretation:
cos = +1.0 → identical direction → maximum similarity (e.g. "cat" vs "cat")
cos = 0.0 → perpendicular → no semantic relationship (e.g. "cat" vs "politics")
cos = −1.0 → opposite directions → antonyms (e.g. "love" vs "hate")
Why cosine and not Euclidean distance? Because cosine is magnitude-invariant. A 500-word document will have a larger embedding magnitude than a 5-word query (more vectors were pooled together), but their cosine similarity still correctly captures semantic closeness. Euclidean distance would be dominated by the length difference, not the meaning difference.
09 — Code: From Token IDs to Semantic Search
# ─────────────────────────────────────────────────────────────────────
# PART 1: The problem with raw token IDs
# ─────────────────────────────────────────────────────────────────────
import numpy as np
vocab = {"cat": 0, "dog": 1, "kitten": 2, "politics": 3, "democracy": 4}
V = len(vocab)
def one_hot(word_id, vocab_size):
v = np.zeros(vocab_size)
v[word_id] = 1.0
return v
def cosine(u, v):
return np.dot(u, v) / (np.linalg.norm(u) * np.linalg.norm(v) + 1e-10)
cat_oh = one_hot(vocab["cat"], V)
dog_oh = one_hot(vocab["dog"], V)
kitten_oh = one_hot(vocab["kitten"], V)
print(f"cos(cat, dog) = {cosine(cat_oh, dog_oh):.3f}") # 0.000
print(f"cos(cat, kitten) = {cosine(cat_oh, kitten_oh):.3f}") # 0.000
# All pairs are equally "different" — one-hot encodes nothing about meaning
# ─────────────────────────────────────────────────────────────────────
# PART 2: Loading GloVe embeddings and word analogy arithmetic
# ─────────────────────────────────────────────────────────────────────
# Download: https://nlp.stanford.edu/projects/glove/ (glove.6B.300d.txt)
def load_glove(path, max_words=50_000):
"""Load GloVe vectors into a dict {word: np.array}."""
embeddings = {}
with open(path, encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= max_words:
break
values = line.split()
word = values[0]
vector = np.array(values[1:], dtype=np.float32)
embeddings[word] = vector
return embeddings
def analogy(glove, a, b, c, topk=5):
"""Find word d such that a:b :: c:d (d ≈ b - a + c)."""
target = glove[b] - glove[a] + glove[c]
target = target / (np.linalg.norm(target) + 1e-10)
exclude = {a, b, c}
scores = [
(word, np.dot(target, vec / (np.linalg.norm(vec) + 1e-10)))
for word, vec in glove.items()
if word not in exclude
]
return sorted(scores, key=lambda x: -x[1])[:topk]
# glove = load_glove("glove.6B.300d.txt")
# print(analogy(glove, "man", "king", "woman")) # → queen ✓
# print(analogy(glove, "france", "paris", "italy")) # → rome ✓
# print(analogy(glove, "good", "better", "bad")) # → worse ✓
def nearest_neighbours(glove, word, topk=5):
"""Find the k most similar words to a query word."""
target = glove[word] / (np.linalg.norm(glove[word]) + 1e-10)
scores = [
(w, np.dot(target, v / (np.linalg.norm(v) + 1e-10)))
for w, v in glove.items()
if w != word
]
return sorted(scores, key=lambda x: -x[1])[:topk]
# nearest_neighbours(glove, "cat")
# → [("cats", 0.924), ("dog", 0.921), ("kitten", 0.908), ("feline", 0.881), ...]
# ─────────────────────────────────────────────────────────────────────
# PART 3: Sentence transformers for semantic search
# ─────────────────────────────────────────────────────────────────────
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2") # 80 MB, 384-dim, fast
# Knowledge base
documents = [
"The Eiffel Tower is located in Paris, France.",
"Photosynthesis converts sunlight into chemical energy in plants.",
"Python is a high-level programming language known for readability.",
"The mitochondria is the powerhouse of the cell.",
"Gradient descent is an optimisation algorithm used in machine learning.",
"The Great Wall of China stretches over 21,000 kilometres.",
"Transformers use self-attention to process sequences in parallel.",
]
# Offline indexing (done once)
doc_embeddings = model.encode(documents, normalize_embeddings=True)
# Shape: (7, 384) — one unit vector per document
# Runtime query
query = "What optimisation algorithm is used in deep learning?"
q_emb = model.encode([query], normalize_embeddings=True)[0]
# Cosine search (dot product of unit vectors = cosine similarity)
scores = doc_embeddings @ q_emb
ranked = np.argsort(scores)[::-1]
print(f"\nQuery: {query}\n")
for rank, idx in enumerate(ranked[:3]):
print(f" {rank+1}. [score={scores[idx]:.3f}] {documents[idx]}")
# Output:
# 1. [score=0.712] Gradient descent is an optimisation algorithm used in ML.
# 2. [score=0.534] Transformers use self-attention to process sequences.
# 3. [score=0.298] Python is a high-level programming language...
# ─────────────────────────────────────────────────────────────────────
# PART 4: FAISS for production-scale vector search
# ─────────────────────────────────────────────────────────────────────
# pip install faiss-cpu
import faiss
dim = 384 # matches all-MiniLM-L6-v2
# Flat index: exact inner product search (cosine on unit vectors)
index = faiss.IndexFlatIP(dim)
index.add(doc_embeddings.astype("float32"))
print(f"Index size: {index.ntotal} vectors")
# Search — returns top-3
D, I = index.search(q_emb.reshape(1, -1).astype("float32"), k=3)
print("\nFAISS results:")
for score, doc_idx in zip(D[0], I[0]):
print(f" [score={score:.3f}] {documents[doc_idx]}")
# For millions of documents: use IndexIVFFlat or IndexHNSWFlat
# for approximate nearest-neighbour search with sub-millisecond latency
# ─────────────────────────────────────────────────────────────────────
# PART 5: Visualising embedding clusters with t-SNE
# ─────────────────────────────────────────────────────────────────────
# pip install scikit-learn matplotlib
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
words = [
"cat", "dog", "kitten", "puppy", "feline", "canine", # animals (green)
"king", "queen", "prince", "princess", "royal", "throne", # royalty (yellow)
"python", "java", "code", "program", "function", "debug", # programming (blue)
]
word_vecs = model.encode(words, normalize_embeddings=True) # (18, 384)
# Reduce to 2D for visualisation
tsne = TSNE(n_components=2, perplexity=5, random_state=42)
coords = tsne.fit_transform(word_vecs)
colors = ["#3fb950"] * 6 + ["#e3b341"] * 6 + ["#58a6ff"] * 6
plt.figure(figsize=(10, 7))
for i, word in enumerate(words):
plt.scatter(coords[i, 0], coords[i, 1], color=colors[i], s=100, zorder=3)
plt.annotate(word, (coords[i, 0] + 0.3, coords[i, 1] + 0.3), fontsize=9)
plt.title("Word Embeddings in 2D (t-SNE projection)")
plt.axis("off")
plt.tight_layout()
plt.savefig("embeddings_tsne.png", dpi=150)
# Expected: three clean clusters — animals (green), royalty (yellow),
# programming (blue). Within each cluster: cat↔kitten closest, king↔queen
# adjacent, python↔java near each other.
10 — The Embedding Evolution: 1950s to 2026
11 — What Embeddings Still Can't Do
12 — A Thought to Sit With
Every word you have ever spoken lives — in some representation — inside these models.
Not as a sound, not as ink, not as memory.
As coordinates in a space of three hundred dimensions.
The question isn't whether that's impressive.
The question is: what gets lost in translation to geometry?
What parts of human language refuse to be a point in space?
We have the embeddings. Now we need a model that can actually read a sequence of them and generate language. Day 24: Attention & the Transformer Architecture — how "Attention Is All You Need" replaced recurrence entirely with a single elegant mechanism, and why that mechanism became the backbone of every language model from GPT to Gemini to Claude.
🔗 TensorFlow Embedding Projector — Interactive 3D visualisation
The single best tool for building embedding intuition. Type any word and watch its nearest neighbours appear in a rotatable 3D word cloud. Run PCA or t-SNE on Word2Vec, GloVe, or your own custom embeddings. Fifteen minutes here makes the "words close in meaning are close in space" concept completely concrete and unforgettable.
📄 Efficient Estimation of Word Representations in Vector Space — Mikolov et al. (2013)
The original Word2Vec paper. Surprisingly readable — the CBOW and skip-gram architectures are described in under four pages. The analogy evaluation results at the end remain striking even knowing the punchline. Essential reading for understanding why prediction-based embeddings work.
📄 GloVe: Global Vectors for Word Representation — Pennington et al. (2014)
Exceptionally well-written. The section explaining why co-occurrence ratios (not raw counts) encode meaning is one of the clearest pieces of mathematical intuition-building in NLP literature. The connection between the objective and the analogy arithmetic is made explicit and elegant.
📄 Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks — Reimers & Gurevych (2019)
The paper that made large-scale semantic search practical. The benchmark comparing BERT cross-encoding vs SBERT sentence encoding (65 hours vs 5 seconds for 10,000 pairs) makes the engineering motivation immediately obvious and the solution feel inevitable.
🔗 SBERT.net — Official sentence-transformers documentation
Model comparisons, benchmark leaderboards, training tutorials, and integration guides for FAISS, Pinecone, Weaviate, and other vector databases. The model selection guide alone is worth bookmarking before any RAG project.
- Token IDs encode nothing about meaning. Raw integers are arbitrary serial numbers. We need dense vectors where similar words have similar vectors — that's what embeddings are: lists of ~300 numbers representing a word's position in a learned semantic space.
- The distributional hypothesis is the foundation. "You shall know a word by the company it keeps." Words appearing in similar contexts have similar meanings. Measuring co-occurrence at scale extracts semantic structure automatically from raw text — no manual labelling needed.
- One-hot encoding fails for three reasons. All non-identical pairs have the same distance (no similarity signal), vectors have V dimensions (sparse and huge), and nothing learned about one word transfers to related words (no generalisation).
- Word2Vec learns by predicting context. Train a network to predict surrounding words from a centre word (skip-gram) or the centre from surrounding words (CBOW). The hidden layer weights that emerge encode what contexts each word appears in — similar contexts → similar weights → similar vectors. Negative sampling makes this practical at scale.
- GloVe factorises global co-occurrence statistics. Rather than local prediction windows, it minimises the difference between the dot product of two word vectors and the log of their co-occurrence count. Slightly different optimisation, very similar results — both produce analogy arithmetic.
- Static embeddings have one vector per word — polysemy invisible. "Bank" gets an averaged vector across all its senses. ELMo (BiLSTM) and BERT (transformer) solve this by computing context-dependent embeddings where the same word gets a different vector in each sentence.
- Sentence-BERT makes semantic search practical. Fine-tunes BERT with a siamese architecture so sentence embeddings are directly comparable by cosine similarity — no joint encoding needed. Pre-compute all document embeddings once, search in milliseconds. 9,000× faster than raw BERT cross-encoding.
- Cosine similarity measures angles, not lengths. Magnitude-invariant: a 500-word document and a 10-word query can be compared fairly even though they'd have very different vector magnitudes. Values in [−1, +1]: 1.0 = identical direction, 0.0 = orthogonal (unrelated), −1.0 = opposite direction (antonyms).
- Embeddings are the foundation of RAG. Every retrieval-augmented generation system relies on sentence embeddings to find relevant documents at query time. Offline index → online search → grounded generation. Understanding embeddings is understanding why RAG works and where it fails.
- Explain Word2Vec, GloVe, and sentence transformers. When would you use each?
Word2Vec and GloVe are both static word-level embeddings — one fixed vector per word, trained from corpus co-occurrence. Word2Vec learns by training a prediction task (skip-gram or CBOW); GloVe factorises the global co-occurrence matrix directly. Both produce vectors with analogy-arithmetic properties and similar downstream task quality. Use these for lightweight word-level representations: input features to a classical ML model, visualising word clusters, or quick semantic similarity between individual words where context is not critical. Their core limitation is polysemy: "bank" gets one vector regardless of whether it's financial or geographic. Sentence transformers solve a different problem: they encode entire sentences into single vectors directly comparable by cosine similarity. Use these for semantic search, RAG retrieval, duplicate detection, clustering documents, or any task where you need to compare the meaning of full sentences at scale. They require more compute than static embeddings but are the right tool for any retrieval-based application. - Why is cosine similarity preferred over Euclidean distance for comparing embeddings?
Cosine similarity measures the angle between two vectors, normalising out their magnitudes entirely. This is critical for embeddings because vector magnitude is not semantically meaningful — it's an artifact of aggregation. A 500-word document embedding will typically be larger in magnitude than a 10-word query embedding simply because more vectors were pooled, not because the document is "more" anything semantically. Euclidean distance is dominated by this magnitude difference and incorrectly ranks the longer text as "farther" from the short query. Cosine similarity removes this confound, comparing only the directional content — which is where the semantic information lives. Additionally, cosine produces normalised values in [−1, 1] that are easy to threshold across different queries, unlike Euclidean distances whose scale grows with embedding dimension. In practice, when using libraries like FAISS with L2-normalised embeddings, the inner product (dot product) equals cosine similarity, which is why normalised embeddings are the standard preprocessing step before vector database indexing. - Your semantic search system returns irrelevant results for the query "not recommended." Why might this happen and how would you fix it?
This is the negation problem. Most sentence embedding models will embed "not recommended" with a vector close to "recommended" — because the word "recommended" dominates the semantic signal, and "not" is a short function word that gets down-weighted. The model learned from paraphrase pairs where positive sentences are similar to other positive sentences; it was not specifically trained to represent negation as directional opposition. Mitigation strategies, in order of increasing effort: First, add a BM25/TF-IDF sparse retrieval layer in a hybrid search setup — keyword matching correctly distinguishes "recommended" from "not recommended" by exact string presence. Second, add a cross-encoder re-ranker (MS MARCO BERT) on the top-k retrieved candidates — cross-encoders read the query and document jointly and handle negation much better than bi-encoder semantic search. Third, fine-tune the embedding model on contrastive pairs that specifically include negation — ("not good", "bad") as a positive pair and ("not good", "good") as a hard negative. Production recommendation: hybrid search (dense + sparse) plus cross-encoder re-ranking is the standard architecture for precision-sensitive retrieval systems.