Fix Jaccard heap selection in IVF search (#5408)

Summary:
- Use `is_similarity_metric()` for IVF heap initialization, merging, reordering, and result handling.
- Add Jaccard IVFFlat regression coverage across parallel modes 0-3.

## Why

Jaccard scanners use similarity ordering, but `IndexIVF::search_preassigned` selected distance heaps for every metric except inner product. The resulting `FLT_MAX` threshold rejected all Jaccard candidates, returning `-1` even when the only inverted list was fully scanned.

Fixes https://github.com/facebookresearch/faiss/issues/5399.

## Validation

- [x] `cmake --build build --target faiss_test --config Release -j 8`
- [x] `ctest --test-dir build --output-on-failure -C Release` (251 tests, 5 skipped)
- [x] `clang-format-21 --dry-run --Werror faiss/IndexIVF.cpp tests/test_ivf_index.cpp`

## Notes for reviewers

Python and GPU suites were not run locally; the fix and regression exercise the shared C++ IVF path.

Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5408

Reviewed By: pankajsingh88, mnorris11

Differential Revision: D112830842

Pulled By: bshethmeta

fbshipit-source-id: c6431d2bee1b4fecd2e46cfda397722947a5407a
This commit is contained in:
saime
2026-07-23 10:53:58 -07:00
committed by meta-codesync[bot]
parent 3110fd2424
commit 7437cac4d9
2 changed files with 26 additions and 4 deletions
+4 -4
View File
@@ -500,7 +500,7 @@ void IndexIVF::search_preassigned(
if (!do_heap_init) {
return;
}
if (metric_type == METRIC_INNER_PRODUCT) {
if (is_similarity_metric(metric_type)) {
heap_heapify<HeapForIP>(k, simi, idxi);
} else {
heap_heapify<HeapForL2>(k, simi, idxi);
@@ -511,7 +511,7 @@ void IndexIVF::search_preassigned(
const idx_t* local_idx,
float* simi,
idx_t* idxi) {
if (metric_type == METRIC_INNER_PRODUCT) {
if (is_similarity_metric(metric_type)) {
heap_addn<HeapForIP>(
k, simi, idxi, local_dis, local_idx, k);
} else {
@@ -524,7 +524,7 @@ void IndexIVF::search_preassigned(
if (!do_heap_init) {
return;
}
if (metric_type == METRIC_INNER_PRODUCT) {
if (is_similarity_metric(metric_type)) {
heap_reorder<HeapForIP>(k, simi, idxi);
} else {
heap_reorder<HeapForL2>(k, simi, idxi);
@@ -598,7 +598,7 @@ void IndexIVF::search_preassigned(
size_t old_scan_cnt = 0;
size_t old_heap_updates = 0;
if (metric_type == METRIC_INNER_PRODUCT) {
if (is_similarity_metric(metric_type)) {
HeapResultHandler<HeapForIP, false> handler(
k, simi, idxi);
old_scan_cnt = handler.stats.scan_cnt;
+22
View File
@@ -256,6 +256,28 @@ TEST(IVF, list_context) {
}
}
TEST(IVF, jaccard_search_returns_most_similar_vector) {
constexpr int d = 3;
constexpr int nb = 3;
const float xb[nb * d] = {
1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f};
faiss::IndexFlatL2 quantizer(d);
quantizer.add(1, xb);
faiss::IndexIVFFlat index(&quantizer, d, 1, faiss::METRIC_Jaccard);
index.add(nb, xb);
for (int parallel_mode = 0; parallel_mode < 4; parallel_mode++) {
index.parallel_mode = parallel_mode;
float distance;
faiss::idx_t label;
index.search(1, xb, 1, &distance, &label);
EXPECT_EQ(label, 0) << "parallel mode " << parallel_mode;
EXPECT_FLOAT_EQ(distance, 1.0f) << "parallel mode " << parallel_mode;
}
}
// Test: search_preassigned with out-of-range keys throws a catchable
// FaissException instead of calling std::terminate from an uncaught
// exception inside the OpenMP parallel region.