01The thing that goes wrong
A vector index is supposed to be a mirror: you ask it for things near your query, and it tells you what is near. It is easy to assume the mirror is neutral — that if a document never gets retrieved, the document is at fault.
It is often the geometry’s fault. In a vector space, some points sit in positions that make them close to everything, so they get dragged into result lists they have no business being in. These are called hubs. Their mirror image is worse and gets discussed far less: the points that are near nothing, which never appear in any result list at all. Call them what they are — unreachable. They are in your index, they cost you storage, they will never be read.
The standard explanation is the curse of dimensionality: in high dimensions distances concentrate, everything is roughly equidistant from everything, nearest-neighbour search stops meaning anything. It is a satisfying story. I set out to draw it, and the measurement did not cooperate.
02The experiment
Everything below is one experiment, run three ways. It is deliberately the simplest thing that can show the effect, so there is nowhere for a confound to hide:
| Knob | Setting |
|---|---|
| Corpus | 500 vectors, each coordinate drawn independently from a standard normal, one fixed seed |
| Queries | every vector queries the other 499 — so the query distribution matches the corpus exactly |
| Retrieve | top k = 10 by the chosen metric |
| Measure | Nk(i) — how many of the 500 result lists contain vector i |
Because every vector queries once and each query returns ten results, the 500 lists contain 5,000 slots in total, spread over 500 vectors. The fair share is exactly ten. Any vector above ten is taking someone else’s place; any vector at zero has been shut out entirely. That is the whole measurement, and it needs no ground-truth labels, which is what makes it honest — there is no relevance judgement to argue about.
The three ways:
| Mode | Vectors | Ranked by | What ships like this |
|---|---|---|---|
| raw · euclidean | as drawn | smallest L2 distance | an index built straight on model outputs |
| raw · dot product | as drawn | largest inner product | MIPS indexes, most ANN libraries’ default |
| normalized · cosine | scaled to unit length | largest inner product | what nearly every embedding API returns |
03What came out
At 256 dimensions:
| Mode | Never retrieved | Most-returned vector | Skew of Nk | corr(length, Nk) |
|---|---|---|---|---|
| raw · euclidean | 18.6% | 191× | 4.29 | −0.74 |
| raw · dot product | 0.0% | 23× | 0.29 | +0.66 |
| normalized · cosine | 0.0% | 20× | 0.14 | +0.03 |
Read the first row slowly. Ninety-three of five hundred vectors were returned to nobody, while a single vector was returned 191 times — it is in 38% of all result lists. The corpus is statistically homogeneous: every vector was drawn from the same distribution. There is no sense in which that one vector is better. It is just better placed.
A hub is a point near the middle
The correlation between a vector’s distance from the centre of the cloud and how often it gets retrieved is −0.74. Nearer the middle, retrieved more. That is the entire mechanism: being close to the centre of a cloud means being close to everything in it, and “close to everything” is exactly what a nearest-neighbour query rewards. The hub is not similar to your query. It is similar to every query.
The dot-product row is the same disease with the symptom inverted. The correlation flips sign: +0.66, meaning the winners are now the longest vectors. Inner product is
The third row is the point of the post. Normalizing does not improve the ranking function. It removes the variable both biases were exploiting. Once every vector has length one, there is no middle to be nearer to and no magnitude to win on. The correlation falls to +0.03 and nobody is shut out.
04Run it yourself
The lab below builds that corpus in your browser and runs the full 500×499 comparison on every control change — no precomputed answers. Each cell in the left panel is one vector, shaded by how often it is retrieved; black means never. Start on raw · euclidean, look at the black, then press normalized · cosine.
Then do the thing the lab is really for: stay on normalized · cosine and drag the dimension slider from 2 to 256. Nothing happens. No black cells appear at 32 dimensions, or 128, or 256. If high dimensionality were the cause of hubness, that slider would be a horror show. It is a flat line.
Now go back to raw · euclidean and drag the same slider. The black spreads:
| Dimensions | raw · euclidean | raw · dot product | normalized · cosine |
|---|---|---|---|
| 2 | 0.2% | 85.2% | 0.0% |
| 8 | 2.2% | 26.8% | 0.0% |
| 32 | 9.4% | 2.6% | 0.0% |
| 128 | 13.6% | 0.2% | 0.0% |
| 256 | 18.6% | 0.0% | 0.0% |
Share of 500 vectors never returned by any of the 500 queries. Same seed throughout.
Three different shapes, which is the most useful thing in the post. Euclidean gets worse with dimension. Dot product gets dramatically better — at two dimensions it is catastrophic, shutting out 85% of the corpus, because with only two coordinates a handful of long vectors dominate every direction at once. And cosine is a flat zero everywhere.
“High dimensions break nearest-neighbour search” cannot explain a column that goes down, and it cannot explain a column that is flat. The dimension is not doing the work. The length gradient is.
Then what is the curse of dimensionality actually about?
It is a real phenomenon and I am not claiming otherwise — it is just a different one. Distance concentration says the gap between the nearest and farthest point, relative to the nearest, shrinks as dimension grows: contrast gets worse, so an approximate index has less signal to prune on and exact search gets relatively more attractive. That is about the difficulty of search.
Hubness is about the fairness of search, and the two come apart cleanly in the table above. Normalized cosine at 256 dimensions has thoroughly concentrated distances and yet perfectly even retrieval. Concentration is necessary for hubness to be possible, but it is not sufficient, and on the unit sphere — where every real embedding lives — it does not produce it.
05What this means if you are building retrieval
The main lesson is cheap, which is the best kind: normalize, and check that you did. Most embedding APIs return unit vectors already, which is why this problem is rarer in practice than the arithmetic suggests. The places it bites are the seams — and seams are where nobody looks.
- You trained your own encoder. Nothing normalized it unless you wrote the line. A two-tower model trained with a dot-product objective will happily learn to encode popularity as magnitude, and then your index ranks by popularity while you believe it ranks by meaning.
- You concatenated embeddings, or glued a sparse signal onto a dense vector, or summed chunk vectors into a document vector. Summing is the classic: a document with more chunks gets a longer vector, so long documents become hubs. Average instead of summing, then renormalize.
- You are doing MIPS on purpose. Some systems want magnitude to count, because magnitude carries a prior. That is legitimate — just know you have opted into a popularity bias, and measure its size rather than discovering it in a complaint.
The measurement is five lines and needs no labels
Take a sample of your corpus. Let every item query the index. Count how many times each item is returned. You now have Nk, and two numbers worth putting on a dashboard: the share of items never returned, and the share of all results taken by the top 1% of items. Neither needs a relevance judgement, a labelled set, or an evaluation budget. If the first number is not near zero, some of your corpus is paying rent on a room nobody can enter.
06Recap
- Retrieval can be unfair in a way that has nothing to do with relevance. Some vectors are returned to everybody and some to nobody, in a corpus where every vector was drawn from the same distribution.
- Unnormalized euclidean shut out 18.6% of the corpus at 256 dimensions and handed one vector 191 of 5,000 result slots.
- The mechanism is a length gradient, not dimension. Euclidean favours the middle (−0.74), inner product favours the long (+0.66). Both are correlations with length.
- Normalizing removes the variable, not the symptom, and takes the unreachable share to 0.0% at every dimension tested, with correlation +0.03.
- Dimension alone does nothing here. On the unit sphere, 256 dimensions behaves like 2 — and for dot product, more dimensions actively helped.
- You can measure this on your own index this afternoon, without a single relevance label.
The satisfying explanation was that high dimensions are strange and strangeness breaks things. The measurement says something more ordinary and more actionable: a quantity you were not looking at — the length of your vectors — was quietly deciding which of your documents exist.
Sources, and what is mine
Hubness in nearest-neighbour search was characterised by Radovanović, Nanopoulos & Ivanović (Hubs in Space, JMLR 2010) · Two Towers, One Index, which is where the index this post picks on gets built
What I could and could not verify. Direct fetches to journals and arXiv were blocked from where this was written, so the attribution above is from memory and search, not from the paper, and I have not checked my terminology or my framing against it. Treat it as a pointer to read, not as a claim about what that paper says.
Every number in this post is from the experiment described in §02, which runs in your browser on this page — there are no quoted figures anywhere. The corpus is 500 vectors from a seeded Gaussian; the lab rebuilds it and runs all 500×499 comparisons on every control change, so the figures in the prose are the figures the page renders. Before writing any of it I checked the effect held across five independent seeds at 256 dimensions: unnormalized euclidean gave skew 4.4–7.3 and 23.5–29.0% unreachable, normalized cosine gave skew 0.20–0.54 and 0.0% unreachable every time.
And it is a model. An isotropic Gaussian is not a real corpus: real embeddings are clustered, anisotropic and nothing like exchangeable, and real queries are not drawn from the corpus. I used a Gaussian precisely because it is the fairest possible case — every vector drawn identically, no popular documents, no topical clumps. The unfairness measured here is the floor that geometry alone imposes, before any property of your actual data is added to it. Clustered data, which I also simulated, lands between the extremes. What the model cannot tell you is the size of the effect on your index, which is why §05 gives you the five-line measurement instead of a number to trust.