Design Goals
iQDB is engineered against the Rust Efficiency & Performance Standards (REPS). Every architectural decision is graded against the same hard constraints:
- Embedded by default — no daemon, no socket, no separate process. The library opens a path and gets out of the way.
- Sub-millisecond queries — exact and approximate search paths are budgeted in microseconds, not milliseconds, with the hot path measured under Criterion every release.
- Enum-dispatched hot paths — the index seam dispatches through a closed
match, neverdyn, so the query loop sees a concrete index with no virtual indirection. - Allocation-aware steady state — vector payloads are shared as
Arc<[f32]>between the authoritative store and the derived index, so a vector that lives in both costs one allocation, not two. - Pluggable indices — flat, IVF, and HNSW share the
iqdb-indextrait surface so callers swap strategies throughIqdbConfigwithout touching their query code. Flat is the exact recall ground truth the approximate indices are measured against. - Crash-safe writes — the durable path uses write-ahead logging and atomic snapshot replacement. A pulled power cord must never corrupt the database.
- Tier-1 cross-platform — Linux (x86_64, aarch64), macOS (x86_64, Apple Silicon), Windows (x86_64) all compile and pass the full test suite on every commit.
Status & Roadmap
iQDB ships milestone-by-milestone. Each tag below corresponds to a published release; everything above the current line is shipped, everything below is planned.
| Milestone | Status | Surface delivered |
|---|---|---|
v0.1.0 — scaffolding |
shipped | Crate scaffolding, lifecycle handle, Error type, CI matrix on all three Tier-1 platforms. |
v0.2.0 — vector primitives |
shipped | Validated vectors, distance metrics, typed payloads, in-memory store with thread-safe CRUD. |
v0.3.0 — search |
shipped | Flat top-k search, filters, batch variants, NaN-aware ranking, property-based tests. |
v0.4.0 — durable storage |
shipped | Directory-backed store, snapshot + WAL, cross-platform sync, atomic compaction, corrupt-tail recovery. |
v0.5.0 — family composition + approximate indices |
shipped | Re-platformed onto the iqdb crate family. Re-exported vocabulary (Vector, VectorId, Metadata, Value, Hit, Filter, DistanceMetric). Selectable index — exact Flat, plus Hnsw and Ivf through IqdbConfig. Durable storage via iqdb-persist; optional result cache via iqdb-cache. Recall validated against the flat oracle. |
v0.6.0 — async surface |
shipped | async-feature-gated AsyncIqdb: a Tokio adapter that offloads each blocking call via spawn_blocking. Additive; the synchronous API and default build are unchanged. |
v0.7.0 — durability tuning (alpha) |
current | IqdbConfig::fsync (WAL fsync cadence) and IqdbConfig::compression (snapshot zstd / lz4), wiring the compression features through. Additive; defaults unchanged. |
v0.8.x — beta · v0.9.x — RC |
planned | Broader testing, final benchmarks, doc polish. |
v1.0.0 — API freeze |
planned | Frozen public API and on-disk format. SemVer guarantees. Full benchmark suite. |
The per-release detail — what was added, what changed, and what was verified — lives in the CHANGELOG and the per-version notes under docs/release/.
Installation
Add to your Cargo.toml:
[]
= "0.5"
Optional features (all additive):
[]
= { = "0.5", = ["serde", "parallel", "zstd"] }
iQDB compiles on stable Rust 1.87 and newer. As of 0.5.0 it composes the published iqdb family crates (iqdb-types, iqdb-index, iqdb-flat, iqdb-hnsw, iqdb-ivf, iqdb-build, iqdb-persist, iqdb-cache, and their transitive dependencies); it is no longer a zero-dependency build.
Quick Start
A database fixes its dimensionality and distance metric at open time, then exposes a small surface: upsert / get / delete for records, search / search_with for queries.
use ;
Filtered and batch search
Filters are declarative Filter expressions evaluated against each record's Metadata. On the exact flat index the filter is applied before scoring, so the result is exact.
use ;
Choosing an index
Tier 1 (open_in_memory / open) defaults to the exact flat index. Tier 2 (open_in_memory_with / open_with) takes an IqdbConfig that selects an approximate index and tunes it, and can attach a result cache.
use ;
On the approximate indices the metadata filter is applied after graph / cluster traversal, so a highly selective filter can return fewer than k hits — widen the search (HNSW filter_widen, IVF n_probes) when that matters. IVF is trained lazily from the stored vectors on the first search; after many writes, Iqdb::optimize retrains its centroids.
Durable, file-backed storage
Iqdb::open(path, dim, metric) opens or creates a durable database. The path is the snapshot file; a write-ahead log lives beside it. Acknowledged writes survive a crash; reopening replays the log onto the snapshot.
use ;
A reopen whose requested dim / metric disagrees with the stored database fails with Error::Config. The stored index kind is part of the database identity and is restored from the snapshot regardless of the kind requested on reopen.
By default every acknowledged write is fsynced and the snapshot is uncompressed. Trade durability for throughput, or shrink the snapshot, through IqdbConfig:
use ;
use Duration;
#
Async (the async feature)
The family is synchronous by design, so the async surface is a thin Tokio adapter: AsyncIqdb holds an Arc<Iqdb> and runs each blocking call on Tokio's blocking pool via spawn_blocking, so awaiting a search or a write never stalls the executor. It is Clone + Send + Sync. Enable the async feature and bring your own runtime.
use ;
async
API Overview
The full API reference lives at docs/API.md; the rustdoc at docs.rs/iqdb carries the same information in browseable form. The public surface:
Iqdb— the database handle.Iqdb::open_in_memory(dim, metric)— an ephemeral, exact-flat database.Iqdb::open_in_memory_with(config)— an in-memory database from a fullIqdbConfig.Iqdb::open(path, dim, metric)/Iqdb::open_with(path, config)— a durable, file-backed database.Iqdb::upsert(id, vector, metadata)— insert or replace. Rejects a wrong-dimension vector at the boundary.Iqdb::get(id)— look up the stored vector and metadata.Ok(None)when absent.Iqdb::delete(id)— remove by id; returns whether it was present.Iqdb::len()/Iqdb::is_empty()— cardinality.Iqdb::search(query, k)— top-ksimilarity search under the database metric.Iqdb::search_with(query, k, filter)— top-krestricted by a metadataFilter.Iqdb::search_batch(...)/search_batch_with(...)— order-preserving batch variants.Iqdb::optimize()— rebuild / retrain the approximate index over the current vectors.Iqdb::cache_stats()— cache hit/miss statistics, when a cache is configured.Iqdb::flush()— compact a file-backed store; no-op in memory.Iqdb::close(self)— final compaction, then release.
AsyncIqdb— (asyncfeature) a Tokio adapter mirroring theIqdbsurface; offloads each blocking call viaspawn_blocking.Clone+Send+Sync.IqdbConfig— fluent construction config:dim,metric, anIndexKind, and an optionalCacheConfig.IndexKind—Flat(exact),Hnsw(HnswConfig),Ivf(IvfConfig).HnswConfig/IvfConfig/CacheConfig— re-exported tuning structs for the approximate indices and the cache.Vector/VectorId/Metadata/Value/Hit/Filter/DistanceMetric— the shared vocabulary, re-exported fromiqdb-types.Error/Result<T>— the unified error type (#[non_exhaustive]) and itsResultalias.
Error variants
| Variant | Meaning | Recovery |
|---|---|---|
Error::Index(IqdbError) |
A failure from the index / vocabulary layer — dimension mismatch, absent id, invalid metric for the chosen index, malformed filter. | Inspect the wrapped [iqdb_types::IqdbError] kind and fix the construction or query site. |
Error::Persist(PersistError) |
A failure from the durable-storage layer — snapshot / WAL I/O, a corrupt or truncated file, a checksum mismatch, or an unsupported compression feature. | Inspect the wrapped [iqdb_persist::PersistError]. A corrupt WAL tail is truncated automatically; a corrupt snapshot fails the open. |
Error::Config(&'static str) |
A handle-level consistency check failed — most often a reopen whose dim / metric does not match the stored database. |
Open with the values the database was created with. |
The enum is #[non_exhaustive]; always include a _ arm in a match.
Examples
Self-contained examples live in examples/. Run them with cargo run --example <name>.
basic— open, upsert, get, search, delete.examples/basic.rsin_memory_store— metadata, replace-on-upsert, and a metadata-filtered search.examples/in_memory_store.rssearch— top-k, batch, and the effect of the distance metric.examples/search.rspersistence— three sessions against one durable file, showing data survives reopen.examples/persistence.rsindex_selection— flat vs HNSW vs IVF throughIqdbConfig, plus a cache andoptimize.examples/index_selection.rsasync_search(asyncfeature) — concurrent searches fanned out across Tokio tasks.examples/async_search.rs
Benchmarks
A Criterion harness lives in benches/search.rs:
flat/search_dim64_n1000_k10/hnsw/search_dim64_n1000_k10— top-kquery throughput on the exact and approximate paths over 1 000 vectors at dim 64.flat/upsert_dim64— write throughput building a fresh database.
Criterion writes reports to target/criterion/. A regression beyond the REPS threshold (5% on a tracked metric) blocks a release.
Testing
Every public path has happy / error / edge-case coverage:
- Unit tests live in
#[cfg(test)] mod testsblocks inside each source file. - Integration tests live in
tests/:tests/persistence.rs— durable lifecycle: open / upsert / close / reopen, delete and metadata persistence, WAL replay without close, dim/metric-mismatch rejection, IVF round-trip, multi-session accumulation.tests/properties.rs—proptest-driven invariants: flat ranking (sorted, bounded, unique) and the durable round-trip preserving arbitrary record sets.tests/recall.rs— recall@k of HNSW and IVF measured against the exact flat oracle on deterministic synthetic data.
- Doc tests run as part of
cargo testand validate every# Examplesblock.
RUSTDOCFLAGS="-D warnings"
RUSTDOCFLAGS="-D warnings"
Cross-Platform Support
Tier 1 targets — every commit is built and tested on:
- Linux (
x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu) - macOS (
x86_64-apple-darwin,aarch64-apple-darwin) - Windows (
x86_64-pc-windows-msvc)
Durable storage is provided by iqdb-persist, which takes the strongest power-loss sync each platform offers and replaces snapshots atomically. The on-disk format is little-endian on every platform, so a database written on one architecture reads back identically on another.
Configuration
Feature flags
Feature flags are strictly additive (per REPS) — enabling any combination never removes or weakens existing functionality.
| Feature | Default | Description |
|---|---|---|
serde |
off | Derive Serialize / Deserialize on the public data types (forwards to the family serde features). |
parallel |
off | Rayon-backed parallel distance scan on the flat index (forwards to iqdb-flat). |
zstd |
off | Zstandard snapshot compression (forwards to iqdb-persist). |
lz4 |
off | LZ4 snapshot compression (forwards to iqdb-persist). |
async |
off | Tokio-driven AsyncIqdb mirror of the public API. Pulls tokio (only the rt feature). |
= { = "0.5", = ["serde"] }
Runtime configuration
Iqdb::open_in_memory(dim, metric) and Iqdb::open(path, dim, metric) cover the common case. Index selection, tuning, and caching are configured through the fluent IqdbConfig passed to the _with constructors.
Architecture
The crate is the integration layer over the iqdb family; each module owns one concern:
src/lib.rs— crate root, lint profile, vocabulary re-exports.src/handle.rs— theIqdbhandle and itsRwLock-guarded in-memory / file-backed storage seam.src/config.rs— the fluentIqdbConfigand theIndexKindunion.src/error.rs— the unifiedErrorwrapping the family error vocabularies.src/engine/mod.rs—IqdbCore, the owned engine that implements theiqdb-indexandiqdb-persisttraits over an authoritative row store plus a derived index.src/engine/store.rs— the authoritative, insertion-ordered row store (the single source of truth forlenand rebuilds).src/engine/index.rs—AnyIndex, the closed enum overFlatIndex/HnswIndex/IvfIndexwith the IVF training hooks.src/engine/codec.rs— the little-endian on-disk payload codec inside theiqdb-persistframe.
Compile-time guarantees
The crate root enables the strict REPS lint profile in src/lib.rs:
#![deny(warnings)]
#![deny(missing_docs)]
#![deny(unsafe_op_in_unsafe_fn)]
#![deny(unused_must_use)]
#![deny(unused_results)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::print_stdout)]
#![deny(clippy::print_stderr)]
#![deny(clippy::dbg_macro)]
#![deny(clippy::unreachable)]
#![deny(clippy::undocumented_unsafe_blocks)]
Test modules locally relax the unwrap_used / expect_used lints — the strict profile is for production library code, not assertion scaffolding inside #[cfg(test)] blocks. The crate contains no unsafe code.
Contributing
Pull requests are welcome. Before opening one, please make sure the full CI gate passes locally:
RUSTDOCFLAGS="-D warnings"
RUSTDOCFLAGS="-D warnings"
Every contribution is expected to honour the standards in REPS.md — performance, security, error handling, testing, documentation, and dependency hygiene are all enforced as merge gates, not afterthoughts. Commit messages are imperative, lowercase, and scoped to a single logical change.