AI Engineer Dojo Contents
Chapter 4

Embeddings & Dense Retrieval

An embedding turns text into a point in space where nearby points mean similar things. That idea powers semantic search, and it fails in specific, predictable ways you should be able to name before relying on it.

Case study

DevPortal can't find the error code

DevPortal runs search over a large API reference using embeddings alone. On conceptual questions like "how do I paginate results?" it did well, with recall@5 of 0.88. Developers mostly search for exact tokens, though: error codes, method names, config keys. A search for ERR_4021 returned pages about other 4000-series errors. On identifier queries, recall@5 was 0.41.

Half of DevPortal's traffic fell into that gap. A better embedding model wouldn't close it, since the problem is how embeddings treat exact strings. The fix, adding keyword search alongside, is the next chapter.

The word-overlap retriever from Chapter 1 can't connect "app keeps crashing" with a page titled "resolving startup failures," because they share no words. Embeddings can. An embedding model maps a piece of text to a list of a few hundred to a few thousand numbers, trained so that texts with similar meanings end up close together. Retrieval becomes geometry: embed the query, find the nearest chunk vectors and return them.

Cosine similarity

Closeness is usually measured with cosine similarity, the cosine of the angle between two vectors. It runs from 1 (same direction, same meaning) through 0 (unrelated) to −1 (opposite). It ignores the length of the vectors, which is what you want, because a passage's meaning shouldn't depend on how long it is.

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

Approximate nearest neighbors

Comparing a query with ten million chunks one by one is too slow. Production systems use approximate nearest neighbor (ANN) indexes, most often HNSW graphs, which find close vectors by walking a graph instead of checking everything. You give up a little recall for a speedup of 100 to 1,000 times. Vector databases such as pgvector, Pinecone, Qdrant and Weaviate all work this way. You'll rarely build ANN yourself, but you will tune it: searching more of the graph (a higher ef_search) improves recall and adds latency.

Where dense retrieval fails

FailureExampleWhy
Exact identifiers"XR-500" returns XR-400 and XR-550Similar-looking codes land close together
Rare jargonLegal, medical or internal termsToo rare in the embedding model's training data
Negation"is compatible" and "is not compatible" end up closeCosine captures topic better than polarity
Domain mismatchA general model on code or financeTrained on different kinds of text
Choosing a model

Pick an embedding model that suits your domain, and use the same model, with the same query and document conventions, for both indexing and search. Vectors from two different models live in different spaces, so comparing them means nothing.

Try it · ~30 minFree · no API key

Build vector search and find its blind spot

Embed a small corpus, retrieve by cosine similarity, and watch dense search get a paraphrase right and an exact identifier wrong. To keep it free and repeatable, the embeddings are a small deterministic stand-in (pip install numpy); the pattern matches what a real model does. 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

"My app won't boot" correctly finds the crash-on-startup page even though the two share almost no words. That's what embeddings are for. "XR-500 firmware" can return the XR-400 page, though, because the useful signal is the exact token 500 and the embedding treats it as close to 400. In one sentence: dense retrieval matches meaning, and XR-500 and XR-400 mean nearly the same thing to it.

Swap in a real embedding API if you'd like to confirm it. A strong model narrows the paraphrase gap further and still mixes up close identifiers, so the weakness isn't about model quality.

Running case · Acme Vault

Acme runs into the same wall. When a user types the exact method name vault.export, dense search returns neighboring methods. Acme adds it to the list for the next chapter.

If you're starting out

If vectors and cosine similarity are new, spend an hour with a short linear algebra refresher on dot products and norms. The math here stops at that level.

Knowing where embeddings fail is more impressive in an interview than knowing how they're trained. Being able to list identifiers, rare jargon and negation, each with an example, shows you've used them on real data.

If you run the team

Semantic search makes a product feel smart, and it has one blind spot you should know about: exact terms such as error codes, SKUs, ticket IDs and precise names. If your users search that way, "just embeddings" will miss a whole class of questions.

If someone proposes a bigger embedding model to fix failing part-number searches, be skeptical. Ask whether plain keyword matching would find those items. It usually would, and adding it is smaller and cheaper than a new model.

Quiz · Chapter 4

  1. Cosine similarity ignores vector length. Why is that right for semantic search?
  2. Your dense retriever returns the XR-400 page for an XR-500 query. What's the root cause?
  3. Why do production systems use ANN indexes instead of an exact 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 shows that cosine similarity:
Back Continue

The AI Search / RAG Engineer · aiengineerdojo.com