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.
- Lock-free hot paths — atomic reads on the query path; writers do not contend with concurrent readers.
- Allocation-free steady state — buffers are pooled and reused. Per-query allocation is a design defect.
- Cache-aware layout — vectors land contiguous in memory and on disk; index nodes are padded to the cache line.
- Pluggable indices — flat, IVF, and HNSW share a common trait surface so callers can swap strategies without touching their query code.
- Pluggable storage — durable file-backed storage, transient in-memory storage, and the org-default
storage-iosubstrate are all addressable behind the same handle. - Crash-safe writes — the durable path uses write-ahead logging and atomic file replacement. A pulled power cord must never corrupt the index.
- 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 (open / open_in_memory / flush / close), Error type, integration test, criterion harness, CI matrix on all three Tier-1 platforms. |
v0.2.0 — vector primitives |
shipped | Vector (validated f32 embeddings), DistanceMetric (L2 / Cosine / Dot), Payload & PayloadValue (typed metadata), RecordId, Record. In-memory store with thread-safe upsert / get / delete / len / is_empty. Optional serde feature. |
v0.3.0 — search |
shipped | Iqdb::search / search_with / search_batch / search_batch_with over the flat index. SearchResult { id, score, payload }. Monomorphic predicate filters. NaN-aware ranking with id tie-break. Property-based ranking tests via proptest. docs/API.md published. |
v0.4.0 — durable storage |
current | Directory-backed Iqdb::open(path). Write-ahead log + snapshot. Cross-platform full_sync (F_FULLFSYNC on macOS, fsync(2) on other Unix, FlushFileBuffers on Windows). Atomic-replace snapshot compaction on close. Recovery handles corrupt WAL tails by truncating to last known-good offset. New Error::Corrupt variant. |
v0.5.0 — approximate indices |
planned | IVF and HNSW indices behind the same trait the flat index implements. Build-time index selection via the builder. |
v0.6.0 — async surface |
planned | async-feature-gated mirror of the public API. Driven by Tokio. Cancellation-safe. |
v0.7.0 — collections |
planned | Named collections / namespaces with per-collection metric and dimensionality. Collection-scoped iteration. |
v1.0.0 — API freeze |
planned | Frozen public API. SemVer guarantees. Full benchmark suite. Migration guide from 0.x. |
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.4"
Enable the optional serde feature to derive Serialize / Deserialize on every public data type:
[]
= { = "0.4", = ["serde"] }
iQDB compiles on stable Rust 1.87 and newer. The default build pulls one runtime dependency on Unix (libc, used exclusively for the macOS F_FULLFSYNC call) and zero on Windows; the serde feature additionally pulls serde itself.
Quick Start
The 0.4.0 surface exposes the full embedded-DB lifecycle: typed vector primitives, the in-memory store, exact top-k similarity search, and directory-backed durable storage. Approximate indices (IVF, HNSW) in v0.5.0 will sit alongside the flat kernel rather than replacing it — exact search remains the correctness baseline.
use ;
Filtered and batch search
Filters are generic — the predicate monomorphises into the search loop, so there is no per-record dynamic dispatch. The smaller-is-closer convention holds across all three metrics; Dot returns −(a · b) so a single ordering rule covers L2, Cosine, and Dot.
use ;
Directory-backed durable store
Iqdb::open(path) opens or creates a directory-backed database. The directory contains a snapshot file and a write-ahead log; records survive process restart. flush syncs the WAL to durable storage; close runs a compaction (snapshot rewrite + WAL truncate) so the next open is a single-file load with no replay.
use ;
A corrupt WAL tail (from a prior crash) is truncated to the last known-good offset on open. A corrupt snapshot surfaces as Error::Corrupt { reason } — recovery from a checkpoint chain is a future-milestone concern.
API Overview
The full API reference lives at docs/API.md; the rustdoc-generated docs at docs.rs/iqdb carry the same information in browseable form. The currently-stable items are:
Iqdb— the top-level database handle.Iqdb::open(path)— open or create a directory-backed durable database (snapshot + WAL). Replays the WAL on top of the snapshot during recovery and truncates corrupt tails.Iqdb::open_in_memory()— open an ephemeral instance backed entirely by RAM.Iqdb::upsert(record)— insert or replace a record. Idempotent.Iqdb::get(id)— look up by id. ReturnsOk(None)when absent.Iqdb::delete(id)— remove by id. Returns whether the id was present.Iqdb::len()/Iqdb::is_empty()— store cardinality.Iqdb::search(query, k, metric)— exact top-ksimilarity search, no filter.Iqdb::search_with(query, k, metric, filter)— top-kwith a per-record predicate. The filter monomorphises into the scan loop; no per-record dynamic dispatch.Iqdb::search_batch(queries, k, metric)/search_batch_with(...)— sequential batch variants. Preserves input order.Iqdb::flush()— sync the WAL to durable storage.F_FULLFSYNCon macOS,fsync(2)on other Unix,FlushFileBufferson Windows. No-op for the in-memory backend.Iqdb::close(self)— close the handle. For the file backend, runs a compaction (snapshot rewrite + WAL truncate).
Vector— owned, contiguous, validated f32 embedding.Vector::new(Vec<f32>)/Vector::from_slice(&[f32])validate at the system boundary (no empty vectors, noNaN, no infinity);as_slice/dim/norm/norm_squaredare non-allocating.DistanceMetric—L2,Cosine,Dot.metric.distance(a, b)returns aResult<f32>under the smaller-is-closer convention; dimensional homogeneity is enforced.Payload— typedBTreeMap<String, PayloadValue>for metadata. Deterministic iteration order makes payloads stable acrossserderound-trips and test assertions.PayloadValue—Null/Bool/Int/Float/Text/Bytes/Array/ nestedObject.From<T>conversions cover the primitives.RecordId— transparent newtype aroundu64. Cheap to copy, hash, and compare.Record—(id, vector, optional payload)aggregate.Record::new/Record::with_payloadare the two constructors;into_partsdecomposes without a clone.SearchResult—{ id, score, payload }returned by the search methods. Sorted ascending byscore; ties broken onid;NaNscores sort to the tail.Error— the unified error type.#[non_exhaustive].Result<T>— alias forcore::result::Result<T, Error>.
Error variants
| Variant | Meaning | Recovery |
|---|---|---|
Error::Io(std::io::Error) |
A lower-level I/O failure occurred — disk full, permission denied, missing path, etc. | Inspect the wrapped ErrorKind. Retry transient errors; surface permanent ones. |
Error::InvalidConfig(&'static str) |
Configuration supplied at open time was invalid (e.g., the path exists but is not a directory). | Programmer error — fix the construction site. |
Error::InvalidVector { reason } |
A vector failed boundary validation — empty, or contains NaN / ±∞. |
Sanitise the input at the producer side; Vector::new rejects bad data before it enters the store. |
Error::DimensionMismatch { left, right } |
A distance-metric or store operation combined two vectors of different dimensionality. | Enforce a homogeneous schema at the producer side or surface a typed error to the caller. |
Error::Corrupt { reason } |
On-disk data failed an integrity check during recovery (bad magic, unknown version, CRC mismatch, truncated frame). | Surfaced by Iqdb::open(path). WAL tails are auto-truncated; a corrupt snapshot fails the open. |
Error::NotImplemented |
Reserved for methods that defer their implementation to a later milestone. No public v0.4.0 method returns this variant. | n/a |
The enum is #[non_exhaustive]. New variants will appear in minor releases as new failure modes emerge. Exhaustive match arms are a forward-compatibility hazard — always include _.
Examples
Self-contained examples live in examples/ and are declared in Cargo.toml. Run them with cargo run --example <name>.
-
Lifecycle (
basic) — open an in-memory instance, upsert a vector, read it back, close.- File:
examples/basic.rs - Run:
- File:
-
In-memory store walk-through (
in_memory_store) — populate the store with three records (vectors + typed payloads), compare distances between them under L2 and cosine, then delete one.- File:
examples/in_memory_store.rs - Run:
- File:
-
Top-
ksearch (search) — unfiltered cosine top-k, payload-filtered search, and batch search across three probes in one file.- File:
examples/search.rs - Run:
- File:
-
Directory-backed persistence (
persistence) — open at a path, upsert records, close. Reopen, verify the records survived, run a search, delete one, close. Reopen a third time and confirm the delete persisted. Three sessions, all consistent against the same on-disk database.- File:
examples/persistence.rs - Run:
- File:
Approximate-index examples land alongside their milestone (v0.5.0).
Benchmarks
A criterion harness is wired in benches/vector_ops.rs. v0.4.0 ships five groups:
vector_new— construction-time validation cost across small / medium / large dimensionalities (32 / 128 / 1024).distance— single-shot distance computation under each of the threeDistanceMetricvariants at dim 128.store—upsertandgetthroughput against a populated in-memory store at 1 000 records, dim 128.search— flat top-ksearch at 1 000 and 10 000 records, dim 128. Three variants: unfiltered, payload-filtered (~50% pruning), and batch-of-4.file_store— durableupsert+flushto a fresh on-disk database, and snapshot-only open with 1 000 records, dim 128.
Run with:
Criterion writes reports to target/criterion/. Approximate-index benches land with v0.5.0; CI will gate merges on a regression threshold once the benches are stable.
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/in_memory.rs— CRUD surface plusserderound-trips behind a feature gate.tests/search.rs— the four search entry points: top-kordering, filter pruning, batch order, dimension-mismatch handling, payload preservation, concurrent readers.tests/persistence.rs— full durable lifecycle: open / upsert / close / reopen, delete persistence, payload round-trip through compaction, WAL replay without close, file-path rejection, corrupt snapshot detection, corrupt WAL tail truncation, multi-cycle state preservation.tests/properties.rs—proptest-driven property tests for distance-metric algebra (symmetry, identity, range bounds), search-ranking invariants (length bound, ascending order, perfect-match presence, no-filter parity), and durable round-trip (open → upsert → close → reopen preserves record sets).tests/smoke.rs— minimal lifecycle smoke check.
- Doc tests run as part of
cargo testand validate every# Examplesblock in the rustdoc.
# Full test sweep (unit + integration + doc tests)
# Documentation build with no warnings (matches CI gating)
RUSTDOCFLAGS="-D warnings"
RUSTDOCFLAGS="-D warnings"
# Lint at the strict profile CI enforces
# Formatting check (no diffs)
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)
Platform-specific paths — the durable storage layer takes the strongest sync primitive each platform exposes:
- Linux (and other Unix except macOS):
fsync(2)viastd::fs::File::sync_all. Atomic snapshot replacement viarename(2). - macOS:
fcntl(fd, F_FULLFSYNC, 0)— directlibccall, the only platform that needs an escape hatch beyondfsyncfor true power-loss durability. Atomic snapshot replacement viarename(2). - Windows:
FlushFileBuffersviastd::fs::File::sync_all. Atomic snapshot replacement viaMoveFileExWwithMOVEFILE_REPLACE_EXISTING(also viastd::fs::rename).
io_uring (Linux batched writes) and mmap (read-mostly indices) are on the v0.4.x / v0.5.0 roadmap behind feature flags; they are not on the default build. No platform is silently degraded — every fallback is explicit and documented inline at each #[cfg] boundary, per the REPS Platform-Specific Code rule.
Configuration
Feature flags
Feature flags are strictly additive (per REPS) — enabling any combination never removes or weakens existing functionality.
| Feature | Default | Available | Description |
|---|---|---|---|
serde |
off | shipping | Derives Serialize / Deserialize on every public data type. |
mmap |
off | planned v0.4.x | Memory-mapped read path for hot indices. |
io-uring |
off | planned v0.4.x | Linux-only io_uring submission for batch writes. |
async |
off | planned v0.6.0 | Tokio-driven async mirror of the public API. |
full |
off | planned post-1.0 | All stable features in one switch. |
= { = "0.4", = ["serde"] }
Runtime configuration
Iqdb::open_in_memory() takes no parameters; Iqdb::open(path) takes only the path. A builder (IqdbBuilder) for tunable open-time knobs (compaction threshold, WAL fsync cadence, page size) lands in a future milestone once enough knobs accumulate to justify the surface.
Architecture
The crate is split along strict module boundaries — each module owns one concern and exposes a single trait or type as its contract:
src/lib.rs— crate root, lint profile, public re-exports.src/db.rs— theIqdbhandle, dispatching through the internalBackendenum to the active store.src/backend.rs— thepub(crate)Backend { Memory, File }enum; pattern-matched dispatch lets the search kernel work over either store with zero dynamic-dispatch cost.src/vector.rs— theVectorprimitive and theDistanceMetricdispatch.src/payload.rs— thePayload/PayloadValuetyped metadata layer.src/record.rs— theRecordId/Recordaggregate.src/search.rs— the flat top-ksearch kernel and theSearchResulttype.src/store.rs— the crate-internalMemoryStore(the read/write engine behindopen_in_memory).src/file_store.rs— the crate-internalFileStore(snapshot + WAL behindopen(path)).src/codec.rs— the binary frame encoder / decoder used by the file store. Length-prefixed, CRC32-checked, little-endian throughout.src/platform.rs— the cross-platformfull_syncprimitive (F_FULLFSYNC/fsync/FlushFileBuffers).src/error.rs— theErrorenum andResultalias.
As milestones land, the tree grows along the same bounded-responsibility pattern (index/ for IVF + HNSW, async_impl.rs for the Tokio surface). The boundary between layers is always a trait or a concrete type with a typed surface — concrete backend implementations are crate-internal and gated behind pub(crate).
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.
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.