Your First AI Project

Lesson 11 of 14

Your first retrieval

Everything so far sends the model a document you chose. Retrieval is how it works with a collection — hundreds of documents, where the right two must be found first. This is the last piece, it is the foundation of most real AI products, and the honest version is about fifteen lines.

Why keyword search is not enough

The obvious approach is to find documents sharing words with the question. Here is that approach, and here is it failing.

python
import math
q = "how much did the repair cost"
docs = {
 "invoice":  "the invoice for the plumbing work came to 4200 rupees",
 "marketing":"the cost of the marketing campaign was discussed at length",
}
def bag(t): return {w: 1 for w in t.lower().split()}
def cos(a, b):
    dot = sum(a[w]*b.get(w, 0) for w in a)
    return dot / (math.sqrt(len(a)) * math.sqrt(len(b)))
qv = bag(q)
for k, d in docs.items():
    shared = sorted(set(qv) & set(bag(d)))
    print(f"{k:10} score={cos(qv, bag(d)):.3f}  shared={shared}")

Actual output:

text
invoice    score=0.136  shared=['the']
marketing  score=0.272  shared=['cost', 'the']

The wrong document wins. The invoice is the answer to the question and shares only the word "the". The marketing note shares "cost" and scores twice as high.

That is not a contrived example — it is the normal case. People ask about meaning, and the words they use are rarely the words in the answer.

Embeddings, in one paragraph

An embedding is a list of numbers representing a piece of text's meaning, produced by a model trained so that texts meaning similar things get similar numbers. "The invoice came to 4200 rupees" and "how much did the repair cost" land near each other despite sharing almost no words, because the model has learned that invoices and costs are related.

Retrieval: everything becomes a point, the question becomes a point, and you take the nearest
Retrieval: everything becomes a point, the question becomes a point, and you take the nearest

Real embeddings have hundreds or thousands of numbers. To see the mechanism, here are four hand-picked dimensions — invented for illustration, not produced by any model — standing for money, repair, marketing, time:

python
import math
V = {
 "query":     [0.9, 0.7, 0.0, 0.1],
 "invoice":   [0.8, 0.8, 0.0, 0.0],
 "marketing": [0.5, 0.0, 0.9, 0.2],
}
def cos(a, b):
    return sum(x*y for x, y in zip(a, b)) / (
        math.sqrt(sum(x*x for x in a)) * math.sqrt(sum(y*y for y in b)))
for k in ("invoice", "marketing"):
    print(f"{k:10} cos = {cos(V['query'], V[k]):.3f}")

Actual output:

text
invoice    cos = 0.988
marketing  cos = 0.392

Now the invoice wins decisively — 0.988 against 0.392, where word-matching gave 0.136 against 0.272. Same documents, same question, opposite answer, because the comparison is on meaning rather than spelling.

Cosine similarity is that one formula: the dot product divided by the lengths. It measures the angle between two lists of numbers, ignoring their magnitude, and it returns 1 for identical direction and 0 for unrelated. That is the only mathematics in this course.

Doing it for real

python
def embed(texts):
    r = client.embeddings.create(model="<your-provider's-embedding-model>", input=texts)
    return [d.embedding for d in r.data]

chunks = [p.read_text(encoding="utf-8") for p in paths]
vectors = embed(chunks)

def search(question, k=3):
    qv = embed([question])[0]
    scored = [(cos(qv, v), c) for v, c in zip(vectors, chunks)]
    scored.sort(reverse=True, key=lambda x: x[0])
    return scored[:k]

Embed everything once, embed the question, score, sort, take the top few. Check your provider's current docs for the embeddings endpoint and model name — the shape is the same everywhere, the names move.

Then the piece that makes it answer questions:

python
top = search("how much did the repair cost")
context = "\n\n---\n\n".join(text for _, text in top)

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=500,
    system="Answer using ONLY the excerpts provided. If they do not contain the "
           "answer, say so. Quote the excerpt you used.",
    messages=[{"role": "user",
               "content": f"Excerpts:\n\n{context}\n\nQuestion: how much did the repair cost?"}],
)
print(response.content[0].text)

That is RAG — retrieval-augmented generation — and you have now built one. Retrieve the relevant passages, put them in the prompt, and instruct the model to answer from those alone.

What is missing, honestly

This works and it is genuinely useful over a few hundred documents. Four things separate it from a production system.

Chunking. Whole files as single units is crude; real systems split into passages with overlap, because a 40-page document produces one averaged vector that matches nothing well.

A vector database. Comparing against every document works to a few thousand and then does not. Real systems use an index built for the job.

Reranking. A second, more careful model re-scores the top twenty to find the true top three.

Evaluation. A test set of questions with known correct sources, so you can tell whether a change improved retrieval or broke it. This is the one that separates people who build RAG from people who have built a RAG demo, and it is where Building RAG Systems starts being worth your time.

Do this today: run the two code blocks above with their real numbers. Watching word-matching pick the wrong document and meaning-matching pick the right one is the moment embeddings stop being a buzzword.

← Previous