The Geometry of Meaning: Similarity and Distance

Embeddings are points in space. How cosine similarity measures meaning, why direction beats distance, and what king minus man plus woman really proves.

Sep 5, 202616 min readFollow

Topics You Will Master

Why an embedding is a point in space, not just a list
How cosine similarity measures meaning, and why direction beats length
What subtracting two vectors actually gives you
How king minus man plus woman is computed, and what it does not prove

Yesterday we ended with a table of numbers: one row per token, each row a few hundred values wide. Useful, but still abstract. Today those rows stop being lists and become locations.

Once a word is a point in space, questions about meaning turn into questions about geometry. Which points are close? Which way is it from man to woman? That shift is what makes search, clustering, and the famous analogy trick possible.

Bestseller

LLM Fine-Tuning with Hugging Face: LoRA, QLoRA, PEFT

Fine-tune BERT, T5, ViT, LLaMA-style models and Qwen3-TTS using Hugging Face Transformers, custom datasets, LoRA, QLoRA.

95% off$199.99
$9.99Enroll now 30 day refund, lifetime access

The Geometry of Meaning

Note

The code below uses only NumPy: pip install numpy. Real embeddings have hundreds of dimensions, so to keep every number visible we use a hand-built toy space with four. The arithmetic is identical, just smaller.


A Vector Is a Point in Space

Two numbers describe a point on a page. Three describe a point in a room. Seven hundred and sixty-eight describe a point in a space nobody can picture, but the mathematics does not care that you cannot picture it.

Here is our toy space. Four dimensions, and for once we will say what each one means: how royal, how male, how female, how fruit.

PYTHON
import numpy as np

#                       royal  male  female  fruit
words = {
    "king":  np.array([0.95, 0.90, 0.05, 0.00]),
    "queen": np.array([0.95, 0.05, 0.90, 0.00]),
    "man":   np.array([0.10, 0.92, 0.05, 0.00]),
    "woman": np.array([0.10, 0.05, 0.92, 0.00]),
    "apple": np.array([0.02, 0.05, 0.05, 0.95]),
}

print("king  ->", words["king"])
print("queen ->", words["queen"])
OUTPUT
king  -> [0.95 0.9  0.05 0.  ]
queen -> [0.95 0.05 0.9  0.  ]

Read those two rows side by side. King and queen agree almost exactly on the first number and disagree almost exactly on the next two. That pattern is the relationship between the words, written as coordinates.

In a real model nobody labels the columns. The dimensions are learned, and they mix concepts together. We label them here only so you can follow the arithmetic.


Direction, Not Length

The obvious way to compare two points is to measure the straight-line gap between them. For embeddings that turns out to be the wrong instinct.

Watch what happens when we take king and simply make it five times longer, without changing which way it points.

PYTHON
king = words["king"]
long_king = king * 5

def cosine(a, b):
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

print("euclidean distance :", round(float(np.linalg.norm(king - long_king)), 3))
print("cosine similarity  :", round(cosine(king, long_king), 3))
OUTPUT
euclidean distance : 5.238
cosine similarity  : 1.0

Straight-line distance says these are far apart. Cosine says they are the same thing, and cosine is right: the vector points in exactly the same direction, it is just longer.

That matters because vector length tends to track how often a word appeared in training, not what it means. A rare word and a common word can mean nearly the same thing while sitting at very different distances from the origin. Measuring the angle ignores that and keeps only the part we care about.

Direction, Not Length


Advertisement

How Cosine Similarity Actually Works

The formula has three pieces: multiply the two vectors position by position and add up the results, then divide by each vector's length.

Write it out once by hand so nothing is hidden behind a library call.

PYTHON
def cosine_from_scratch(a, b):
    dot = sum(x * y for x, y in zip(a, b))          # line up and multiply
    len_a = sum(x * x for x in a) ** 0.5            # length of a
    len_b = sum(y * y for y in b) ** 0.5            # length of b
    return dot / (len_a * len_b)

print("by hand :", round(cosine_from_scratch(words["king"], words["queen"]), 3))
print("numpy   :", round(cosine(words["king"], words["queen"]), 3))
OUTPUT
by hand : 0.579
numpy   : 0.579

The dot product rewards dimensions where both words are large. King and queen are both high on royal, so that term contributes a lot. King is high on male where queen is near zero, so that term contributes almost nothing.

Dividing by the two lengths is what turns a raw score into a number between −1 and 1, so scores from different word pairs can be compared fairly.

Tip

Because dividing by length is the whole trick, most vector databases normalise every vector to length 1 when storing it. After that, cosine similarity is just the dot product, which is much faster at scale.

How Cosine Similarity Works


Reading a Similarity Table

One score in isolation means little. Scores become readable when you rank them.

PYTHON
def rank_against(target, table):
    scores = [(name, round(cosine(target, vec), 3)) for name, vec in table.items()]
    return sorted(scores, key=lambda pair: pair[1], reverse=True)

for name, score in rank_against(words["king"], words):
    print(f"king vs {name:6s} {score}")
OUTPUT
king vs king   1.0
king vs man    0.763
king vs queen  0.579
king vs woman  0.153
king vs apple  0.053

A word is always a perfect 1.0 against itself, which is a useful sanity check. Apple sits near zero, exactly as it should.

Now look at the middle of that list, because it is more interesting than it first appears. King is closer to man (0.763) than to queen (0.579). Nothing has gone wrong. King and man share a strong male dimension, while king and queen share royal but sit at opposite ends of two gender dimensions. "Similar" always means similar along the dimensions the model happens to have learned, which is not always the axis you had in mind.

Reading a Similarity Table


Subtraction Gives You a Direction

Here is the idea the analogy trick rests on. Subtracting one vector from another does not give you a word. It gives you the step that takes you from the first to the second.

PYTHON
male_to_female = words["woman"] - words["man"]
king_to_queen  = words["queen"] - words["king"]

print("man    -> woman :", male_to_female)
print("king   -> queen :", king_to_queen)
print("same direction? :", round(cosine(male_to_female, king_to_queen), 3))
OUTPUT
man    -> woman : [ 0.   -0.87  0.87  0.  ]
king   -> queen : [ 0.   -0.85  0.85  0.  ]
same direction? : 1.0

Two different pairs of words, and almost the same step: drop the male dimension by roughly 0.86, raise the female dimension by roughly 0.86, leave everything else alone. Cosine confirms the two steps point the same way.

That shared step is what people mean when they say a "gender direction" exists in embedding space. It was never programmed in. It emerged because the training text used these pairs of words in parallel ways.

Subtraction Gives You a Direction


Advertisement

King Minus Man Plus Woman, Step by Step

Now the famous line. Read it as a journey rather than an equation: start at king, walk back along the male direction, then walk forward along the female direction, and see where you land.

PYTHON
result = words["king"] - words["man"] + words["woman"]

print("start at king   :", words["king"])
print("minus man       :", words["king"] - words["man"])
print("plus woman      :", result)
print("queen looks like:", words["queen"])
OUTPUT
start at king   : [0.95 0.9  0.05 0.  ]
minus man       : [ 0.85 -0.02  0.    0.  ]
plus woman      : [0.95 0.03 0.92 0.  ]
queen looks like: [0.95 0.05 0.9  0.  ]

Follow the middle line, because that is where the work happens. Subtracting man knocks the male dimension from 0.90 down to −0.02 and wipes out the small female value, leaving a nearly pure royalty vector. It has stopped being a person and become a property. Adding woman then puts a person back, with the female dimension restored to 0.92.

The journey ends at [0.95, 0.03, 0.92, 0.00]. Queen sits at [0.95, 0.05, 0.90, 0.00]. Close, but not identical, and that gap matters for the next step.


The Answer Is a Nearest Neighbour, Not an Equation

The result vector is not queen. It is a point near queen. So the last step of every analogy demo is a search: score the result against every word in the vocabulary and take the best match.

PYTHON
for name, score in rank_against(result, words):
    print(f"result vs {name:6s} {score}")
OUTPUT
result vs queen  1.0
result vs woman  0.769
result vs king   0.563
result vs man    0.138
result vs apple  0.053

Queen wins at 1.0, rounded from a fraction just under it. The demo works.

But look at second and third place. Woman scores 0.769 and king scores 0.563 — both high, and both are words we put into the question ourselves. Of course the answer sits near them; we built it out of them.

This is why real implementations of this trick exclude the three input words from the search before reporting a winner. That single line of housekeeping is rarely mentioned when the result is shown off.

PYTHON
inputs = {"king", "man", "woman"}
candidates = {w: v for w, v in words.items() if w not in inputs}

best = rank_against(result, candidates)[0]
print("excluding the inputs, the winner is:", best)
OUTPUT
excluding the inputs, the winner is: ('queen', 1.0)

Nearest Neighbour, Not Equality


Advertisement

How Much This Really Proves

Being precise here is worth more than being impressed.

Our toy space was built by hand to make the answer come out right, and even then queen only won because we removed the competition. In real embedding spaces the picture is messier. The famous analogies work well for a narrow family of relationships — gender, capital cities, verb tenses — and much less reliably elsewhere. Researchers who removed the exclusion rule found the "winner" was very often just one of the input words wearing a disguise.

So the honest summary is smaller than the headline, and still remarkable: nobody defined a gender direction, a royalty direction, or a plural direction. Predicting text well enough, for long enough, arranged the space so that those directions exist and can be measured.

What the Analogy Proves


Where This Geometry Gets Used

This is not a party trick sitting off to one side of the field. It is the engine under a lot of everyday systems.

Semantic search and RAG embed your question and every document, then return the chunks with the highest cosine score. Recommendation systems place users and items in one space and look for near neighbours. Clustering groups vectors that sit together. Deduplication flags pairs above a threshold. Every one of those is the same operation you just wrote by hand.

The reason it works is the reason we spent today on geometry: once meaning is a location, "find me something similar" becomes "find me something nearby", and computers are extremely good at that.

Where the Geometry Gets Used


Recap

An embedding is a point in space, and the geometry of that space carries the meaning. Straight-line distance is the wrong ruler because vector length tracks word frequency more than sense; cosine similarity measures the angle instead and returns a comparable score between −1 and 1.

Subtracting two vectors gives a direction rather than a word, and the same direction turns up between many word pairs, which is what makes analogies possible at all. King minus man plus woman lands near queen, but "near" is the operative word: the final step is a nearest-neighbour search, and it only names queen once the three input words are excluded from the running.

That geometry is what semantic search, RAG retrieval, recommendation, and clustering all run on. In the next article we leave single vectors behind and ask why a sentence needs more than a bag of points, which is the problem attention was invented to solve.

Found this useful? Keep building with me.

New tutorials every week on YouTube: or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments