#5584·faiss

IndexShardsIVF silently drops SearchParametersIVF.sel

Author: leemeiiCreated Sep 5, 2026Updated Sep 5, 2026

Environment

  • Faiss: faiss-cpu==1.15.0
  • Interface: Python CPU runtime
  • Index: IndexShardsIVF
  • Metric: METRIC_L2

Summary

IndexShardsIVF.search() accepts SearchParametersIVF, reads nprobe, but does not forward the search parameters to the child IVF indexes.

As a result, SearchParametersIVF.sel is silently ignored. A selector that should restrict results to ID 100 is not applied to the second shard, and disallowed ID 200 is returned.

Minimal reproduction

python
import faiss
import numpy as np

x0 = np.array([[0.0]], dtype="float32")
x1 = np.array([[1.0]], dtype="float32")


def make_child(vector, external_id):
    child_quantizer = faiss.IndexFlatL2(1)
    child_quantizer.add(np.array([[0.0]], dtype="float32"))

    child = faiss.IndexIVFFlat(child_quantizer, 1, 1)
    child.add_with_ids(
        vector,
        np.array([external_id], dtype="int64"),
    )
    child.nprobe = 1
    return child


left = make_child(x0, 100)
right = make_child(x1, 200)

outer_quantizer = faiss.IndexFlatL2(1)
outer_quantizer.add(np.array([[0.0]], dtype="float32"))

shards = faiss.IndexShardsIVF(
    outer_quantizer,
    1,
    False,  # threaded
    False,  # successive_ids: child IDs are already global
)

shards.add_shard(left)
shards.add_shard(right)

params = faiss.SearchParametersIVF(nprobe=1)
params.sel = faiss.IDSelectorArray(
    np.array([100], dtype="int64")
)

D_child, I_child = left.search(x0, 2, params=params)
D_shards, I_shards = shards.search(x0, 2, params=params)

print("child:", D_child, I_child)
print("shards:", D_shards, I_shards)

Expected behavior

The selector allows only ID 100.

The direct child control returns:

child labels: [[100, -1]]

The sharded index should return only ID 100:

shards labels: [[100, -1]]

The exact ordering of empty-result sentinels is not important.

Actual behavior

child labels: [[100, -1]]
shards labels: [[100, 200]]

The second shard returns ID 200 even though it is not included in params.sel.

The result is deterministic with two exact one-list IVF children and nprobe=1.

Root cause

IndexShardsIVF::search() validates and reads the concrete IVF parameters:

cpp
const IVFSearchParameters* params = nullptr;

if (params_in) {
    params = dynamic_cast<const IVFSearchParameters*>(params_in);
}

idx_t nprobe = params ? params->nprobe : index0->nprobe;

However, each child is called as follows:

cpp
index->search_preassigned(
    n,
    x,
    k,
    Iq.data(),
    Dq.data(),
    distances,
    labels,
    false);

The params argument is omitted. Therefore, the child receives no selector and scans all eligible IDs.

Relevant source:

SearchParameters.sel is documented as restricting the search to selected IDs.

Impact

Applications using selectors for filtering, tenant isolation, authorization boundaries, or result scoping may receive IDs that were explicitly excluded by the caller.

The request succeeds and returns plausible distances, so the filtering failure is silent.

Suggested fix

For configurations where child IDs already share the global ID space, forward the IVF parameters:

cpp
index->search_preassigned(
    n,
    x,
    k,
    Iq.data(),
    Dq.data(),
    distances,
    labels,
    false,
    params);

For successive_ids=True, a selector cannot necessarily be forwarded unchanged because child IDs may be translated after search. Faiss should either:

  1. translate the selector into each shard's local ID space before searching; or
  2. reject selector searches with an explicit error until selector translation is supported.

The implementation should also audit forwarding of other SearchParametersIVF fields such as max_codes.

Duplicate search

A bounded search found no exact issue or PR for IndexShardsIVF silently dropping SearchParametersIVF.sel.

Issue #5523 concerns IndexIDMap.range_search rejecting IVF parameters. Issue #5581 and PR #5582 concern selector ID-space translation in ordinary IndexShards; PR #5582 explicitly leaves the separate IndexShardsIVF::search() override unchanged.