#5581·faiss

IndexShards applies selectors to local IDs with successive_ids enabled

Author: rioyu123Created Sep 5, 2026Updated Sep 5, 2026

Summary

With IndexShards(successive_ids=True), SearchParameters.sel is evaluated against each shard's local IDs, while the returned labels are shifted into the concatenated ID space. This can omit selected global IDs and return global IDs that the selector does not contain.

For two IndexFlatL2 shards holding two vectors each, selecting ID 2 returns no results; selecting ID 0 returns both 2 and 0. Searching an equivalent single flat index returns only the selected ID. Both serial and threaded IndexShards reproduce this behavior.

Platform

  • Linux, CPU.
  • Current source: 2ed4c106e9fb9686e7727e5daf8ad6ad1e164109 (Faiss 1.15.0), built with GCC 13.3, C++20, generic CPU implementation and BLAS/LAPACK. The same cases were reproduced through a standalone C++ program linked against this source build.
  • The Python reproduction below was separately run with faiss-cpu==1.15.0 on Python 3.12.3. The wheel is corroborating evidence, not the current-source build.
  • No GPU or full repository test-suite coverage is claimed.

Reproduction instructions

python
import numpy as np
import faiss

vectors = np.array([[0.0], [10.0], [20.0], [30.0]], dtype="float32")
query = np.array([[20.0]], dtype="float32")

reference = faiss.IndexFlatL2(1)
reference.add(vectors)

for threaded in (False, True):
    first = faiss.IndexFlatL2(1)
    second = faiss.IndexFlatL2(1)
    first.add(vectors[:2])
    second.add(vectors[2:])
    shards = faiss.IndexShards(1, threaded, True)
    shards.add_shard(first)
    shards.add_shard(second)

    for selected_id in (2, 0):
        ids = np.array([selected_id], dtype="int64")
        selector = faiss.IDSelectorBatch(ids)
        params = faiss.SearchParameters(sel=selector)
        _, expected = reference.search(query, 2, params=params)
        _, actual = shards.search(query, 2, params=params)
        print(threaded, selected_id, expected.tolist(), actual.tolist())

Output (threaded, selected ID, reference labels, sharded labels):

False 2 [[2, -1]] [[-1, -1]]
False 0 [[0, -1]] [[2, 0]]
True 2 [[2, -1]] [[-1, -1]]
True 0 [[0, -1]] [[2, 0]]

Expected: the sharded index should match the reference labels in these cases, with the selector interpreted in the same ID space as the wrapper's results.

Additional C++ controls: without a selector, the sharded index correctly returns [2, 1]; with successive_ids=False, selecting local ID 0 returns [0, 0], with both labels satisfying the selector.

Likely cause and fix direction

IndexShards::search passes the unchanged params to every sub-index, then translates labels after each search. Selector membership therefore uses a different ID space from the returned labels. Parameter pass-through was added in #4387.

I'd be happy to prepare a focused fix, but would appreciate guidance on the intended approach first. Translating the selector per shard needs to preserve derived search parameters, and SearchParameters currently has no clone facility. Temporarily replacing sel on the shared caller-owned parameters would also be unsafe across threaded shard searches.

Would you prefer a mechanism for per-shard selector adaptation, or a narrow error for successive_ids=True with a selector until that is supported? I can add regression coverage for both thread modes and keep no-selector and successive_ids=False behavior unchanged.