AI Engineer Dojo Contents
AI Search / RAG Engineer · Chapter 4

Embeddings & Dense Retrieval

An embedding turns text into a point in space where "near" means "means the same thing." That one idea powers semantic search — and it fails in specific, predictable ways that you must be able to name before you reach for it. This chapter is the physics of dense retrieval.

A word-overlap retriever (Ch. 1) can't match "app keeps crashing" to a doc titled "resolving startup failures" — zero shared words. Embeddings fix exactly that. An embedding model maps a chunk of text to a vector of a few hundred to a few thousand numbers, trained so that texts with similar meaning land close together and unrelated texts land far apart. Retrieval becomes geometry: embed the query, find the nearest chunk vectors, return them.

Cosine similarity: the one operation

"Near" is measured by cosine similarity — the cosine of the angle between two vectors, ranging from 1 (identical direction, same meaning) through 0 (orthogonal, unrelated) to −1 (opposite). It ignores vector length and cares only about direction, which is what you want: the meaning of "20 requests per minute" shouldn't depend on how long the passage is. The whole retrieval step is: compute cosine between the query vector and every chunk vector, sort, take the top-k.

Cosine, in one line

cos(a, b) = dot(a, b) / (norm(a) * norm(b))     # 1 = same meaning, 0 = unrelated

You don't scan every vector: ANN

Cosine against all 10 million chunks per query is too slow. Production uses Approximate Nearest Neighbor (ANN) indexes — most commonly HNSW (Hierarchical Navigable Small World graphs) — which find the near-neighbors by walking a graph instead of scanning everything, trading a sliver of recall for 100–1000× speed. This is the machinery inside every vector database (pgvector, Pinecone, Qdrant, Weaviate). The engineering knob is the recall/latency trade: search more of the graph (higher ef_search) for better recall at higher latency, or less for speed. You rarely implement ANN; you tune it and know what it's approximating.

Where dense retrieval fails — name these cold

Embeddings are not magic; they have a signature blind spot that Chapter 5 exists to cover. Know exactly where they break:

FailureExampleWhy
Exact identifiers"XR-500" retrieves XR-400, XR-550The model embeds IDs by similarity of form; adjacent codes look "near" in meaning-space
Rare in-domain jargonLegal/medical/internal terms embed poorlyUnderrepresented in the embedder's training data
Negation & small edits"is compatible" vs "is not compatible" embed closeCosine captures topic, not always polarity
Domain mismatchGeneral embedder on code or financeWrong training distribution → weak separation
The model choice that actually matters

Pick the embedding model for your domain, and match query and document embeddings to the same model and the same instruction convention (many models want queries and documents embedded differently). A domain-appropriate embedder beats a "bigger" general one on your corpus. Never mix embedding models between index and query — the spaces aren't comparable.

Try it · ~30 min

Build vector search — and expose its blind spot

You'll embed a small corpus, retrieve by cosine, and watch dense search win on paraphrase and lose on an exact identifier — the failure that motivates hybrid (Ch. 5). To keep it free and reproducible, the "embeddings" are a small deterministic stand-in; the shape of the win and the loss is identical to a real model.

Setup: pip install numpy. (Optional: flip USE_REAL_API=True to embed with a real provider — the harness is unchanged.)

Step 1. Embed the corpus; write cosine similarity.

Step 2. Retrieve top-1 for a paraphrase query and an exact-ID query.

Step 3. Observe which one dense gets right and which it misses.

Your goal: see dense nail the paraphrase and miss the identifier — and be able to explain the miss in one sentence.

Starter code

import numpy as np, re

USE_REAL_API = False   # True -> swap embed() for a real embeddings API; harness unchanged

CORPUS = [
    "Resolving crash-on-startup and boot failures",     # 0
    "XR-500 firmware v4 release notes",                  # 1
    "XR-400 firmware v2 release notes",                  # 2
    "Streamline employee onboarding for new hires",      # 3
]

# Toy deterministic 'embedding': hashed bag-of-words into a fixed vector.
# Rewards concept overlap (stands in for semantics), and — like a real model —
# maps XR-500 and XR-400 to *near* vectors because their forms overlap.
DIM = 64
def embed(text):
    v = np.zeros(DIM)
    for w in re.findall(r"[a-z0-9]+", text.lower()):
        v[hash(w) % DIM] += 1.0
        # blur digits so "xr" dominates and 500/400 look similar (mimics ID blurring)
        if w.isalnum() and any(c.isdigit() for c in w):
            v[hash(re.sub(r'\d','#',w)) % DIM] += 1.5
    return v

def cosine(a, b):
    return float(a @ b) / ((np.linalg.norm(a)*np.linalg.norm(b)) or 1)

DOCVECS = [embed(d) for d in CORPUS]

def top1(query):
    qv = embed(query)
    sims = [cosine(qv, dv) for dv in DOCVECS]
    i = int(np.argmax(sims))
    return i, CORPUS[i], round(sims[i], 3)

# TODO: run top1() for a paraphrase query and an exact-ID query; print both.
Worked solution
print("paraphrase:", top1("my app won't boot"))       # -> doc 0, the startup-failures doc
print("exact ID:  ", top1("XR-500 firmware"))          # -> may return doc 2 (XR-400!), not doc 1

What you should see: the paraphrase query "my app won't boot" correctly returns the "crash-on-startup" doc despite sharing almost no words — that's the semantic win embeddings exist for. But "XR-500 firmware" can return the XR-400 doc, because the identifier's signal lives in the exact token 500, and embedding blurs it toward its neighbor 400. One sentence: dense retrieval matches meaning, and to it XR-500 and XR-400 mean nearly the same thing.

Why this is the whole setup for Ch. 5: you can't prompt or rerank your way out of a retrieval miss — if the XR-500 doc never enters the candidate set, it's gone. The fix isn't a bigger embedder; it's adding a lexical retriever that treats XR-500 as an exact token, then fusing the two. That's next chapter, and now you know precisely why it's needed.

Going further: swap embed() for a real embeddings API (set USE_REAL_API, call the provider, cache vectors) and re-run. A strong model narrows the paraphrase gap further but still confuses close identifiers — proving the blind spot is structural to dense retrieval, not a model-quality issue.

Case study

DevPortal: the error code embeddings couldn't see

DevPortal hosts search over a large API reference and went dense-only — one embedding model, cosine similarity, a clean vector index. On conceptual questions ("how do I paginate results?") it was excellent: recall@5 of 0.88. But developers mostly search by exact token — error codes, method names, config keys — and there it collapsed. A query for ERR_4021 returned pages about other 4000-series errors, because the embedding blurred the exact string toward its near-neighbors. On identifier queries, recall@5 was just 0.41.

The lesson isn't "embeddings are bad" — it's that dense retrieval has a structural blind spot: it captures topic and meaning but smears exact identifiers, where the signal lives in the token itself, not its neighborhood. Half of DevPortal's traffic lived in that blind spot. The fix isn't a better embedding model; it's adding a retriever that matches tokens directly — the subject of the next chapter.

Running case · Acme Vault

Acme hits the same wall: dense retrieval can't reliably surface the page for vault.export when a user types the exact method name — it returns neighboring methods instead. Acme flags it and moves on to lexical retrieval next.

Quiz · Chapter 4 — reasoning, not recall

  1. Cosine similarity ignores vector magnitude and uses only direction. Why is that the right choice for semantic search?
  2. Your dense retriever returns the XR-400 doc for a query about "XR-500." The root cause is:
  3. Why do production systems use HNSW/ANN instead of exact nearest-neighbor scan?
  4. You embed documents with model A and queries with model B. What's wrong?
  5. "is compatible with v4" and "is NOT compatible with v4" embed very close together. This illustrates that cosine similarity:

← Back Continue →

The AI Search / RAG Engineer · AI Engineer Dojo · aiengineerdojo.com