Skip to content

fix: Fixed wrong distances and neighbours across backends - #82

Merged
Pringled merged 5 commits into
mainfrom
fix/backend-distance-contract
Sep 26, 2026
Merged

Pringled merged 5 commits into
mainfrom
fix/backend-distance-contract

Conversation

@Pringled

Copy link
Copy Markdown
Member

This PR fixes many many bugs that I found while comparing Vicinity backends in SemHash. In no particular order:

  • Several backends returned wrong distances, and in some cases wrong neighbours. Every backend now returns the same distances as the basic backend: cosine distance = 1 − cosine similarity, and Euclidean distance = normal distance (it was squared in some cases).
  • In FAISS we checked whether the metric equalled the string "cosine", but the metric is stored as an enum. Annoy had the reverse problem: it stored the metric as a string and compared it to the enum. Either way the check never matched, so cosine queries were never normalized.
  • When FAISS found fewer than k neighbours, it padded the results with index -1, and these leaked out as results.
  • The FAISS range search radius used the wrong units, so query_threshold could miss items for larger thresholds.
  • We (wrongly) returned squared euclidean distances for FAISS, HNSW, and Voyager.
  • We normalized PyNNDescent query vectors regardless of the metric used.
  • Voyager could assign item ids out of order when building the index, so some queries returned the wrong items.
  • FAISS hnsw, scalar, ivf_scalar and ivfpq indexes are now built with inner product for cosine, like flat and ivf.
  • Querying a PyNNDescent index after save/load crashed, because its neighbour graph was saved as floats.

The tests now catch all of these cases. I also want to add some proper integration tests in a followup.

- faiss: compare against Metric.COSINE (the string comparison never matched), convert
  inner-product and squared-L2 scores to distances, use metric-aware range search radii,
  and drop results padded with index -1
- annoy: store the metric enum so cosine queries are normalized
- voyager: add items with explicit ids (ids could be assigned out of order) and return
  non-squared Euclidean distances
- hnsw: return non-squared Euclidean distances
- pynndescent: only normalize query vectors for the cosine metric
…es unconverted

Halving squared L2 only gives cosine distance for unit vectors, so zero vectors
came out at distance 0.5 instead of 1 on hnsw/scalar/ivf_scalar/ivfpq indexes.
These are now built with inner product where FAISS supports it. LSH returns
Hamming distances, which cannot be converted, so they are returned as is. The
contract test now covers scalar indexes, zero vectors and threshold membership.
…scent graphs on load

- faiss: pq and ivfpqr stay on L2, where halving squared L2 only gives cosine distance
  for unit vectors. Zero vectors (stored or queried) now get cosine distance 1; stored
  zero vectors are tracked through inserts and save/load.
- pynndescent: the saved (indices, distances) neighbour graph came back as one float
  array, so querying a loaded index failed. The original dtypes are restored on load,
  which also fixes indexes saved by earlier versions.
- The save/load test now queries the loaded index.
Every reachable fix now fails a test when reverted: FAISS padding (IVF with small
clusters), range-search radii (thresholds beyond 0.5 cosine / 1 Euclidean), zero
vectors through insert and save/load, LSH Hamming distances, item-to-vector mapping
(every stored vector queried for itself), and string metrics stored as enums.
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
tests/test_vicinity.py 100.00% <100.00%> (ø)
vicinity/backends/annoy.py 95.18% <100.00%> (ø)
vicinity/backends/faiss.py 95.68% <100.00%> (+5.93%) ⬆️
vicinity/backends/hnsw.py 95.77% <100.00%> (+0.39%) ⬆️
vicinity/backends/pynndescent.py 95.71% <100.00%> (+0.19%) ⬆️
vicinity/backends/voyager.py 97.05% <100.00%> (+0.28%) ⬆️
vicinity/version.py 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Pringled
Pringled requested a review from stephantul September 26, 2026 06:53

@stephantul stephantul left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LBTM

@Pringled
Pringled merged commit c15ab7d into main Sep 26, 2026
4 checks passed
@Pringled
Pringled deleted the fix/backend-distance-contract branch September 26, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants