chassisdb
Python bindings for Chassis, an embedded vector index for local semantic search: one file, in your process, no server.
Features
- Zero-copy operations: Direct memory access via ctypes
- NumPy integration: Native support for NumPy arrays
- Type hints: Full type annotations for IDE support
- Thread-safe: Search from many threads while another adds or deletes; writes run one at a time
- Pythonic API: Context managers, properties, and familiar patterns
Installation
pip install chassisdb
Import it as chassisdb. Wheels are built for Python 3.11 and later on Linux (x86-64 and ARM),
macOS on Apple silicon and Windows (x86-64). Elsewhere, or from a
checkout of the repository, pip install . in this directory builds it, which needs Rust.
Development Installation
# Build the library, then install with dev dependencies
cargo build --release -p chassis-ffi
pip install -e ".[dev]"
# Run tests
pytest
# Type checking
mypy chassisdb
# Formatting
black chassisdb tests
ruff check chassisdb tests
Quick Start
import numpy as np
from chassisdb import VectorIndex
# Create or open an index
index = VectorIndex("embeddings.chassis", dimensions=768)
# Add vectors
vectors = np.random.rand(1000, 768).astype(np.float32)
for i, vec in enumerate(vectors):
vector_id = index.add(vec)
print(f"Added vector {vector_id}")
# Flush to disk
index.flush()
# Search
query = np.random.rand(768).astype(np.float32)
results = index.search(query, k=10)
for result in results:
print(f"ID: {result.id}, Distance: {result.distance:.6f}")
# Index info
print(f"Total vectors: {len(index)}")
print(f"Dimensions: {index.dimensions}")
print(f"Empty: {index.is_empty()}")
Advanced Usage
Custom HNSW Parameters
from chassisdb import VectorIndex, IndexOptions
# Configure HNSW parameters
options = IndexOptions(
max_connections=32, # Higher = better recall, more memory
ef_construction=400, # Higher = better index, slower build
ef_search=100, # Higher = better search, slower queries
metric="cosine", # "euclidean" (default) or "cosine"; fixed at creation
precision="half", # "full" (default) or "half": 16-bit floats, half the file; fixed at creation
)
index = VectorIndex("tuned.chassis", dimensions=768, options=options)
Context Manager
with VectorIndex("vectors.chassis", dimensions=128) as index:
index.add([0.1] * 128)
# Flushed and closed here
Leaving the block flushes, unless an exception is leaving it, then closes the index. Closing
without a flush, with close() or by garbage collection, discards what was added or deleted since
the last flush(), as a crash would.
Batch Operations
import numpy as np
vectors = np.random.rand(10000, 768).astype(np.float32)
queries = np.random.rand(100, 768).astype(np.float32)
with VectorIndex("large.chassis", dimensions=768) as index:
# Added and linked on every core
index.add_batch(vectors)
index.flush()
# One call for every query: ids and distances are (100, 10) arrays
ids, distances = index.search_batch(queries, k=10)
NumPy Integration
import numpy as np
# From NumPy array
vec = np.array([0.1, 0.2, 0.3], dtype=np.float32)
index.add(vec)
# From list
index.add([0.1, 0.2, 0.3])
# From tuple
index.add((0.1, 0.2, 0.3))
# All are automatically converted to float32
Error Handling
from chassisdb import VectorIndex, DimensionMismatchError, IndexLockedError
index = VectorIndex("vectors.chassis", dimensions=128)
try:
index.add([0.1] * 64)
except DimensionMismatchError as e:
print(e)
# The vector has 64 components, but this index holds vectors of 128
# help: make the vector with the same model as the index's vectors; this index was created for 128 dimensions
try:
VectorIndex("vectors.chassis", dimensions=128)
except IndexLockedError:
# One writer at a time; a reader searches while it writes.
reader = VectorIndex("vectors.chassis", dimensions=128, read_only=True)
API Reference
VectorIndex
Main class for interacting with Chassis indexes.
Constructor
VectorIndex(path: str | Path, dimensions: int, options: IndexOptions | None = None, read_only: bool = False)
To reopen an existing index, pass the metric, precision and max_connections it was created
with; others raise OptionsMismatchError, which names them. With read_only=True it only
searches, takes no lock, and takes the metric and precision from the file (max_connections has
to match); any number can search while one writer writes, in this process or others.
Methods
-
add(vector: Sequence[float] | ndarray, id: int | None = None) -> int
Add a vector underid(0 to 2**64 - 2;IdInUseErrorif a live vector has it), or else under one past the largest id so far. Returns the id. -
add_batch(vectors: ndarray, ids: Iterable[int] | None = None) -> ndarray
Add a(count, dimensions)array at once, linking it on every core: much faster thanaddin a loop. Returns the ids. All or nothing: if any vector or id is rejected, none is added. -
delete(id: int) -> bool
Delete the vector with this id. Returns whether there was one. Search stops returning it at once; the delete is durable after the nextflush(). To replace a vector, delete it and add the new one under the same id: the next flush saves both or neither. -
get(id: int) -> ndarray | None
The vector with this id, orNoneif no live vector has it. It comes back as the index keeps it: scaled to length 1 in a cosine index, and rounded in half precision. -
id in index
Whether a live vector has this id. -
ids() -> ndarray
The ids of the live vectors, in the order they were added. -
search(query: Sequence[float] | ndarray, k: int = 10, allowed: Iterable[int] | ndarray | None = None, excluded: Iterable[int] | ndarray | None = None) -> List[SearchResult]
Search for k nearest neighbors. Returns sorted list of results. Withallowed, only those ids can be returned, e.g.allowed=[row[0] for row in db.execute("SELECT id FROM docs WHERE owner = ?", (user,))]; when walking the graph would cost more, as when few vectors match, every vector is checked instead and the results are exact. Withexcluded, those ids are never returned, as for "more like this":index.search(index.get(42), k=10, excluded=[42]). -
search_batch(queries: ndarray, k: int = 10) -> tuple[ndarray, ndarray]
Search for the k nearest neighbors of each row of a(count, dimensions)array, in one call: faster thansearchin a loop. Returns(ids, distances), two(count, k)arrays, nearest first; a row with fewer than k results is filled out with id2**64 - 1and distanceinf. -
flush() -> None
Flush changes to disk. Call after batch insertions. -
warm() -> None
Read the index into memory on another thread, so that searches stop waiting for the disk while its file is not in memory yet, as after a reboot. Returns at once; searches go on meanwhile.IndexOptions(warm=True)does the same from the moment the index is opened. -
compact() -> None
Rewrite the index without its deleted vectors and with a rebuilt graph, reclaiming their space. Ids don't change. Takes as long as building the index and needs disk for a second copy. Deletes cost speed, not recall: with half the vectors deleted, searches ran at about 70% of their speed, so compacting oncedeleted_count()nearslen()is a good rule. The file shrinks by whole segments, which double as an index grows, so only when the live vectors fit in fewer of them. -
close() -> None
Close the index and free resources. Adds and deletes since the lastflush()are discarded, as after a crash; leaving awithblock flushes first, unless an exception is leaving it. -
__len__() -> int
Get number of vectors in the index. -
deleted_count() -> int
Deleted vectors the file still holds, untilcompact()removes them. -
is_empty() -> bool
Check if index is empty.
Properties
dimensions: int- Number of dimensions per vectormetric: str-"euclidean"or"cosine", as the index was created; opening withread_only=Trueand explicitoptionsnaming another raisesOptionsMismatchErrorprecision: str-"full"or"half", as the index was createdpath: Path- Path to the index fileoptions: IndexOptions- HNSW configuration
IndexOptions
Configuration for HNSW algorithm.
@dataclass
class IndexOptions:
max_connections: int = 16 # M parameter
ef_construction: int = 200 # Build-time search quality
ef_search: int = 50 # Query-time search quality
metric: str = "euclidean" # or "cosine": 1 - cosine similarity, unit-length storage
precision: str = "full" # or "half": vectors kept as 16-bit floats, half the file and memory
huge_pages: bool = False # Linux: keep the vectors on 2 MB pages (faster search on a large index)
warm: bool = False # read the index into memory in the background from the start
SearchResult
Search result with ID and distance.
@dataclass
class SearchResult:
id: int # Vector ID in index
distance: float # Distance to query (lower = closer)
Exceptions
Each says what was wrong, with the value, and on a line starting help: what to do instead. Each
is a ChassisError and also the built-in exception it is a kind of, so except ValueError
catches a bad argument. The errors reference
lists their causes and fixes.
InvalidArgumentError(ValueError) - an argument out of range: dimensions, an option, an id, a vector with NaN or infinity. A wrong type is a plainTypeError.DimensionMismatchError(ValueError) - a vector or query of other dimensions than the index'sOptionsMismatchError(ValueError) - reopened with another metric, precision ormax_connectionsIndexNotFoundError(FileNotFoundError) - no index at the path, forread_only=TrueNotAnIndexError(ValueError) - the file isn't a Chassis indexIdInUseError(ValueError) - the id is taken; delete it first to replace itIndexLockedError- another writer has the index openReadOnlyError- aread_only=Trueindex asked to writeCorruptIndexError,IndexFullError,InvalidPathError(OSError) - a damaged file, a full index, an operating-system error
Thread Safety
Every method is safe to call from any thread. Searches run concurrently (ctypes releases the GIL
during each call); add(), delete() and flush() take the index's write lock, so they run one
at a time and searches wait for them. Don't call close() while other threads still use the index.
Other processes can search the file while one process writes it: open it with
VectorIndex(path, dimensions, read_only=True). Such an index takes no lock, sees the writer's
newest flush() on every search, runs one search at a time, and raises ReadOnlyError on writes.
A flush becomes visible just before its final fsync, so one that then fails, or is lost to power
loss, may already have been seen.
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor() as pool:
results = list(pool.map(lambda q: index.search(q, k=10), queries))
Performance Tips
-
Batch inserts before flushing:
for vec in many_vectors: index.add(vec) index.flush() # Once at the end
-
Use NumPy arrays:
# Faster (zero-copy) vec = np.array([0.1] * 768, dtype=np.float32) # Slower (creates copy) vec = [0.1] * 768
-
Tune HNSW parameters:
- Increase
ef_constructionfor better index quality - Increase
ef_searchfor better search recall - Increase
max_connectionsfor better graph connectivity
- Increase
-
Use context managers:
with VectorIndex(...) as index: # Automatic cleanup
Library Location
chassisdb looks for libchassis_ffi in this order:
CHASSIS_LIB_PATHenvironment variable- Next to the Python package
../target/release(development)- System library paths
To set a custom location:
export CHASSIS_LIB_PATH=/path/to/lib
python your_script.py
Examples
See the examples/ directory for complete examples:
notes_search.py- Notes in SQLite, their vectors in Chassis: search, "more like this", deletesbasic_usage.py- Simple add and searchbatch_insert.py- Efficient bulk loading
Testing
# Run all tests
pytest
# Run with coverage
pytest --cov=chassisdb --cov-report=html
# Run specific tests
pytest tests/test_index.py -k delete
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Contributing
See CONTRIBUTING.md for development guidelines.
Metadata
Release files for chassisdb 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| chassisdb-0.8.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| chassisdb-0.8.0-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| chassisdb-0.8.0-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| chassisdb-0.8.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
Total release size: 1.7 MB
Release files / chassisdb-0.8.0-py3-none-win_amd64.whl
| Download URL | chassisdb-0.8.0-py3-none-win_amd64.whl |
|---|---|
| Size | 342.4 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
562b0a1e79ed4fa61ca7291e507bce635c0be9d80dbe4d565c8cd99f23919d5b
|
|
BLAKE2b-256 checksum How to use checksums |
0c852367e34e6e9e289f7e5f3fb7e073bf92c4d8369ba4bca1474de5a11a1f24
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / chassisdb-0.8.0-py3-none-manylinux_2_28_x86_64.whl
| Download URL | chassisdb-0.8.0-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 491.5 kB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
f3f1f469f6ddb0ab258b95a50d8e550c77d06e2be731867afa3033ef6a7fd2bd
|
|
BLAKE2b-256 checksum How to use checksums |
417be2de61cdb12bbf225a47ecab2d31c2c8d2228ee58254d9d01c97be8280b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / chassisdb-0.8.0-py3-none-manylinux_2_28_aarch64.whl
| Download URL | chassisdb-0.8.0-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 452.5 kB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
9fcbae1e602695bdc42a348b485ca6ed327940496f3529fda5ec3bb6232e4ecd
|
|
BLAKE2b-256 checksum How to use checksums |
79a6d35fb6045562889aa48e632643df53dd16de5ce310b8ef53fe5687f8d07d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / chassisdb-0.8.0-py3-none-macosx_11_0_arm64.whl
| Download URL | chassisdb-0.8.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 408.4 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5622dfde3fd02b4f185ea6acb370b9aa03c6a03f1436b33cc3f3c5258c614341
|
|
BLAKE2b-256 checksum How to use checksums |
f4e3adaf8568c30678ad63d749784ef68329678334380446f66ee7ea8dba2caf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log