For Coding Agents¶
Dense, prescriptive reference for AI agents working on or integrating with iscc-usearch.
Tables and code over prose. Terminology matches the codebase exactly.
Architecture map¶
File layout¶
| File | Contains |
|---|---|
src/iscc_usearch/__init__.py |
Public re-exports (__all__), package docstring |
src/iscc_usearch/index.py |
Index — USearch wrapper with upsert support (internal base) |
src/iscc_usearch/nphd.py |
NphdIndex, pad_vectors, unpad_vectors, type aliases |
src/iscc_usearch/sharded.py |
ShardedIndex, ShardedIndex128, _UuidKeyMixin, lazy iterators, bloom integration |
src/iscc_usearch/sharded_nphd.py |
ShardedNphdIndex, ShardedNphdIndex128, ShardedNphdIndexedVectors |
src/iscc_usearch/bloom.py |
ScalableBloomFilter — chained bloom filter with persistence |
src/iscc_usearch/utils.py |
timer, atomic_write, durable_write |
Class hierarchy¶
usearch.index.Index
└── Index (index.py — adds upsert, internal base)
└── NphdIndex (nphd.py — variable-length + NPHD metric)
ShardedIndex (sharded.py — composition-based sharding, uint64 keys)
├── ShardedIndex128 (sharded.py — 128-bit UUID keys via _UuidKeyMixin)
└── ShardedNphdIndex (sharded_nphd.py — variable-length + NPHD metric)
└── ShardedNphdIndex128 (sharded_nphd.py — 128-bit UUID keys via _UuidKeyMixin)
ScalableBloomFilter (bloom.py — standalone, used by ShardedIndex)
Import dependency flow¶
Public API exports (__all__)¶
__all__ = [
"NphdIndex",
"ShardedIndex",
"ShardedIndex128",
"ShardedNphdIndex",
"ShardedNphdIndex128",
"ScalableBloomFilter",
"timer",
]
Index (from index.py) is not exported — it is an internal base class.
Decision dispatch¶
Which index class?¶
| Use case | Class | Key type |
|---|---|---|
| Variable-length ISCC codes, fits in RAM | NphdIndex |
uint64 |
| Fixed-length vectors, large scale | ShardedIndex |
uint64 |
| Fixed-length vectors, 128-bit UUID keys | ShardedIndex128 |
bytes(16) |
| Variable-length ISCC codes, large scale (production) | ShardedNphdIndex |
uint64 |
| Variable-length ISCC codes, large scale, 128-bit keys | ShardedNphdIndex128 |
bytes(16) |
Which persistence method?¶
| Method | Reads file into RAM | Memory-mapped | Creates new object | Resets dirty counter |
|---|---|---|---|---|
load(path) |
Yes | No | No (mutates self) | Yes |
view(path) |
No | Yes (read-only) | No (mutates self) | Yes |
restore(path) |
Yes (default) | Optional (view=True) |
Yes (static method) | Yes |
copy() |
N/A | N/A | Yes (deep copy) | No (new index, dirty=0) |
load/view/restore are on NphdIndex. Sharded indexes handle persistence internally
via save()/constructor — they auto-load/view shards from the path directory.
Which add method?¶
| Method | Existing key behavior | Duplicate keys in batch | keys=None | Requires multi=False |
|---|---|---|---|---|
add() |
Skips duplicates in multi=False; appends in multi=True |
Skipped in multi=False; all added in multi=True |
Yes | No |
upsert() |
Remove old + add new (last-write-wins) | Last occurrence kept | No | Yes |
add_once() |
Silently skipped across all shards | First occurrence kept | No | No |
For sharded indexes, add() only checks duplicate keys within the active shard. It does not scan
view shards. Use add_once() when keys may already exist across shard boundaries and first-write-wins
semantics are required.
Which read method?¶
| Method | Returns | Bloom fast-reject | Tombstone-aware | Registers rotations |
|---|---|---|---|---|
search() |
Matches / BatchMatches |
No | Yes | No |
get() |
Vector(s) or None |
Yes | Yes | No |
contains() |
bool / NDArray[bool] |
Yes | Yes | No |
count() |
int / NDArray[uint64] |
Yes | Yes | No |
None of these register completed background rotations. Call drain_rotations() before reads
that require visibility of recently rotated shards.
Unsupported operations (ShardedIndex)¶
These raise NotImplementedError: rename(), join(), cluster(), pairwise_distance(),
copy(), clear().
Constraints and invariants¶
Concurrency rules¶
- Single-process only. No file locking on
.usearchfiles. - Multiple processes against the same index → data corruption.
- Within one process:
async/awaitis safe for concurrent reads. - Threads require an external lock — index objects are not thread-safe.
- Shard rotation is not atomic — concurrent writes must be serialized.
background_rotation=Trueuses a single background thread for shard serialization. The detached shard has exclusive ownership in the background thread. Pending rotations serialize via aThreadPoolExecutor(max_workers=1).- Key-dependent writes (
upsert,add_once,remove,save,compact,reset) drain pending rotations first.add()registers completed rotations non-blocking.
Vector constraints¶
| Constraint | Value | Enforced in |
|---|---|---|
max_dim upper bound |
256 (bits) | NphdIndex.__init__, ShardedNphdIndex.__init__ |
max_dim alignment |
Multiple of 8 | NphdIndex.__init__ |
| Vector dtype | np.uint8 (packed bits) |
pad_vectors |
| Scalar kind | ScalarKind.B1 |
NphdIndex.__init__ |
| Internal ndim | max_dim + 8 (length byte = 8 bits) |
NphdIndex.__init__ |
| Truncation | Vectors longer than max_bytes are silently truncated to max_bytes |
pad_vectors |
| Length byte | padded[i, 0] = original vector length in bytes |
pad_vectors / unpad_vectors |
Key constraints¶
| Variant | Single key type | Batch key type | Tombstone canonical type |
|---|---|---|---|
| uint64 (default) | int |
np.ndarray[np.uint64] |
int |
| 128-bit UUID | bytes (len 16) |
np.ndarray[dtype='V16'] |
bytes |
- 128-bit variants:
keys=None(auto-generation) raisesValueError. _bloom_key()/_tombstone_key()convert raw keys to canonical Python types.
Dirty counter semantics¶
| Operation | Effect on dirty |
|---|---|
add() |
+= count_added |
remove() |
+= count_of_existing_keys_removed |
upsert() |
Combines remove + add effects |
save() |
Reset to 0 |
load() / view() |
Reset to 0 |
reset() |
Reset to 0 |
compact() |
Reset to 0 (calls save() internally) |
| Shard rotation | Not reset — survives rotation |
close() |
Reset to 0 via save (if non-empty or dirty) |
read_only=True |
Always returns 0 |
Bloom filter behavior¶
- Enabled by default (
bloom_filter=TrueonShardedIndex). - Updated on
add()— keys are added viaadd_batch(). - Never updated on
remove()— bloom filter is append-only. contains()/get()/remove()use bloom for fast rejection of non-existent keys.- False positives are expected (probabilistic). False negatives do not occur.
rebuild_bloom()creates a fresh filter from all existing keys.- Automatically rebuilt on load if the file is missing or corrupt (writable indexes only).
compact()callsrebuild_bloom()internally.- Bloom filter file:
bloom.isbfin the shard directory. - Always loaded from disk if file exists, regardless of
_use_bloomsetting.
Tombstone lifecycle¶
remove(key) → key added to _tombstones (in-memory set)
↓
save() → bloom persisted, then shard, then _tombstones as tombstones.npy
↓
compact() → view shards rebuilt excluding tombstoned entries
↓
_tombstones.clear(), tombstones.npy deleted
_needs_compactflag: set whenadd()clears a tombstone (creating cross-shard duplicates).tombstones.npyfile existence signals_needs_compact=Trueon next load.- Tombstones are never written to bloom filter — bloom has no "remove" operation.
- Tombstones are persisted after shard data so that tombstone removals only become visible once the shard they depend on is durable.
Side effects catalog¶
| Method | Disk writes | _dirty |
_tombstones |
Bloom update | Shard rotation |
|---|---|---|---|---|---|
add() |
None (until rotation; rotation durably writes bloom, shard, tombstones) | += count_added |
Clears matching tombstones | add_batch() |
Yes (if size exceeded) |
remove() |
None | += N (existing only) |
Adds view-shard keys | None | No |
upsert() |
None | Via remove + add | Via remove + add | Via add | Via add |
add_once() |
None | Via add (new keys only) | None | Via add | Via add |
save() |
.usearch, bloom.isbf, tombstones.npy |
Reset to 0 | Persisted | Persisted | No |
load() |
None | Reset to 0 | Loaded from .npy |
Loaded from .isbf |
No |
view() |
None | Reset to 0 | N/A (NphdIndex) | N/A | No |
reset() |
None | Reset to 0 | Cleared | Cleared | No |
compact() |
Rebuilds .usearch, bloom.isbf, deletes tombstones.npy |
Reset to 0 | Cleared | Rebuilt | No |
search() |
None | No change | No change | None | No |
get() |
None | No change | No change | None | No |
contains() |
None | No change | No change | None | No |
count() |
None | No change | No change | None | No |
rebuild_bloom() |
bloom.isbf (if save=True) |
No change | No change | Rebuilt from scratch | No |
drain_rotations() |
None normally; retries failed rotations (writes shard + tombstones) | No change | No change | None | No |
close() |
Drains, saves if non-empty or dirty, releases mmap/executor | N/A (closed) | Persisted via save | Persisted via save | No |
Task recipes¶
Add vectors to a sharded index¶
from iscc_usearch import ShardedNphdIndex
import numpy as np
idx = ShardedNphdIndex(max_dim=256, path="my_index")
# Single vector
key = 1
vec = np.random.randint(0, 256, size=32, dtype=np.uint8)
idx.add(key, vec)
# Batch of uniform-length vectors
keys = np.arange(100, 200, dtype=np.uint64)
vecs = np.random.randint(0, 256, size=(100, 32), dtype=np.uint8)
idx.add(keys, vecs)
# Batch of variable-length vectors
keys2 = np.arange(200, 205, dtype=np.uint64)
vecs2 = [np.random.randint(0, 256, size=s, dtype=np.uint8) for s in [8, 16, 24, 32, 8]]
idx.add(keys2, vecs2)
idx.save()
Search with mixed bit-lengths¶
# Query vectors can differ in length from stored vectors.
# NPHD metric normalizes by the shorter prefix.
q8 = np.random.randint(0, 256, size=8, dtype=np.uint8) # 64-bit query
q32 = np.random.randint(0, 256, size=32, dtype=np.uint8) # 256-bit query
matches_8 = idx.search(q8, count=10) # compares prefix of stored vectors
matches_32 = idx.search(q32, count=10) # full comparison with 256-bit vectors
# Batch search with variable-length queries
queries = [q8, q32]
batch_matches = idx.search(queries, count=5)
Flush based on dirty counter¶
Upsert then compact¶
# Upsert replaces existing vectors (remove + add).
# After many upserts, view shards accumulate stale entries.
idx.upsert(key, new_vec)
# Compact rebuilds view shards to reclaim space.
# Call periodically, not after every upsert.
if idx.tombstone_count > 1000:
removed = idx.compact()
Rebuild bloom filter for existing index¶
# For indexes created without bloom filter, or after corruption:
idx = ShardedNphdIndex(max_dim=256, path="existing_index")
count = idx.rebuild_bloom() # Scans all shards, saves bloom.isbf
Background rotation with context manager¶
from iscc_usearch import ShardedNphdIndex
with ShardedNphdIndex(
max_dim=256,
path="my_index",
background_rotation=True,
max_pending_rotations=2,
) as idx:
for key, vec in data_stream:
idx.add(key, vec)
# close() called automatically: drains rotations, saves, releases resources
Reopen existing index (max_dim auto-detected)¶
# max_dim is auto-detected from existing shard metadata when omitted
idx = ShardedNphdIndex(path="existing_index")
assert idx.max_dim == 256 # read from shard_*.usearch files
Iterate keys and vectors lazily¶
# keys and vectors are lazy iterators — no full materialization
for key in idx.keys:
print(key)
for vec in idx.vectors:
print(vec.shape) # variable-length, unpadded
# Indexing, slicing, len(), and numpy conversion supported
first_key = idx.keys[0]
last_vec = idx.vectors[-1]
all_keys = np.asarray(idx.keys)
Inspect index with stats()¶
info = idx.stats()
# Returns: total_vectors, dimensions, metric, dtype, connectivity,
# view_shards, active_shard_vectors, shard_size, dirty, tombstones,
# bloom_filter, memory_usage, path, read_only
Open existing index read-only¶
# All shards memory-mapped, no active shard. Write operations raise RuntimeError.
idx = ShardedNphdIndex(path="existing_index", read_only=True)
matches = idx.search(query_vec)
# idx.add(...) # RuntimeError: index is read-only
Change playbook¶
If modifying padding logic¶
Files: nphd.py (pad_vectors, unpad_vectors)
Also update:
sharded_nphd.py—add(),search(),get(),vectorsiterator (all callpad_vectors/unpad_vectors)tests/test_nphd.py— padding round-trip testsdocs/explanation/architecture.md— "Length-prefixed padding" section
If modifying shard rotation¶
Files: sharded.py (_rotate_shard, _rotate_shard_sync, _rotate_shard_background,
_background_rotate_task, _schedule_next_size_check, _adds_until_size_check)
Also update:
sharded.py—add()(calls rotation),_load_existing()(loads rotated shards),drain_rotations(),close(),_enforce_pending_limit(),_register_completed_rotations()sharded_nphd.py—_create_shard(),_restore_shard()(shard creation/restore hooks)tests/test_sharded_background_rotation.py— background rotation testsdocs/explanation/sharding-design.mddocs/howto/sharding.md
If modifying bloom filter format¶
Files: bloom.py (save, load, MAGIC, VERSION)
Also update:
sharded.py—_load_bloom_if_exists(),save(),rebuild_bloom(),compact()- Bump
VERSIONconstant inbloom.pyif format changes tests/test_bloom.pydocs/howto/bloom-filters.md
If modifying key handling¶
Files: sharded.py (_UuidKeyMixin, key hook methods on ShardedIndex)
Also update:
sharded_nphd.py—ShardedNphdIndex128inherits_UuidKeyMixinindex.py—Index.upsert()has its own key normalizationtests/test_sharded.py,tests/test_sharded_nphd.py— key-related testsdocs/howto/uuid-keys.md
If adding a new index class¶
Also update:
src/iscc_usearch/__init__.py— add to__all__and importdocs/reference/api.md— add mkdocstrings directive (or handled viagen_llms_full.py)scripts/gen_llms_full.py— add to_API_CLASSESlistdocs/explanation/architecture.md— update class hierarchy and "Choosing an index class" table- This page — update architecture map, class hierarchy, and decision dispatch tables
- Add test file in
tests/
Common mistakes¶
NEVER open the same index directory from multiple processes.
Use a single process with async/await for concurrent access.
NEVER mutate an index opened with view(). Viewed indexes are memory-mapped and read-only.
Use load() or restore() for mutable access. Sharded indexes use read_only=True for the
same purpose.
NEVER pass ndim, metric, or dtype to NphdIndex. These are computed from max_dim.
Passing them raises TypeError.
NEVER skip compact() after sustained upsert/remove workloads. View shards accumulate
tombstoned and duplicate entries that waste disk space and slow searches.
NEVER assume len(sharded_index) is exact after upserts without compaction. Cross-shard
duplicates cause size to overcount. Call compact() for an exact count.
ALWAYS call close() or use a context manager before process exit. close() drains
pending background rotations, saves unsaved state, and releases mmap/executor resources.
save() is sufficient for durability (it also drains pending rotations) but does not
release mmap or executor resources. Unsaved data in the active shard is lost.
ALWAYS use np.uint8 dtype for vectors. Other dtypes cause silent data corruption in
the NPHD metric computation.
ALWAYS pass explicit keys to upsert() and add_once(). Both raise ValueError on
keys=None. For 128-bit variants, add() also requires explicit keys.
ALWAYS serialize concurrent writes within a single process (e.g., via asyncio.Lock).
Shard rotation during concurrent writes can cause data loss.