Skip to content

feat(python): train, add and search for SymqgIndex - #205

Merged
gouyt13 merged 2 commits into
mainfrom
feat/symqg-train-api
Nov 1, 2026
Merged

gouyt13 merged 2 commits into
mainfrom
feat/symqg-train-api

Conversation

@gouyt13

@gouyt13 gouyt13 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Third of three PRs moving the Python indexes to a Faiss-style train → add → search workflow, after #203 (C++) and #204 (IvfIndex/HnswIndex). This PR brings SymqgIndex into the same workflow. The C++ QuantizedGraph and QGBuilder are unchanged.

New API

  • SymqgIndex(dim, *, nbits=32, max_degree=32, ef_construction=200, init="pipnn", metric="l2", seed=None). Everything after dim is keyword-only.
    • nbits (4, 8 or 32) replaces quantization_bits; 32 means raw vectors, which 0.7 called 0.
    • max_degree keeps its name: it is a hard bound and a multiple of 32, unlike HNSW's M.
    • dim is now checked at construction, as IVF/HNSW already do, rather than only in add.
  • seed drives both the rotation and graph construction. None draws one seed at construction, matching 0.7's random default while making it reproducible, and index.seed reports it.
  • train(x, *, num_threads=None) checks x as add would (shape and coordinates) and changes nothing; is_trained is always True.
  • add(data, *, num_threads=0) builds the graph from all vectors, using the constructor's ef_construction, init and seed. A second add, or add on a loaded index, raises RuntimeError("SymqgIndex does not support incremental addition; ..."), as Faiss's NSG index does. add returns None, as in Faiss. IvfIndex.add and HnswIndex.add keep returning the new IDs, a public behavior since 0.5.1/0.5.2 that the docs teach; changing them to None would break existing code without any possible warning, so they are unchanged.
  • search(queries, k, ef=None, num_threads=1): a missing or zero ef uses max(k, 10), and a smaller ef is raised to k, as in HnswIndex. HnswIndex.search now also accepts ef=None.
  • len(index) and the nbits, ef_construction, init and seed properties. After load, ef_construction, init and seed are None, because files do not store them.
  • Before vectors are added, search and save raise "SymqgIndex has no vectors; add them or load an index before ...".

0.8 compatibility layer (removed in 0.9)

  • SymqgIndex(dim, max_degree, metric, quantization_bits) and quantization_bits= are mapped to nbits in rabitqlib/__init__.py, so there is no private native overload. The other deprecated names are build(data, ef_construction, num_threads=1, init="pipnn"), search_batch(...), and the quantization_bits (0/4/8) and is_built properties. Each raises a DeprecationWarning at the caller's line.
  • The deprecated build keeps its 0.7 behavior: it replaces any existing graph, and without a user-given seed it draws a new seed each time. Mixed or invalid old forms raise TypeError/ValueError.
  • The stubs declare every old form @deprecated.

Behavior changes for existing code

  • search with ef=0 or ef < k no longer raises: the zero case uses the default, and a smaller value is raised to k.
  • Before vectors are added, the error message is the new one above. repr shows nbits and no longer quantization_bits/is_built.

Other changes

  • Docs: index/qg.md, usage.md (SymphonyQG's train only reads, so it may run alongside searches), hnsw.md (ef=None), and a SymphonyQG section on the migration page, which is reorganized into IVF/HNSW, SymphonyQG, and files/C++ sections.
  • Samples: symqg_indexing.py uses the constructor and add, and its --quantization-bits flag is now --nbits (4/8/32). symqg_querying.py uses search.
  • As in feat(python): train, set_centroids and add for IvfIndex and HnswIndex #204, version pins and benchmarks/run.py are left for the 0.8.0 release, and existing tests stay on the 0.7 forms as coverage of the compatibility layer.

No saved-index format changes and no new dependencies.

Testing

All checks below ran on 45049b4 (on a4d0c8a), on Linux x86-64, GCC 14.3, RABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF. The follow-up 273c0c8 changes only test_txtai_compat.py, which passes and is clean under check-python.sh.

  • Formatting and static analysis: ./scripts/check-format.sh, ./scripts/check-python.sh, and full ./scripts/check-tidy.sh (library and bindings) passed.
  • Python, after reinstalling into the project environment and confirming the imported extension is the new build:
    • pytest: 990 passed; 2 skipped (macOS/ARM wheel-only tests).
    • Editor, mypy and Pyright checks: 19 passed. The mypy deprecation check now also confirms the six SymphonyQG 0.7 forms are reported.
  • New tests:
    • test_symqg_api.py: signatures, defaults and keyword-only arguments; argument errors, including dim at construction; seeds; train (shape and NaN checks, num_threads=None); and add once per index, including after load. Also: settings being None after load, add using the constructor settings (it matches a deprecated build with the same settings byte for byte), the ef defaulting and raising rules, operations before add, and type errors not mentioning private overloads.
    • test_compat_08.py: each SymphonyQG 0.7 form warns once at the caller, and quantization_bits= gives the same file as the matching nbits. Also covered: mixed or invalid old forms, build/search_batch/is_built, rebuilding with build, and a fresh seed per build unless one was given.
    • test_train_api.py: HnswIndex.search(ef=None) equals ef=0.
    • test_txtai_compat.py now mirrors txtai upstream (neuml/txtai 4d5e05f): it reads num_stored_points for the ID offset and delete bounds and num_points for the count, and no longer counts deletes itself. txtai ignores add's return value and does not use SymqgIndex.
    • Every new test for the review fixes was first run against the previous build and failed there.
  • Old forms give the same results as main: main before feat(index): train centroids inside IVF and HNSW indexes #203 (6ba97a8) and this branch were built in scratch copies, with the random seeds pinned by temporary patches that are not part of this PR. 108 SymphonyQG arrays and 302 IVF/HNSW arrays from the 0.7 call forms were compared bit for bit, and none differ. The SymphonyQG cases cover L2/IP, 0/4/8 bits, PiPNN and random init, search, search_batch, saved bytes and loaded searches.
  • Samples: the SymphonyQG sample tests run with DeprecationWarning as an error.
  • Docs: mkdocs build --strict passed, and the qg.md snippet was executed with DeprecationWarning as an error.
  • Reproducibility was not claimed beyond what is tested: same-seed builds were byte-identical with 1 and 8 threads in a local check, but the docs promise this only for num_threads=1.

SymqgIndex follows the same train -> add -> search workflow as IvfIndex
and HnswIndex.

- SymqgIndex(dim, *, nbits=32, max_degree=32, ef_construction=200,
  init="pipnn", metric="l2", seed=None). nbits replaces quantization_bits,
  with 32 instead of 0 for raw vectors. dim is checked at construction.
  seed drives the rotation and graph construction; None draws one, which
  index.seed reports.
- train(x) checks x as add would and changes nothing; is_trained is always
  True. add(data, *, num_threads=0) builds the graph once and a second call
  raises, as Faiss's NSG index does. search(queries, k, ef=None) uses
  max(k, 10) for a missing or zero ef and raises smaller values to k.
  ef_construction, init and seed are None after load.
- The 0.8 compatibility layer keeps SymqgIndex(dim, max_degree, metric,
  quantization_bits), quantization_bits=, build(), search_batch() and the
  quantization_bits and is_built properties with DeprecationWarning. The
  deprecated build replaces any graph and, without a user seed, draws a new
  seed each time, as in 0.7.
- HnswIndex.search also accepts ef=None.
- Docs, samples and the migration page use the new API.
@github-actions github-actions Bot added the release:added Release category managed from the PR title label Nov 1, 2026
txtai now reads num_stored_points for its id offset and delete bounds and
num_points for its count, and no longer counts deletes itself. Exercise
those calls, as neuml/txtai 4d5e05f makes them, instead of max_elements.
@gouyt13
gouyt13 merged commit 08bc325 into main Nov 1, 2026
40 checks passed
@gouyt13
gouyt13 deleted the feat/symqg-train-api branch November 1, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:added Release category managed from the PR title

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant