Generative AI Engineer Series · Article 04 · Phase 1: Foundations

Embeddings from Scratch

How language models turn meaning into geometry, why "puppy" ends up near "dog" without anyone saying so, and how that one idea powers semantic search, RAG, and everything we build in Phases 3 and 5.

all-MiniLM-L6-v2
384numbers per sentence, from a free 90 MB model
Zero shared words
0.547 vs 0.048"dog chased cat" vs "puppy ran after kitten", and vs "stock market crashed"
Vector arithmetic
king − man + womannearest word: queen (in real GloVe vectors)
…and closer to home
Delhi − India + Japannearest word: Tokyo

§ 01 · HookThe Magic of Semantic Similarity

Run this code:

Pythonrun this first
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

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

sentences = [
    "The dog chased the cat",
    "A puppy ran after a kitten",
    "The stock market crashed today",
]

emb = model.encode(sentences)
sim = cosine_similarity(emb)

print(f"Sentences 1 & 2: {sim[0][1]:.3f}")   # 0.547
print(f"Sentences 1 & 3: {sim[0][2]:.3f}")   # 0.048

Sentences 1 and 2 score 0.547. Sentences 1 and 3 score 0.048, more than ten times lower and close to zero. Here's the part that should make you stop: sentences 1 and 2 share no words at all. "Dog" and "puppy" are different words. "Chased" and "ran after" are different phrases. "Cat" and "kitten" are different tokens. Yet the model clearly places them together, and places the stock market somewhere else entirely.

The mechanism behind this is embeddings: a way of turning meaning into geometry. It's not a niche academic idea. Embeddings sit underneath semantic search engines, every RAG system we'll build in Phase 5, and every vector database in Phase 3. They're also how the Transformer from Article #02 represents every token internally.

About the numbers in this article

Every similarity score here was produced by actually running all-MiniLM-L6-v2, and the word-arithmetic examples by running real GloVe word vectors. Your results may differ in the third decimal place across library versions and hardware, but the rankings won't change. And remember that the absolute size of a score depends on the model: another model might give the same pair 0.8. What matters is which pairs score higher than which.

📹 YouTube note Run this code live and print the scores. Pause on them. Say: "The model has never been told these sentences are similar. It worked it out from reading a huge amount of text. How?" Let the question sit before moving on.

§ 02 · The Core IdeaMeaning as Geometry

An embedding is a dense vector (Article #02, §03: a list of decimal numbers where nearly every position carries information) that represents the meaning of a piece of text. Typical sizes run from a few hundred to a few thousand numbers. Every word, sentence, or document can be mapped to a point in this high-dimensional space.

Nobody can picture 384 dimensions, and that's fine; the researchers who build these models can't either (notice that §07 flattens everything to 2D just to put it on a screen). What matters is that distance and direction work the same way in 384 dimensions as on a sheet of paper. One helpful way to think about it: each dimension is roughly one more question the model has learned to answer about the text. Is it formal or casual? Technical or everyday? About a place or a person? Positive or negative? There are hundreds of these "questions" layered together, each adding one coordinate, and in reality no single dimension lines up neatly with one human idea the way "formality" does in this simplified picture.

Similar meanings land at nearby points. Distance in this space is semantic distance.

Here's what that looks like in real word vectors (GloVe, 100 dimensions):

  • "king" and "queen" score 0.75. "king" and "carrot" score 0.09.
  • "Paris" relates to "France" the way "Berlin" relates to "Germany": the country-to-capital relationship is a consistent direction in space.
  • The most famous consequence: king − man + woman ≈ queen. You can do arithmetic on meaning.
Pythonarithmetic on meaning (GloVe vectors via gensim)
import gensim.downloader as api
kv = api.load("glove-wiki-gigaword-100")   # ~130 MB, downloaded once

print(kv.most_similar(positive=["king", "woman"], negative=["man"], topn=1))
# [('queen', 0.7698...)]

print(kv.most_similar(positive=["paris", "germany"], negative=["france"], topn=1))
# [('berlin', 0.8846...)]

print(kv.most_similar(positive=["delhi", "japan"], negative=["india"], topn=1))
# [('tokyo', 0.8396...)]

This isn't hand-waving. Subtract one vector from another, add a third, and the nearest word in the whole vocabulary really is "queen", or "berlin", or "tokyo". Direction in space encodes relationships.

Figure 1Meaning as geometry, in real word vectors
kingqueenmanwomanprinceprincesscatdogpuppykittenfrancegermanyindiajapanparisberlindelhitokyoClusterspeople & royaltyanimalscountriescapitalscountry → capitalmale → femaleIn the full 100 dimensions,paris − france andberlin − germany point thesame way (cosine 0.78).A random pair scores −0.08.Flattening to 2D bendsthe arrows a little.Real GloVe word vectors (100 dimensions), projected to 2D with PCA.

← swipe to see the whole figure →

Related words cluster together, and the country → capital arrows all point roughly the same way. That shared direction is what makes the arithmetic above work.

Why engineers care

Embeddings turn a fuzzy problem, "find things that mean the same as this query", into a precise geometric one: find the nearest points in a vector space. No keyword matching, no SQL LIKE, no hand-written synonym lists. Just geometry.

That's exactly what a vector database stores (Phase 3) and what RAG uses to fetch relevant context (Phase 5). Remember the open-book exam from Article #01, §15? Embeddings are how the system finds the right pages of the book.

📹 YouTube note Open the TensorFlow Embedding Projector at projector.tensorflow.org. Zoom into the cluster of animals, then zoom out to the countries. Say: "Nobody told the model that 'dog' and 'cat' are related. It learned it purely from which words appear near each other in text."

§ 03 · The FoundationWord2Vec: Where Modern Embeddings Began

In 2013, researchers at Google published Word2Vec, the method that showed word embeddings could be learned at scale. You don't need to implement it, but its core idea answers a question intuition alone can't: why do embeddings work at all?

The distributional hypothesis

Word2Vec rests on an old idea from linguistics, summed up by J. R. Firth in 1957: "You shall know a word by the company it keeps." Words that appear in similar contexts tend to have similar meanings.

Take two sentences: "The cat sat on the mat." and "The dog sat on the rug." "cat" and "dog" appear in almost identical surroundings: both come after "the" and before "sat". After millions of examples like this, the method concludes that these words play the same role, so their vectors should be close. Nobody labelled either one as an "animal". The signal came entirely from context. It's the same trick as Article #01's self-labelling training data: the text supplies its own lessons.

How it learns: skip-gram training

Word2Vec's skip-gram version trains a shallow neural network (a single hidden layer, the opposite of the many-layered networks in Article #01, §05) on a simple task: given a centre word, predict the words around it. The interesting part is what's left over afterwards. The network's weight matrix has one row per word, and after training, those rows are the embeddings. The prediction task is just a way to make useful rows appear.

Figure 2Skip-gram: how embeddings fall out of a prediction task
1 · SLIDE A WINDOW OVER REAL TEXT (±2 WORDS)thecatsatonthematcentre word "cat"; its neighbours are the answers2 · TURN IT INTO PREDICTION PAIRScatthecatsatcaton3 · A SHALLOW NETWORK LEARNS TO PREDICT THEMinput: “cat”weight matrix Wone row per wordguess the neighbourscompare, adjust W4 · AFTER MILLIONS OF SENTENCES, THE ROWS OF W ARE THE EMBEDDINGScat+0.62-0.18+0.44+0.09…dog+0.59-0.21+0.40+0.12…carrot-0.30+0.52+0.05-0.41…"cat" and "dog" keep turning upnext to the same words, sotheir rows end up alike.(numbers are illustrative)

← swipe to see the whole figure →

The network is a means to an end. What we keep is the weight matrix, whose rows become the word vectors.

The limitation that leads to the next step

Word2Vec gives each word one fixed vector. "bank" gets a single embedding forever, whether it appears in "river bank" or "savings bank". This is the polysemy problem (polysemy means one word having several meanings): one vector tries to average all of them. It's the same "bank of the river" puzzle from Article #01, §10, and it's exactly what the Transformer's attention solves.

§ 04 · Scaling UpFrom Word to Sentence Embeddings

Word2Vec gives you one vector per word. But RAG pipelines, semantic search, and document retrieval need one vector per sentence, paragraph, or document.

The naive approach: average the word vectors

Take the vector for every word in a sentence and average them. It works for simple cases, but it has a fatal flaw: word order disappears. "Dog bites man" and "man bites dog" contain the same three words, so they produce the same average. Article #02 showed the same order problem for Transformers, and solved it with position information.

The Transformer solution: contextual embeddings

A Transformer encoder (Article #02, §06) reads the whole sentence at once, with attention. So the same word gets a different vector depending on its neighbours: "bank" next to "river" comes out differently from "bank" next to "account". To squeeze the sentence into one vector, you mean-pool: average all the token vectors number by number, so dozens of token vectors collapse into one sentence vector that blends a bit of everything. Because each token vector already reflects its context, the average keeps far more meaning than Word2Vec averaging did.

Sentence-BERT (2019)

BERT on its own wasn't very good at sentence-level similarity. In 2019, Nils Reimers and Iryna Gurevych fine-tuned it specifically for that job using a siamese network: two identical copies of the same network, sharing the same weights, each reading one sentence of a pair. Think of identical twins who each read a different book and then compare notes on how similar the books felt. Trained on many sentence pairs, the result was a 768-number vector per sentence that captured meaning well across topics. all-MiniLM-L6-v2, the model we use in this article, comes from the same family: a smaller, faster model trained with the same sentence-transformers approach.

Figure 3From tokens to one sentence vector
Transformer route (what sentence-transformers does)IsatbytheriverbankTransformer encoder (attention: every token sees every other)"bank" near "river"mean pooling (average, number by number)one 384-number vectorWord2Vec averaging"dog bites man""man bites dog"→ identical vectorsSame words, sameaverage. Order andcontext are lost, and"bank" always getsthe same vector.With a transformer, each token's vector already reflects its neighbours before pooling. "bank" next to "river" and "bank" next to"account" come out as different vectors, so the pooled sentence vector keeps that difference.all-MiniLM-L6-v2: 6 transformer layers, 384 numbers per token, mean pooling, then scaled to length 1.

← swipe to see the whole figure →

The encoder produces one context-aware vector per token; mean pooling collapses them into one. Word2Vec averaging, on the right, can't tell word orders apart.

§ 05 · Hands-on CodeGenerating Embeddings with sentence-transformers

Install the library and open a Jupyter notebook. We'll build semantic search from the ground up, with no database and no index, just vectors and cosine similarity.

Bashsetup
$ pip install sentence-transformers scikit-learn

Encoding, and what comes out

Pythonexperiment 1 · basic encoding
from sentence_transformers import SentenceTransformer
import numpy as np

# Small, fast, and good. Downloads about 90 MB the first time, then it's cached.
model = SentenceTransformer("all-MiniLM-L6-v2")

sentences = [
    "Machine learning is a subset of artificial intelligence",
    "Deep learning uses neural networks with many layers",
    "I love eating pizza on Friday nights",
    "The best pasta recipe uses fresh tomatoes",
]

embeddings = model.encode(sentences)
print(embeddings.shape)
# (4, 384)   4 sentences, 384 numbers each

print(embeddings[0][:6].round(3))
# [-0.046 -0.004  0.07   0.036  0.049 -0.03 ]

print(np.linalg.norm(embeddings[0]).round(3))
# 1.0   this model already scales every vector to length 1

Measuring similarity: cosine similarity

Cosine similarity measures the angle between two vectors and ignores how long they are; only direction matters. It's built on the dot product you met in Article #02, §04. Two vectors pointing the same way score 1.0. Two at a right angle score 0. Two pointing in opposite directions score −1.0.

Picture two people in a room, each pointing at something. Cosine similarity only asks "are they pointing the same way?" It doesn't care if one has a longer arm. That's why it's the standard way to compare embeddings: two sentences can produce vectors of different lengths for incidental reasons, but if they mean the same thing, they point the same way.

Figure 4Cosine similarity: direction, not length
Same directioncos = 1.0same meaningRight anglecos = 0unrelatedOppositecos = −1.0oppositedifferent lengths,same direction: still ≈1Real scoresall-MiniLM-L6-v2dog chased cat ↔puppy ran after kitten0.547ML is part of AI ↔deep learning, networks0.415pizza on Fridays ↔best pasta recipe0.273dog chased cat ↔stock market crashed0.048

← swipe to see the whole figure →

Left: the geometry. Right: real scores from all-MiniLM-L6-v2 for pairs used in this article.
Pythonexperiment 2 · similarity between every pair
from sklearn.metrics.pairwise import cosine_similarity

sim = cosine_similarity(embeddings)

for i, s1 in enumerate(sentences):
    for j, s2 in enumerate(sentences):
        if i < j:
            print(f"{sim[i][j]:.3f} | {s1[:38]:<38} ↔ {s2[:38]}")

# 0.415 | Machine learning is a subset of artifi ↔ Deep learning uses neural networks wit
# 0.016 | Machine learning is a subset of artifi ↔ I love eating pizza on Friday nights
# 0.033 | Machine learning is a subset of artifi ↔ The best pasta recipe uses fresh tomat
# 0.021 | Deep learning uses neural networks wit ↔ I love eating pizza on Friday nights
# 0.121 | Deep learning uses neural networks wit ↔ The best pasta recipe uses fresh tomat
# 0.273 | I love eating pizza on Friday nights   ↔ The best pasta recipe uses fresh tomat

The two AI sentences score 0.415, the two food sentences 0.273, and every tech-versus-food pair lands between 0.016 and 0.121. The within-topic pairs clearly win, even though none of those scores is anywhere near 1.0. That's normal: with this model, closely related sentences often score between 0.3 and 0.7, and unrelated ones near zero. Compare scores with each other, not against a fixed idea of what "similar" should look like.

Semantic search from scratch

This is where it all comes together: a working semantic search engine in a dozen lines. The same pattern is the retrieval half of every RAG system.

Pythonexperiment 3 · semantic search (minimal RAG retrieval)
query = "how do neural networks learn?"
query_emb = model.encode([query])

documents = [
    "Backpropagation is the algorithm that trains neural networks",
    "Paris is the capital of France",
    "Gradient descent minimises the loss function during training",
    "The Eiffel Tower was built in 1889",
    "Weights in a neural network are updated using gradients",
    "Sourdough bread requires a live starter culture",
]

doc_embs = model.encode(documents)
scores = cosine_similarity(query_emb, doc_embs)[0]

for score, doc in sorted(zip(scores, documents), reverse=True):
    print(f"{score:.3f}  {doc}")

# 0.672  Backpropagation is the algorithm that trains neural networks
# 0.463  Weights in a neural network are updated using gradients
# 0.377  Gradient descent minimises the loss function during training
# 0.088  Paris is the capital of France
# 0.032  Sourdough bread requires a live starter culture
# 0.006  The Eiffel Tower was built in 1889

The top three results are all about how networks train, which is exactly right. And look at the gap: the third-best relevant document (0.377) scores more than four times higher than the best irrelevant one (0.088). The word "learn" doesn't appear in any of them. The ranking comes from meaning, through geometry.

Figure 5Same documents, two different queries
QUERY"how do neural networks learn?"1. Backpropagation … trains neural networks0.6722. Weights … updated using gradients0.4633. Gradient descent minimises the loss …0.3774. Paris is the capital of France0.0885. Sourdough bread requires … starter0.0326. The Eiffel Tower was built in 18890.006QUERY"how do I make sourdough?"1. Sourdough bread requires … starter0.5302. Backpropagation … trains neural networks0.1553. Paris is the capital of France0.0374. Gradient descent minimises the loss …0.0095. The Eiffel Tower was built in 18890.0016. Weights … updated using gradients-0.002Real cosine scores from all-MiniLM-L6-v2. No query word appears in the top result on the left.

← swipe to see the whole figure →

Change the query to "how do I make sourdough?" and the ranking flips: the bread document jumps to the top and the machine-learning documents fall away.
This is RAG retrieval

What you just built is the retrieval half of a RAG system. In Phase 5 we swap the in-memory array for a vector database, scale to millions of documents, and hand the top results to an LLM as context. But the core mechanic is exactly this: embed the query, embed the documents, rank by cosine similarity, take the top matches.

Batch encoding for large datasets

Pythonexperiment 4 · scale up
large_corpus = [f"Document number {i}" for i in range(10_000)]

embeddings = model.encode(
    large_corpus,
    batch_size=64,              # encode 64 texts at a time
    show_progress_bar=True,
    normalize_embeddings=True,  # length-1 vectors: cosine similarity = plain dot product
)
print(embeddings.shape)
# (10000, 384)

normalize_embeddings=True rescales every vector to length 1 before you store it. Once every vector has length 1, the cosine formula (which normally divides by both lengths) reduces to a plain dot product, the cheapest comparison there is. This particular model already does that for you, as experiment 1 showed, but many models don't, so it's a good habit. It's the same optimisation the vector databases in Phase 3 rely on.

📹 YouTube note Run the semantic search live. Then change the query to "how do I make sourdough?" and show the ranking flip, as in Figure 5. Say: "No keywords, no rules, no SQL. Just geometry."

§ 06 · Production OptionGenerating Embeddings with the OpenAI API

When you move from prototype to production, or when retrieval quality really matters, you may switch to a hosted embedding API. OpenAI's is a common choice, and the interface is simple.

Bashsetup
$ pip install openai
PythonOpenAI embeddings API
from openai import OpenAI

client = OpenAI()  # reads OPENAI_API_KEY from your environment

response = client.embeddings.create(
    model="text-embedding-3-small",
    input=[
        "What is retrieval augmented generation?",
        "How does RAG work in LangChain?",
    ],
)

vectors = [item.embedding for item in response.data]
print(len(vectors[0]))                # 1536
print(response.usage.total_tokens)      # 15   (billed by the token, as in Article #03)
A useful, easy-to-miss feature

OpenAI's text-embedding-3 models were trained with a technique called Matryoshka Representation Learning, named after Russian nesting dolls, that lets you shorten the vector and keep most of its usefulness. Pass dimensions=256 to client.embeddings.create() and you get 256 numbers instead of 1,536, at a fraction of the storage and search cost, with only a modest drop in quality. That matters when §08's "more dimensions cost more" trade-off hits your storage budget.

all-MiniLM-L6-v2text-embedding-3-small
Dimensions3841,536 (or fewer, via dimensions)
CostFree, runs locallyAbout $0.02 per million tokens (check current pricing)
SpeedFast on a CPU, faster on a GPUNetwork round trip on every call
PrivacyText never leaves your machineText is sent to OpenAI
QualityGood for most prototypesStronger retrieval, longer inputs (up to 8,191 tokens)
Best forLearning, prototyping, private dataProduction systems where quality matters most
Practical guidance

Develop locally with sentence-transformers: fast iteration, no cost, and your data stays private. Consider a hosted model like text-embedding-3-small when you need the extra retrieval quality in production. For sensitive content (healthcare, legal, internal documents), prefer a model you host yourself, even if a hosted one scores a little higher.

§ 07 · Making It VisibleVisualising Embedding Space

384-dimensional vectors are invisible. PCA (Principal Component Analysis) finds the few directions along which the data varies most and projects everything onto them, so 384 dimensions can be flattened to the 2 you can plot. Some structure is lost, but a surprising amount survives.

Bashsetup
$ pip install matplotlib scikit-learn
PythonPCA cluster plot
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sentence_transformers import SentenceTransformer

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

sentences = [
    "Machine learning models learn from data",          # Tech
    "Neural networks are inspired by the human brain",  # Tech
    "Python is the most popular language for AI",       # Tech
    "Pizza is a beloved Italian dish",                  # Food
    "Pasta with tomato sauce is a classic meal",        # Food
    "Risotto requires constant stirring",               # Food
    "Football is the most popular sport globally",      # Sport
    "A cricket match can last five days",               # Sport
    "Swimming is an excellent full-body workout",       # Sport
]
colors = ["#0E6E62"] * 3 + ["#8A5E0F"] * 3 + ["#B23A26"] * 3   # tech, food, sport

embeddings = model.encode(sentences)
reduced = PCA(n_components=2).fit_transform(embeddings)

plt.figure(figsize=(9, 6))
for i, (x, y) in enumerate(reduced):
    plt.scatter(x, y, color=colors[i], s=130, edgecolors="white", linewidths=1.5)
    plt.annotate(sentences[i][:28], (x, y), fontsize=8, ha="center",
                 xytext=(0, 7), textcoords="offset points")
plt.axis("off")
plt.tight_layout()
plt.savefig("embedding_clusters.png", dpi=150, bbox_inches="tight")
plt.show()
Figure 6What you'll see: three topic clusters
Machine learning models lear…Neural networks are inspired…Python is the most popular l…Pizza is a beloved Italian d…Pasta with tomato sauce is a…Risotto requires constant st…Football is the most popular…A cricket match can last fiv…Swimming is an excellent ful…TechFoodSportReal PCA output for the nine sentences in §07. Average similarity within a topic: 0.27; across topics: 0.06.

← swipe to see the whole figure →

Real output. Nobody told the model which sentences were about tech, food, or sport, yet they fall into three groups. The food sentences sit almost on top of each other.

This is what §02 promised: three clean clusters, without any topic labels. For bigger or messier datasets, try t-SNE (t-distributed Stochastic Neighbor Embedding). It's slower and its axes mean nothing on their own, but it's often better at pulling separate groups visibly apart. One catch: its perplexity setting must be smaller than the number of points, so for our nine sentences it has to be tiny.

Pythonswap PCA for t-SNE
from sklearn.manifold import TSNE

# perplexity must be less than the number of points. Use ~3 for 9 sentences;
# values between 5 and 50 are typical once you have hundreds of points.
reduced = TSNE(n_components=2, perplexity=3, random_state=42).fit_transform(embeddings)
📹 YouTube note Generate the PCA plot live. Say: "Every dot is a sentence. Nearby dots mean similar things. The model worked out that pizza, pasta, and risotto belong together purely from reading text."

§ 08 · Engineering DecisionsChoosing the Right Embedding Model

Your choice of embedding model directly affects retrieval quality, speed, cost, and storage. Here's what to weigh:

  • Dimensions. More numbers can capture more nuance but cost more to store and search: 384, 768, 1,024, 1,536, 3,072.
  • Maximum input length. all-MiniLM-L6-v2 reads at most 256 tokens; all-mpnet-base-v2 reads 384. Anything longer is silently cut off by default, which is one reason RAG systems split documents into chunks (Phase 5).
  • Languages. English-only models produce poor vectors for Hindi, Tamil, or Chinese. Article #03 showed how much older tokenisers struggle with Indian scripts, and embedding models inherit their tokeniser's strengths and weaknesses. Use a multilingual model for non-English text, and test it on your own users' languages.
  • Task type. Retrieval compares a short query with a long document (asymmetric). Similarity compares two texts of a similar kind (symmetric). A model trained for one can do noticeably worse at the other, because the shapes it learned to compare don't match what you give it. The open-source BGE family (from the Beijing Academy of Artificial Intelligence) is strong at retrieval; classic SBERT models are strong at similarity.
  • Benchmarks. The MTEB leaderboard (huggingface.co/spaces/mteb/leaderboard) ranks embedding models across many tasks and languages. The original MTEB benchmark covered 56 datasets, and it has grown a lot since. Check it before picking a production model, then test the top few on your data, because leaderboard rank doesn't always carry over.
ModelDimsMax tokensCostGood for
all-MiniLM-L6-v2384256FreeLearning, prototyping, offline
all-mpnet-base-v2768384FreeBetter English quality, longer inputs
BAAI/bge-large-en-v1.51,024512FreeOpen-source retrieval in production
paraphrase-multilingual-mpnet-base-v2768128Free50+ languages
text-embedding-3-small1,5368,191~$0.02 / 1M tokensHosted production retrieval
text-embedding-3-large3,0728,191~$0.13 / 1M tokensQuality-critical and multilingual work
Decision guide

Learning or prototyping: all-MiniLM-L6-v2. Fast, free, no network calls.
Production retrieval: BAAI/bge-large-en-v1.5 (open source) or text-embedding-3-small (hosted).
Multilingual, including Indian languages: a multilingual model, tested on your own text.
Sensitive data: a model you host yourself.

§ 09 · SummaryWhat You Now Understand

Eight things to take with you

  • An embedding is a dense vector that encodes meaning. Similar meanings land at nearby points, so distance becomes semantic distance.
  • Directions carry relationships. king − man + woman lands on queen, and Delhi − India + Japan lands on Tokyo, in real word vectors.
  • Word2Vec learned embeddings from context (the distributional hypothesis). Transformers extended this to context-aware vectors that tell the two meanings of "bank" apart.
  • Sentence-BERT trained Transformers to produce good sentence vectors; all-MiniLM-L6-v2 is a small, free model from the same family.
  • Cosine similarity measures the angle between vectors, from −1 to +1. Scores are model-specific: rank them against each other rather than reading them as absolute truth.
  • Semantic search is embed the query, embed the documents, rank by cosine similarity. You built it from scratch, and it's the retrieval half of RAG.
  • Develop with a local model, consider a hosted one for production, and check the MTEB leaderboard and your own data before committing.
  • PCA and t-SNE make embedding space visible, and the clusters they reveal show the geometry of meaning is real.

📝 Learner exercise

  1. Build your own search engine. Pick a corpus you care about: movie plot summaries, news headlines, Stack Overflow questions, or the docs of a library you use. Embed all of it, then write a loop that takes any query and returns the five most similar documents. No vector database yet, just NumPy and cosine similarity. This is the architecture you'll upgrade in Phase 3.
  2. Find the model's blind spot. Write two sentences that mean opposite things but share most of their words (for example, "the movie was good" and "the movie was not good"). How high is their similarity? What does that tell you about using embeddings for sentiment?
  3. Try your language. Embed an English sentence and its Hindi or Tamil translation with all-MiniLM-L6-v2, then with paraphrase-multilingual-mpnet-base-v2. Which model thinks they mean the same thing?