KevsRobots Learning Platform
15% Percent Complete
By Kevin McAleer, 7 Minutes
Page last updated August 23, 2026

An embedding is a list of numbers that represents the meaning of a piece of text. That is the whole idea. Everything else is detail.
Feed the sentence “the robot’s motors stalled on the carpet” into an embedding model and you get back something like:
[0.021, -0.184, 0.067, 0.298, ..., -0.043]
384 numbers, in the model we will be using. Each one is a coordinate. Together they place that sentence at a specific point in a 384 dimensional space.
Now here is the useful bit. “the drive train couldn’t handle the deep pile rug” lands at a nearby point, even though it shares almost no words with the first sentence. Meanwhile “feed the sourdough starter every 12 hours” lands miles away.
Similar meaning, nearby points. That is the entire trick.
384 dimensions is impossible to picture, so shrink it to two. Imagine a map where the horizontal axis is roughly “how mechanical is this” and the vertical axis is roughly “how much is this about food”:
food
^
| sourdough starter
| bread proving times
|
|
+-------------------------------> mechanical
motor stall torque
gearbox ratios
Ask “why did my robot stop moving?” and it lands down in the bottom right, near the motor notes and nowhere near the bread. You did not use the word “motor” - it went there because of what the question means.
Real embedding models do this in hundreds of dimensions, and no single axis has a tidy human name. But the principle holds exactly.
Once everything is a point, “most relevant” becomes “closest”. There are a few ways to measure closeness, and the one almost everyone uses for text is cosine distance.
Cosine distance ignores how long the vectors are and only cares about the direction they point. That matters, because a three word note and a three paragraph note about the same topic should score as similar - and with cosine distance they do.
The numbers come back like this:
| Cosine distance | Direction of the two vectors |
|---|---|
| 0.0 | Identical - as close as it gets |
| 1.0 | At right angles - nothing in common |
| 2.0 | Exactly opposite |
It is a distance, so smaller is better. This catches people out constantly, because “score” usually means bigger is better. Later on we will convert it with 1 - distance and show it as a percentage, purely so the output reads naturally.
One warning before you build a mental rule of thumb: with a small model like the one we are using, real text rarely gets anywhere near 0.0. A genuinely excellent match often scores around 0.5, and an unrelated one around 0.95. Those numbers are not a bug and they are not something to fix. Judge results by their rank, never by an absolute cutoff.
ChromaDB ships with a default embedding model called all-MiniLM-L6-v2. It is small - about 80MB - runs on your CPU, and produces 384 dimensional vectors.
It is not the most accurate embedding model in the world. It is, however, free, fast, private, and downloads itself the first time you use it. For personal notes it is a genuinely good choice, and the pipeline we build will let you swap it later if you want.
Here is what it looks like in practice:
# embed_demo.py - a scratch script, run on its own (not part of the tool we build later)
from chromadb.utils import embedding_functions
embed = embedding_functions.DefaultEmbeddingFunction()
vectors = embed([
"The N20 motors stall on carpet",
"The drive train can't cope with a thick rug",
"Feed the sourdough starter every 12 hours",
])
print(f"Each vector has {len(vectors[0])} dimensions")
Run that and you get Each vector has 384 dimensions. The first time it runs it will pause to download the model.
Cosine distance is easy to compute by hand, and doing it once makes the whole idea concrete:
# cosine_demo.py - another scratch script
import math
from chromadb.utils import embedding_functions
embed = embedding_functions.DefaultEmbeddingFunction()
def cosine_distance(a, b):
"""1 minus the cosine of the angle between two vectors."""
dot = sum(x * y for x, y in zip(a, b))
length_a = math.sqrt(sum(x * x for x in a))
length_b = math.sqrt(sum(y * y for y in b))
return 1 - dot / (length_a * length_b)
sentences = [
"The N20 motors stall on carpet",
"The drive train can't cope with a thick rug",
"Feed the sourdough starter every 12 hours",
]
vectors = embed(sentences)
question = embed(["why won't my robot move?"])[0]
for sentence, vector in zip(sentences, vectors):
print(f"{cosine_distance(question, vector):.3f} {sentence}")
Run it and you get something like:
0.701 The N20 motors stall on carpet
0.712 The drive train can't cope with a thick rug
0.969 Feed the sourdough starter every 12 hours
Both robot sentences beat the bread one comfortably, and the question contained none of the words “motor”, “stall”, “carpet” or “drive”. That is semantic search working.
Look at those numbers again though. The best match scored 0.70. If you had written a rule saying “only accept results below 0.3” you would have thrown away the correct answer. Rank, not threshold.
You will never write cosine_distance again after this lesson - ChromaDB does it for you, and does it far faster. But now you know exactly what it is doing.
Being honest about the limits saves you a lot of debugging later:
TB6612FNG or an error code is a job for keyword search. Embeddings blur precise strings into general “this looks like a part number” meaning. We handle this in lesson 12.~/.cache/chroma/onnx_models/. Let it finish; it only happens once.Why: ChromaDB fetches the ONNX build of all-MiniLM-L6-v2 on first use rather than bundling 80MB in the package.
Why: What matters is ranking, not the raw number. A distance of 0.4 is excellent if everything else scored 0.9. Absolute thresholds are the single most common mistake in a first RAG build.
You can use the arrows ← → on your keyboard to navigate between lessons.
Comments