#1246·txtai

IVFSparse: documented `nfeatures` setting has no effect (feature selection is overwritten)

Author: liugj0710Created Sep 18, 2026Updated Sep 18, 2026

Describe the bug

The nfeatures option of the ivfsparse sparse ANN backend is documented as:

yaml
ivfsparse:
  sample: percent of data to use for model training (0.0 - 1.0)
  nfeatures: top n features to use for model training (int)
  nlist: desired number of clusters (int)
  nprobe: search probe setting (int)
  minpoints: minimum number of points for a cluster (int)

(see docs/embeddings/configuration/scoring.md)

It has no effect. In src/python/txtai/ann/sparse/ivfsparse.py, build() computes the feature-selected matrix and then immediately overwrites it with the full matrix:

python
# Select top n most important features that contribute to L2 vector norm
indices = np.argsort(-norm(train, axis=0))[: self.setting("nfeatures", 25)]
data = train[:, indices]
data = train        # <-- overwrites the feature selection

k-means is therefore always trained on all dimensions, regardless of nfeatures.

To Reproduce

Spy on MiniBatchKMeans.fit and record the width of the matrix that build() passes in. nfeatures=2 with 8 dimensions should produce a (rows, 2) matrix.

python
import numpy as np
from scipy.sparse import csr_matrix
import sklearn.cluster as skc
from txtai.ann.sparse.ivfsparse import IVFSparse

seen = []
_orig = skc.MiniBatchKMeans.fit
def spy(self, X, *a, **k):
    seen.append(tuple(X.shape))
    return _orig(self, X, *a, **k)
skc.MiniBatchKMeans.fit = spy

rng = np.random.default_rng(7)
for nfeatures in (2, 4, 8):
    seen.clear()
    ann = IVFSparse({
        "backend": "ivfsparse",
        "dimensions": 8,
        "ivfsparse": {"nfeatures": nfeatures, "nlist": 4, "minpoints": 1},
    })
    ann.build(csr_matrix(rng.random((300, 8))), 4)
    print("nfeatures =", nfeatures, "-> fit received", seen[0])

Output on master (9.14.0):

nfeatures = 2 -> fit received (300, 8)
nfeatures = 4 -> fit received (300, 8)
nfeatures = 8 -> fit received (300, 8)

Expected (300, 2), (300, 4), (300, 8).

Expected behavior

MiniBatchKMeans.fit receives a matrix with nfeatures columns.

Removing the data = train line is enough — the selection itself is valid:

nfeatures = 2 -> fit received (300, 2)
nfeatures = 4 -> fit received (300, 4)
nfeatures = 8 -> fit received (300, 8)

The rest of the IVFSparse lifecycle (index / append / delete / save-load) still behaves correctly with that line removed.

Fix

diff
         # Select top n most important features that contribute to L2 vector norm
         indices = np.argsort(-norm(train, axis=0))[: self.setting("nfeatures", 25)]
         data = train[:, indices]
-        data = train
 
         # Cluster data using k-means
         kmeans = MiniBatchKMeans(n_clusters=clusters, random_state=0, n_init=5).fit(data)

Happy to open a PR with this one-line change plus a regression test if that is the preferred direction. If feature selection was disabled on purpose, the alternative would be to drop the dead assignment and remove nfeatures from the docs so the configuration is not misleading.

Secondary observation (related, same file)

IVFSparse.delete() guards only the upper bound, so a negative index is accepted:

python
size = self.size()
for x in ids:
    if x < size and x not in self.deletes:      # missing 0 <=
        self.deletes.append(int(x))

delete([-1]) appends -1 to self.deletes, but no indexed id is ever negative, so count() (= size() - len(self.deletes)) is permanently reduced by one:

count 1249 -> 1248 after delete([-1])   # expected unchanged

int(x) also truncates floats (delete([2.5]) deletes id 2). The same missing lower bound appears in ann/dense/numpy.py:64 (x < self.backend.shape[0]) and ann/dense/ggml.py:225 (x < rows and x not in self.deletes). Not reachable from Embeddings.delete(), which passes non-negative index ids — only from direct ANN usage. Worth tightening to 0 <= x < size while in the area.

Environment

  • txtai 9.14.0 (master, 19ff237c)
  • Python 3.13.14
  • numpy 2.5.3, scipy 1.18.1, scikit-learn, msgpack 1.2.2
  • No GPU / model download required for this repro