When Retrieval Lies β Distance, Similarity, and How to Debug It
When semantic search returns junk β low scores, irrelevant hits on top β the reflex is to blame the embedding model and swap it. Resist that. The model is usually fine and the pipeline around it is broken: pooling, normalization, storage, or the sneakiest of all, a distance-vs-similarity mix-up that makes a healthy system look catastrophic. But the deeper lesson here is about how you debug it β because the wrong method will hand you confident, wrong answers all day.

Field names lie: distance vs similarity
Every vector store returns a number per result, and you must know which:
- Similarity (e.g. cosine similarity):
1.0= identical, higher is better. - Distance (e.g. cosine/L2 distance):
0.0= identical, lower is better.
Read one as the other and your conclusions invert. Worse, the field name can be wrong: one popular
store returns cosine similarity in a field literally named distance. The only reliable way to tell
is a self-match β embed a text and score it against itself. Whatever value that produces is your
"identical" anchor. If self scores 0.99, higher means more similar; if it scores 0.00, lower does.
The bug that wore three costumes
A real case: code search over an ASP.NET MVC repo returned junk. There was one root cause β a
single 1 - score inversion in the search layer β but it got "diagnosed" three times:
- "The embedding model is too weak."
- "Vector storage is broken β reindex everything." A self-match read as
0.00002, which looked like the stored vectors were corrupt. - "The collection architecture is fundamentally wrong β full reindex + redesign."
Each theory was confirmed by a test. Each test was wrong β because every test ran through the inverting code. The pipeline lied consistently, so it manufactured consistent, convincing, false evidence. Two needless reindexes were one decision away.
The one measurement that wasn't contaminated
The break came from going around the suspect. A throwaway script embedded the query, one known-relevant chunk, and two known-irrelevant chunks, and read the store's raw score field directly β no application pipeline. The result:
self = 0.9999 (highest)
relevant = 0.2711
irrelevant = 0.0000 (lowest)
That single uncontaminated reading proved three things at once: the field is similarity (self is
the maximum), the embeddings are perfect, and the app was inverting them. The fix was deleting
one 1 - score. No reindex, no new model β exactly what the discipline had refused to do on every
wrong theory.
The discipline
Two rules, and the second is the one people skip:
- Pin down your store's metric, and trust the values over the field name. Verify with a self-match; make sort and threshold agree on one convention.
- Never debug through the component you suspect. A test that runs through a broken transform inherits the lie and produces confident, wrong theories. One controlled measurement that isolates the variable beats ten end-to-end tests. When you reach your third root cause, stop theorizing and isolate.