The vector and the distance metric

'Closest' is not one thing — L2 measures the gap between two arrowtips, cosine measures only the angle between them, and the chosen distance metric must match how the embeddings were made, or every neighbor it returns is silently wrong.

Previously

Now that we can measure how close any two vectors are, we finally have a precise definition of 'the true nearest neighbors'. The next problem: that brute-force scan from scene 1 is the only thing that gets those neighbors perfectly right — so before we make anything faster, we need a way to MEASURE how much correctness a faster method gives up.

Scene 02

The vector and the distance metric

  1. Watch
  2. Try it
  3. Predict
  4. Capture
acoustic → electroniccalm → energeticangle θtip gapBig WaveHigh PulseNear TipTwin BeatSlow JamNow PlayingRaw arrows — L2 (tip gap) picks the winner.NEIGHBOUR RANKINGL2 · tip gapsmaller gap = closer1Near Tip0.072Twin Beat0.243High Pulse0.324Big Wave0.335Slow Jam0.46change the metric — watch #1 change name
What to watch for

A vector (the list of numbers we turned each song into last scene) can be drawn as an arrow from the origin — its direction and its length both carry meaning. Watch the blue 'Now Playing' query arrow and the 'Big Wave' arrow beside it: Big Wave points almost the SAME direction but is much longer. Now ask 'how close are these?' two honest ways at once. The violet wedge measures only the angle between the arrows — tiny, so it says 'near-perfect match'. The amber line measures the gap between the two arrowtips — big, so it says 'far apart'. Same two arrows, two opposite verdicts. There is no single 'closest'; there's a CHOICE of metric.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Metric.score
the three rules that turn two vectors into one number
def l2(q, v): # tip gap
return sqrt(sum((qi - vi)**2
for qi, vi in zip(q, v)))
def dot(q, v): # angle and length
return sum(qi * vi for qi, vi in zip(q, v))
def cosine(q, v): # angle only
return dot(q, v) / (norm(q) * norm(v))
Index.normalizeAtIngest
snap every stored vector to length 1, once
def ingest(vectors):
for v in vectors:
if NORMALIZE:
v = v / norm(v) # onto the unit circle
store(v)
# on unit-length vectors:
# cosine(q, v) == dot(q, v)
# argmax dot == argmin l2
Index.topK
score the query against every vector, rank, return k
def top_k(query, k):
q = query / norm(query) if NORMALIZE else query
scored = []
for v in stored_vectors:
if METRIC == 'l2': s = -l2(q, v)
elif METRIC == 'dot': s = dot(q, v)
else: s = cosine(q, v)
scored.append((s, v))
scored.sort(reverse=True) # higher score = closer
return scored[:k]

Where this sits in Build a vector database (Pinecone / Weaviate / pgvector style)

Scene 02 of 15, in the Why vectors act — Why a billion-vector exact scan can't hit 10 ms, and what 'closest' even means.. 'Closest' isn't one thing — L2 is the gap between arrowtips, cosine is the angle — and a metric that mismatches the embedding silently returns garbage. Normalize once and they agree.

Up next. We have a definition of correct, but no scorecard. Let's build the simplest possible 'index' — one that just runs the brute-force scan — call its perfect result the baseline, and use it to define the two numbers every faster trick will be judged on.

All 15 scenes in Build a vector database (Pinecone / Weaviate / pgvector style) · Every curriculum

Built with Arqly
Every scene in Build a vector database (Pinecone / Weaviate / pgvector style) builds on the one before it.All 15 Build a vector database (Pinecone / Weaviate / pgvector style) scenes