Skip to content

Merge shard results by metric type rather than assuming similarity - #5509

Open
wilyan09007 wants to merge 1 commit into
facebookresearch:mainfrom
wilyan09007:fix/issue-5503
Open

Merge shard results by metric type rather than assuming similarity#5509
wilyan09007 wants to merge 1 commit into
facebookresearch:mainfrom
wilyan09007:fix/issue-5503

Conversation

@wilyan09007

@wilyan09007 wilyan09007 commented Aug 9, 2026

Copy link
Copy Markdown

Summary

IndexShardsTemplate::search picked the comparator for merging per-shard results by testing for METRIC_L2:

if (this->metric_type == METRIC_L2) {
    merge_knn_results<idx_t, CMin<distance_t, int>>(...);   // smaller is better
} else {
    merge_knn_results<idx_t, CMax<distance_t, int>>(...);   // larger is better
}

Everything that is not METRIC_L2 was therefore merged as a similarity. METRIC_L1, METRIC_Linf and METRIC_Lp return distances, so once results crossed a shard boundary they came back farthest-first, while the same vectors in a single unsharded index came back nearest-first.

This PR selects the comparator with the existing is_similarity_metric() helper from MetricType.h, which is exactly the distinction the branch needs.

Fixes #5503

Behaviour change

is_similarity_metric() is true only for METRIC_INNER_PRODUCT and METRIC_Jaccard, so:

metric before after
METRIC_L2 CMin CMin (unchanged)
METRIC_INNER_PRODUCT CMax CMax (unchanged)
METRIC_L1, METRIC_Linf, METRIC_Lp, METRIC_Canberra, METRIC_BrayCurtis, METRIC_JensenShannon CMax CMin
METRIC_Jaccard CMax CMax (unchanged)

IndexShardsTemplate<IndexBinary> is also unaffected: IndexBinary::metric_type defaults to METRIC_L2, which keeps CMin as before.

Reproduction

Against the released faiss-cpu 1.15.0 wheel, using the reporter's example:

import faiss, numpy as np

xb0 = np.array([[0., 0.]], dtype='float32')
xb1 = np.array([[10., 0.]], dtype='float32')
xq  = np.array([[1., 0.]], dtype='float32')

flat = faiss.IndexFlat(2, faiss.METRIC_L1)
flat.add(np.vstack([xb0, xb1]))
print(flat.search(xq, 2))

s0 = faiss.IndexFlat(2, faiss.METRIC_L1); s0.add(xb0)
s1 = faiss.IndexFlat(2, faiss.METRIC_L1); s1.add(xb1)
shards = faiss.IndexShards(2, False, True)
shards.add_shard(s0); shards.add_shard(s1)
print(shards.search(xq, 2))
single IndexFlat  D,I = [[1. 9.]] [[0 1]]     # nearest first, correct
IndexShards       D,I = [[9. 1.]] [[1 0]]     # farthest first, reversed

Test

Adds Shards::test_shards_distance_metric_ordering to tests/test_meta_index.py. It splits the dataset across three METRIC_L1 shards and checks that each result row is ordered nearest-first and matches the distances an unsharded IndexFlat(METRIC_L1) returns. Distances rather than labels are compared so that equidistant neighbours may be returned in either order.

The test fails on main (the rows come back reversed) and passes with this change.

Testing notes

I reproduced the bug against the released 1.15.0 wheel as shown above, but I was not able to build faiss from source on this machine (Windows, no local C++ toolchain), so I have not executed the C++ build or run the test suite locally. The added test's pass/fail claim above follows from the comparator change rather than from a local run. Please treat CI as the gate, and I am happy to adjust if anything in the suite disagrees.

IndexShardsTemplate::search chose the merge comparator by testing for
METRIC_L2, so every other metric was merged as a similarity. Distance metrics
such as METRIC_L1, METRIC_Linf and METRIC_Lp therefore returned the farthest
vectors first once results crossed a shard boundary, while the same data in a
single unsharded index returned them nearest first.

Select the comparator with is_similarity_metric() instead. METRIC_L2 and
METRIC_INNER_PRODUCT keep their existing behaviour, as does IndexBinary, whose
metric_type defaults to METRIC_L2.
@meta-cla meta-cla Bot added the CLA Signed label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IndexShards merges non-L2 distance metrics as similarities, reversing METRIC_L1 result ordering

1 participant