Skip to content

IndexShardsIVF silently drops SearchParametersIVF.sel #5584

Description

@leemeii

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

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:

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:

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:

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions