// lesson: embeddings

Embeddings and Similarity

An embedding turns text into a vector โ€” a plain list of floats, a few hundred long โ€” with one magic property: texts that mean similar things get vectors that point in similar directions. "How do I reset my password?" and "steps to recover account access" share almost no words, but their embeddings are near-parallel. That property is the entire trick behind semantic search; everything else in RAG is plumbing around it.

Add local embeddings to rag.py with sentence-transformers:

from sentence_transformers import SentenceTransformer

_model = SentenceTransformer("all-MiniLM-L6-v2")


def embed_texts(texts):
    return _model.encode(texts, normalize_embeddings=True).tolist()

all-MiniLM-L6-v2 maps each string to 384 floats โ€” that's the "few hundred long" vector made concrete. It also has an input limit: text past 256 word pieces gets truncated, and a truncated chunk embeds only its first part. This is why the 800-character default from the chunking lesson isn't arbitrary โ€” it comfortably fits under that limit with room to spare, so every chunk gets embedded in full instead of silently losing its tail. Swap in a model with a shorter limit (or hand it much bigger chunks) and this stops being true โ€” worth checking whenever you change either knob.

Prefer a hosted embeddings API? Same shape, swap the body (and pip install openai, export OPENAI_API_KEY=...):

from openai import OpenAI

_openai = OpenAI()  # reads OPENAI_API_KEY

def embed_texts(texts):
    resp = _openai.embeddings.create(model="text-embedding-3-small", input=texts)
    return [item.embedding for item in resp.data]

Either way, embed_texts takes a list of strings and returns a list of vectors โ€” the rest of the course doesn't care which one you picked. That's the same injectable-callable seam you used for the LLM client in the CLI chatbot course, and it's what will keep the graded challenges offline.

How do we compare two vectors? Cosine similarity: the cosine of the angle between them.

cos(a, b) = (a . b) / (|a| * |b|)

Dot product over the product of lengths. It ranges from 1.0 (same direction โ€” same meaning), through 0.0 (unrelated), to -1.0 (opposite). Dividing by the lengths makes it care only about direction, so a long document and a three-word query can still match. Add it to rag.py:

import math


def cosine_similarity(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(y * y for y in b))
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return dot / (norm_a * norm_b)

(The zero-vector guard avoids a ZeroDivisionError on degenerate input; by convention a zero vector is similar to nothing.)

Now the payoff โ€” semantic search over your own corpus, brute force:

if __name__ == "__main__":
    corpus = build_corpus(load_documents("docs"))
    vectors = embed_texts([chunk for _, chunk in corpus])

    query = "how do I restore a backup"   # ask something YOUR docs answer
    qvec = embed_texts([query])[0]

    scored = [
        (cosine_similarity(qvec, vec), chunk_id, chunk)
        for (chunk_id, chunk), vec in zip(corpus, vectors)
    ]
    scored.sort(reverse=True)
    for score, chunk_id, chunk in scored[:5]:
        print(f"{score:.3f}  {chunk_id}  {chunk[:70]!r}")

Run it a few times with different queries. Spend real time here โ€” this is the eyeball test every RAG engineer runs constantly. Do the top hits actually answer the question? Try a query using words that appear nowhere in your docs but mean the same thing as something that does; watch it match anyway. That's embeddings earning their keep.

One note before the challenge: normalize_embeddings=True asked the model for unit-length vectors, which makes cosine similarity collapse to a plain dot product. Vector databases lean on that constantly. Your graded function takes arbitrary vectors, so it does the full formula โ€” the one line every vector database on earth evaluates a billion times a day. No stub needed; it's pure arithmetic.

โ€บ Cosine Similarity

15 pts

Implement cosine_similarity(a, b) for two equal-length lists of numbers:

  • Return the dot product of a and b divided by the product of their Euclidean lengths.
  • If either vector has length zero (all zeros), return 0.0.
  • Plain Python lists of ints or floats must work โ€” no numpy.

Log in to submit a solution and earn points.