#5577·faiss

IndexIVFIndependentQuantizer leaves its public ntotal stale after add

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

Environment

  • Faiss: faiss-cpu==1.15.0
  • Python: 3.x (CPython)
  • OS: Windows
  • Build: CPU
  • Interface: in-process Python API

Summary

IndexIVFIndependentQuantizer.add() successfully adds vectors to its inner IVF index, but does not update the wrapper's public ntotal field.

As a result, the wrapper reports zero indexed vectors even though the inner IVF contains all added vectors. Search still works, but public metadata and lifecycle accounting are incorrect.

Minimal reproduction

python
import faiss
import numpy as np

d = 4

inner = faiss.IndexIVFFlat(
    faiss.IndexFlatL2(d),
    d,
    2,
)

index = faiss.IndexIVFIndependentQuantizer(
    faiss.IndexFlatL2(d),
    inner,
)

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

index.train(x)
index.add(x)

index.index_ivf.nprobe = 2

D, I = index.search(x[:1], 2)

print("outer ntotal:", index.ntotal)
print("inner ntotal:", inner.ntotal)
print("search:", D[0], I[0])

assert index.ntotal == inner.ntotal

Expected behavior

The wrapper's public ntotal should track the number of vectors stored by the inner IVF:

outer ntotal: 4
inner ntotal: 4

The assertion should pass. Search should continue to return valid results, for example:

D[0] = [0.0, 1.0]
I[0] = [0, 1]

The exact ID among the tied distance-1 results may vary.

Actual behavior

outer ntotal: 0
inner ntotal: 4
search: [0.0, 1.0] [0, 1]

AssertionError

The add operation succeeds and the search result is usable, but the public wrapper metadata is stale.

Root cause

The constructor initializes the wrapper metadata once:

cpp
ntotal = index_ivf->ntotal;

However, IndexIVFIndependentQuantizer::add() only delegates storage to the inner IVF:

cpp
index_ivf->add_core(n, tv.x, nullptr, I.data());

It does not update the wrapper's inherited public ntotal field.

reset() explicitly resets the wrapper metadata:

cpp
void IndexIVFIndependentQuantizer::reset() {
    index_ivf->reset();
    ntotal = 0;
}

This creates an inconsistent lifecycle: reset keeps the wrapper and inner index synchronized, while add leaves them inconsistent.

Relevant source:

Impact

Applications that rely on the public ntotal field may make incorrect:

  • cardinality checks;
  • capacity decisions;
  • pagination calculations;
  • empty-index checks;
  • lifecycle and monitoring decisions.

Because add() and search() complete successfully, the inconsistency is silent.

Suggested fix

Synchronize the wrapper after adding vectors, for example:

cpp
index_ivf->add_core(n, tv.x, nullptr, I.data());
ntotal = index_ivf->ntotal;

A regression test should verify ntotal after:

  1. one add() call;
  2. multiple add() calls;
  3. reset() followed by another add();
  4. comparison between the wrapper and its inner IVF.

Duplicate search

I searched Faiss issues and pull requests for IndexIVFIndependentQuantizer, ntotal, add_core, and related combinations. I did not find an existing report describing this stale wrapper metadata.