This document is the canonical API reference for iqdb v1.0.0. Every public type, method, error variant, and feature flag is recorded here with parameter descriptions and at least one runnable example. The narrative companion is the README; the per-release notes live under docs/release/.
As of v0.5.0, iqdb is the integration layer over the iqdb crate family. The vector vocabulary it exposes (Vector, VectorId, Metadata, Value, Hit, Filter, DistanceMetric, SearchParams) is re-exported from iqdb-types; the tuning structs HnswConfig, IvfConfig, and CacheConfig are re-exported from the respective family crates. They are documented here as part of iqdb's public surface, with links to the originating crate for the exhaustive reference.
[dependencies]
iqdb = "1"With optional features:
[dependencies]
iqdb = { version = "1", features = ["serde", "parallel", "zstd"] }MSRV is Rust 1.87.
use iqdb::{DistanceMetric, Iqdb, Result, Vector, VectorId};
fn main() -> Result<()> {
let db = Iqdb::open_in_memory(3, DistanceMetric::Cosine)?;
db.upsert(VectorId::from(1u64), Vector::new(vec![1.0, 0.0, 0.0])?, None)?;
db.upsert(VectorId::from(2u64), Vector::new(vec![0.0, 1.0, 0.0])?, None)?;
let hits = db.search(&Vector::new(vec![1.0, 0.0, 0.0])?, 1)?;
assert_eq!(hits[0].id, VectorId::from(1u64));
db.close()
}Every fallible call returns Result<T> whose error is the single Error enum. It wraps the family's two error vocabularies plus a handle-level configuration variant. The enum is #[non_exhaustive]; a match must include a wildcard _ arm.
use iqdb::{DistanceMetric, Error, Iqdb, Vector, VectorId};
# fn main() {
let db = Iqdb::open_in_memory(3, DistanceMetric::Cosine).unwrap();
// Wrong dimensionality is rejected at the boundary.
let err = db
.upsert(VectorId::from(1u64), Vector::new(vec![1.0, 0.0]).unwrap(), None)
.unwrap_err();
match err {
Error::Index(_) => { /* a vocabulary / index failure */ }
Error::Persist(_) => { /* a durable-storage failure */ }
Error::Config(_) => { /* a handle-level consistency failure */ }
_ => {}
}
# }The database handle. A database fixes its dimensionality and distance metric at open time and is Send + Sync (share it behind an Arc<Iqdb>). Concurrent searches share a read guard; writes and the lazy IVF build take the write guard. Lock poisoning is recovered rather than propagated as a panic.
| Method | Signature | Description |
|---|---|---|
open_in_memory |
(dim: usize, metric: DistanceMetric) -> Result<Iqdb> |
Ephemeral, exact-flat database. Errors on dim == 0. |
open_in_memory_with |
(config: IqdbConfig) -> Result<Iqdb> |
In-memory database from a full IqdbConfig (index kind + optional cache). |
open |
<P: AsRef<Path>>(path: P, dim: usize, metric: DistanceMetric) -> Result<Iqdb> |
Durable, file-backed database. Creates the file if absent, loads it if present. |
open_with |
<P: AsRef<Path>>(path: P, config: IqdbConfig) -> Result<Iqdb> |
Durable database from a full IqdbConfig. |
dim is the dimensionality every vector must have; metric is the distance used by every search. On a reopen, the stored dim / metric must match the requested values or the call fails with Error::Config.
use iqdb::{DistanceMetric, HnswConfig, IndexKind, Iqdb, IqdbConfig};
# fn main() -> iqdb::Result<()> {
// Tier 1: exact flat, in memory.
let flat = Iqdb::open_in_memory(64, DistanceMetric::Cosine)?;
// Tier 2: an HNSW graph, configured.
let cfg = IqdbConfig::new(64, DistanceMetric::Cosine)
.index(IndexKind::Hnsw(HnswConfig::default().with_ef_search(96)));
let hnsw = Iqdb::open_in_memory_with(cfg)?;
assert!(flat.is_empty() && hnsw.is_empty());
# Ok(())
# }| Method | Signature | Description |
|---|---|---|
upsert |
(id: VectorId, vector: Vector, metadata: Option<Metadata>) -> Result<()> |
Insert or replace the record under id. Replacing keeps the count unchanged. Rejects a wrong-dimension vector. |
get |
(id: &VectorId) -> Result<Option<(Vector, Option<Metadata>)>> |
The stored vector and metadata, or None if absent. |
delete |
(id: &VectorId) -> Result<bool> |
Remove by id; true if it was present (idempotent). |
len |
() -> usize |
Number of stored vectors. |
is_empty |
() -> bool |
true if empty. |
use iqdb::{DistanceMetric, Iqdb, Metadata, Value, Vector, VectorId};
# fn main() -> iqdb::Result<()> {
let db = Iqdb::open_in_memory(2, DistanceMetric::Cosine)?;
let meta: Metadata = [("kind".to_string(), Value::String("doc".into()))]
.into_iter().collect();
db.upsert(VectorId::from(1u64), Vector::new(vec![1.0, 0.0])?, Some(meta))?;
// Replacing the same id keeps len at 1.
db.upsert(VectorId::from(1u64), Vector::new(vec![0.0, 1.0])?, None)?;
assert_eq!(db.len(), 1);
let (vector, _meta) = db.get(&VectorId::from(1u64))?.expect("present");
assert_eq!(vector.as_slice(), &[0.0, 1.0]);
assert!(db.delete(&VectorId::from(1u64))?);
assert!(!db.delete(&VectorId::from(1u64))?);
# Ok(())
# }| Method | Signature | Description |
|---|---|---|
search |
(query: &Vector, k: usize) -> Result<Vec<Hit>> |
Top-k, nearest-first. |
search_with |
(query: &Vector, k: usize, filter: Filter) -> Result<Vec<Hit>> |
Top-k restricted to records matching filter. |
search_batch |
(queries: &[Vector], k: usize) -> Result<Vec<Vec<Hit>>> |
One result list per query, in order. |
search_batch_with |
(queries: &[Vector], k: usize, filter: Filter) -> Result<Vec<Vec<Hit>>> |
Batch with a shared filter. |
Results sort by Hit::distance ascending (smaller is nearer). On the exact flat index the filter is applied before scoring; on HNSW / IVF it is a post-filter, so a highly selective filter can return fewer than k hits.
use iqdb::{DistanceMetric, Filter, Iqdb, Metadata, Value, Vector, VectorId};
# fn main() -> iqdb::Result<()> {
let db = Iqdb::open_in_memory(2, DistanceMetric::Cosine)?;
let doc: Metadata = [("kind".to_string(), Value::String("doc".into()))].into_iter().collect();
let img: Metadata = [("kind".to_string(), Value::String("image".into()))].into_iter().collect();
db.upsert(VectorId::from(1u64), Vector::new(vec![1.0, 0.0])?, Some(doc))?;
db.upsert(VectorId::from(2u64), Vector::new(vec![0.99, 0.10])?, Some(img))?;
// Unfiltered top-2.
let all = db.search(&Vector::new(vec![1.0, 0.0])?, 2)?;
assert_eq!(all.len(), 2);
// Documents only.
let docs = db.search_with(
&Vector::new(vec![1.0, 0.0])?,
5,
Filter::eq("kind", Value::String("doc".into())),
)?;
assert_eq!(docs.len(), 1);
assert_eq!(docs[0].id, VectorId::from(1u64));
# Ok(())
# }| Method | Signature | Description |
|---|---|---|
optimize |
() -> Result<()> |
Rebuild / retrain the approximate index over the current vectors (IVF centroids). No-op for flat / HNSW. |
cache_stats |
() -> Option<CacheStats> |
Cache hit/miss statistics, or None when uncached. |
flush |
() -> Result<()> |
Compact a file-backed store (fold the WAL into a fresh snapshot); no-op in memory. |
close |
(self) -> Result<()> |
Final compaction, then release the handle. |
dim |
() -> usize |
The fixed dimensionality. |
metric |
() -> DistanceMetric |
The fixed distance metric. |
use iqdb::{DistanceMetric, Iqdb, Vector, VectorId};
# fn main() -> iqdb::Result<()> {
let db = Iqdb::open("./data/vectors.iqdb", 3, DistanceMetric::Cosine)?;
db.upsert(VectorId::from(1u64), Vector::new(vec![0.1, 0.2, 0.3])?, None)?;
db.flush()?;
db.close()
# }Available with the async feature. A Tokio adapter over Iqdb. It holds an Arc<Iqdb> and runs each blocking operation on Tokio's blocking pool via tokio::task::spawn_blocking, so awaiting a search or a write never stalls the executor. The family is synchronous by design — a search is CPU-bound, a durable write is a blocking fsync — so AsyncIqdb is a thin adapter, not a re-implementation; the sync Iqdb remains the source of truth. It is Clone (shares the handle through the Arc), Send, and Sync. A panic in a blocking closure is re-raised on the awaiting task.
The async methods mirror the sync surface; search and batch methods take their query by value (the work runs on another thread). Cheap accessors stay synchronous.
| Method | Signature | Description |
|---|---|---|
open_in_memory |
async (dim, metric) -> Result<AsyncIqdb> |
In-memory, exact flat. |
open_in_memory_with |
async (config: IqdbConfig) -> Result<AsyncIqdb> |
In-memory from a config. |
open |
async <P: AsRef<Path>>(path, dim, metric) -> Result<AsyncIqdb> |
Durable; open runs on the blocking pool. |
open_with |
async <P: AsRef<Path>>(path, config) -> Result<AsyncIqdb> |
Durable from a config. |
upsert |
async (id: VectorId, vector: Vector, metadata: Option<Metadata>) -> Result<()> |
Insert or replace. |
get |
async (id: VectorId) -> Result<Option<(Vector, Option<Metadata>)>> |
Look up by id (taken by value). |
delete |
async (id: VectorId) -> Result<bool> |
Remove by id. |
search |
async (query: Vector, k: usize) -> Result<Vec<Hit>> |
Top-k. |
search_with |
async (query: Vector, k: usize, filter: Filter) -> Result<Vec<Hit>> |
Filtered top-k. |
search_batch / search_batch_with |
async (queries: Vec<Vector>, k, [filter]) -> Result<Vec<Vec<Hit>>> |
Batch variants. |
optimize |
async () -> Result<()> |
Rebuild / retrain the approximate index. |
flush |
async () -> Result<()> |
Compact a file-backed store. |
close |
async (self) -> Result<()> |
Final compaction, then release. |
len / is_empty / dim / metric / cache_stats |
sync (&self) |
Cheap accessors — no offload. |
# tokio::runtime::Runtime::new().unwrap().block_on(async {
use iqdb::{AsyncIqdb, DistanceMetric, Vector, VectorId};
let db = AsyncIqdb::open_in_memory(3, DistanceMetric::Cosine).await?;
db.upsert(VectorId::from(1u64), Vector::new(vec![1.0, 0.0, 0.0])?, None).await?;
let hits = db.search(Vector::new(vec![1.0, 0.0, 0.0])?, 1).await?;
assert_eq!(hits[0].id, VectorId::from(1u64));
db.close().await?;
# Ok::<(), iqdb::Error>(())
# }).unwrap();The fluent, construction-time configuration consumed by open_in_memory_with / open_with. Small by design: dimensionality, metric, an IndexKind, and an optional CacheConfig. Per-index tuning lives inside the IndexKind variants.
| Method | Signature | Description |
|---|---|---|
new |
(dim: usize, metric: DistanceMetric) -> IqdbConfig |
Start a config; defaults to IndexKind::Flat, no cache. |
index |
(self, kind: IndexKind) -> IqdbConfig |
Select the index implementation. |
cache |
(self, cache: CacheConfig) -> IqdbConfig |
Attach a result cache. |
fsync |
(self, policy: FsyncPolicy) -> IqdbConfig |
WAL fsync cadence for the file-backed path (Always default / Periodic / Never). Ignored in memory. |
compression |
(self, compression: Compression) -> IqdbConfig |
Snapshot compression (None default / Zstd { level } / Lz4); the latter two require the zstd / lz4 feature. Ignored in memory. |
dim |
(&self) -> usize |
The configured dimensionality. |
metric |
(&self) -> DistanceMetric |
The configured metric. |
index_kind |
(&self) -> IndexKind |
The configured index kind. |
is_cached |
(&self) -> bool |
Whether a cache is configured. |
use iqdb::{CacheConfig, Compression, DistanceMetric, FsyncPolicy, IndexKind, IqdbConfig, IvfConfig};
let cfg = IqdbConfig::new(128, DistanceMetric::Euclidean)
.index(IndexKind::Ivf(IvfConfig::default().with_n_probes(16)))
.cache(CacheConfig::new().capacity(10_000))
.fsync(FsyncPolicy::Always)
.compression(Compression::None);
assert_eq!(cfg.dim(), 128);
assert!(cfg.is_cached());
assert!(matches!(cfg.index_kind(), IndexKind::Ivf(_)));FsyncPolicy and Compression are re-exported from iqdb-persist; they tune only the durable, file-backed path and are ignored by an in-memory database.
Selects which index backs the database. Copy, #[derive(Default)] (defaults to Flat).
| Variant | Payload | Description |
|---|---|---|
Flat |
— | Exact brute-force search; the recall ground truth. No tuning. |
Hnsw |
HnswConfig |
Hierarchical Navigable Small World graph (approximate). |
Ivf |
IvfConfig |
Inverted-file clustered index (approximate), IVF-Flat or IVF-PQ. |
use iqdb::{IndexKind, IvfConfig};
let exact = IndexKind::default();
assert!(matches!(exact, IndexKind::Flat));
let ivf = IndexKind::Ivf(IvfConfig::default().with_n_clusters(256));
assert!(matches!(ivf, IndexKind::Ivf(_)));Re-exported tuning structs for the approximate indices (see iqdb-hnsw and iqdb-ivf for the full reference). Both follow the with_* builder idiom.
HnswConfig knobs: m (graph degree), ef_construction (build beam), ef_search (query beam), filter_widen (beam multiplier under a filter), seed.
IvfConfig knobs: n_clusters, n_probes (clusters visited per query), training_sample_size, use_pq / pq_subvectors / pq_refine_factor (IVF-PQ), seed.
use iqdb::{HnswConfig, IvfConfig};
let hnsw = HnswConfig::default().with_m(32).with_ef_search(128);
assert_eq!(hnsw.m, 32);
let ivf = IvfConfig::default().with_n_clusters(64).with_n_probes(8);
assert_eq!(ivf.n_probes, 8);IVF is trained lazily from the stored vectors on the first search; Iqdb::optimize retrains it. IVF-PQ (use_pq = true) supports only Euclidean, DotProduct, and Manhattan; constructing it under Cosine or Hamming fails with Error::Index.
Re-exported from iqdb-cache. CacheConfig sizes a result cache (capacity, policy, ttl); CacheStats reports hits, misses, evictions, len, capacity, with hit_rate() and lookups() helpers. The cache invalidates on every mutation, so it never serves a stale result.
use iqdb::{CacheConfig, DistanceMetric, Iqdb, IqdbConfig, Vector, VectorId};
# fn main() -> iqdb::Result<()> {
let db = Iqdb::open_in_memory_with(
IqdbConfig::new(2, DistanceMetric::Cosine).cache(CacheConfig::new().capacity(1024)),
)?;
db.upsert(VectorId::from(1u64), Vector::new(vec![1.0, 0.0])?, None)?;
let q = Vector::new(vec![1.0, 0.0])?;
let _ = db.search(&q, 1)?;
let _ = db.search(&q, 1)?; // served from cache
assert_eq!(db.cache_stats().expect("cached").hits, 1);
# Ok(())
# }An owned, validated, immutable dense embedding (re-exported from iqdb-types). Vector::new(Vec<f32>) rejects empty and non-finite input; as_slice, dim, and into_inner are non-allocating accessors.
use iqdb::Vector;
let v = Vector::new(vec![0.1, 0.2, 0.3])?;
assert_eq!(v.dim(), 3);
assert_eq!(v.as_slice(), &[0.1, 0.2, 0.3]);
// Empty and non-finite input is rejected.
assert!(Vector::new(vec![]).is_err());
assert!(Vector::new(vec![f32::NAN]).is_err());
# Ok::<(), iqdb::Error>(())A stable identifier (re-exported from iqdb-types): either a compact U64 or an opaque non-empty Bytes key.
use iqdb::VectorId;
let numeric = VectorId::from(7u64);
assert_eq!(numeric, VectorId::U64(7));
let key = VectorId::try_from(vec![0xde, 0xad]).expect("non-empty");
assert_eq!(key.to_string(), "dead");
# Ok::<(), iqdb::Error>(())Metadata is an immutable, ordered map of string keys to scalar Values (re-exported from iqdb-types). Value is a closed set of scalars: String, Int(i64), Float(f64), Bool, Null. Build Metadata from an iterator or a BTreeMap.
use iqdb::{Metadata, Value};
let meta: Metadata = [
("title".to_string(), Value::String("intro".into())),
("year".to_string(), Value::Int(2026)),
]
.into_iter()
.collect();
assert_eq!(meta.get("year"), Some(&Value::Int(2026)));
assert_eq!(meta.get("missing"), None);A declarative metadata predicate (re-exported from iqdb-types) passed to search_with. Leaf comparisons (eq, neq, lt, lte, gt, gte, is_in) compose with and, or, and not. Absent fields and type mismatches evaluate to false (closed-world).
use iqdb::{Filter, Value};
let published_recent = Filter::and(vec![
Filter::eq("published", Value::Bool(true)),
Filter::gte("year", Value::Int(2026)),
]);
let _ = published_recent;The distance used by a database (re-exported from iqdb-types, #[non_exhaustive]): Cosine, DotProduct, Euclidean, Manhattan, Hamming. All five sort smaller-is-nearer; Hit::distance for DotProduct is negated so one ordering rule holds everywhere.
use iqdb::DistanceMetric;
let metric = DistanceMetric::Cosine;
assert_eq!(metric, DistanceMetric::Cosine);A search result (re-exported from iqdb-types): id: VectorId, distance: f32 (smaller is nearer), metadata: Option<Metadata>.
use iqdb::{DistanceMetric, Iqdb, Vector, VectorId};
# fn main() -> iqdb::Result<()> {
let db = Iqdb::open_in_memory(2, DistanceMetric::Euclidean)?;
db.upsert(VectorId::from(1u64), Vector::new(vec![0.0, 0.0])?, None)?;
let hit = &db.search(&Vector::new(vec![0.1, 0.1])?, 1)?[0];
assert_eq!(hit.id, VectorId::from(1u64));
assert!(hit.distance >= 0.0);
# Ok(())
# }Error is the single #[non_exhaustive] error type; Result<T> aliases core::result::Result<T, Error>. Every variant's Display carries only static text or library-produced values, so it is safe to log.
| Variant | Meaning |
|---|---|
Error::Index(IqdbError) |
An index / vocabulary failure — dimension mismatch, absent id, invalid metric for the chosen index, malformed filter. |
Error::Persist(PersistError) |
A durable-storage failure — snapshot / WAL I/O, a corrupt or truncated file, a checksum mismatch, an unsupported compression feature. |
Error::Config(&'static str) |
A handle-level consistency failure — most often a reopen whose dim / metric does not match the stored database. |
use iqdb::{DistanceMetric, Error, Iqdb, Vector, VectorId};
# fn main() {
let db = Iqdb::open_in_memory(3, DistanceMetric::Cosine).unwrap();
let err = db
.upsert(VectorId::from(1u64), Vector::new(vec![1.0]).unwrap(), None)
.unwrap_err();
assert!(matches!(err, Error::Index(_)));
# }All feature flags are additive.
| 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). |
- Construction-time shape.
dimandmetricare fixed at open. A wrong-dimension vector is rejected atupsert; a reopen with a mismatcheddim/metricfails withError::Config. - Authoritative store. iqdb owns the vectors in an internal row store; the search index (flat / HNSW / IVF) is a derived structure rebuilt from that store on load.
len()always reflects the authoritative store. - IVF lifecycle. IVF is trained lazily from the stored vectors on the first search after a write, and rebuilt by
Iqdb::optimize. Flat and HNSW accept incremental inserts directly, so search-after-write is immediate for them. - Approximate filters. HNSW / IVF apply a metadata
Filterafter traversal; a highly selective filter can return fewer thankhits. The flat index applies the filter exactly before scoring. Widen HNSWfilter_widenor IVFn_probesto compensate. - Concurrency.
IqdbisSend + Sync. Reads share aRwLockread guard; writes and the lazy IVF build take the write guard. Lock poisoning is recovered, never propagated as a panic. - Durability. The default file-backed write policy
fsyncs the WAL on every acknowledged write, so a clean process exit (or even a crash) replays to the last acknowledged state on reopen.flush/closecompact the WAL into a snapshot. - Cross-platform format. The on-disk payload is little-endian on every platform; a database written on x86_64 reads back identically on aarch64.
- Hostile-input safety. The on-disk decoder bounds every allocation: a corrupt or crafted payload naming an oversized length is rejected with
Error::Persistrather than triggering an out-of-memory abort, and a malformed frame never panics. This is covered by fuzz-style property tests. - Non-exhaustive types.
ErrorandDistanceMetricare#[non_exhaustive]. Always include a wildcard_arm in amatch.