IndexShards applies selectors to local IDs with successive_ids enabled
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.0on 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
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.
Source: facebookresearch/faiss