- Transparent wrapper —
CachedIndex<I>implementsIndexCore, so it slots in anywhere the wrapped index does, including behindBox<dyn IndexCore> - Result memoization — identical searches (same query, same
SearchParams) are served from an in-memory cache instead of re-running - Mutation-exact invalidation — every
insert/insert_batch/deleteclears the cache, so a search never observes a stale result - Optional TTL — give entries an expiry to bound staleness from changes the wrapper can't see; off by default, and verified deterministically with a mock clock
- Bounded LRU — an arena-backed least-recently-used cache with amortized
O(1)lookup, insert, and eviction; the footprint never exceeds the configured capacity - Off by default — size the cache, or disable it with capacity
0for a pure passthrough to A/B the cache's effect without touching call sites - Hit/miss stats —
CacheStatsexposes lifetime hit and miss counters plus ahit_ratefor tuning - Zero
unsafe— the whole crate is#![forbid(unsafe_code)]
Installation
[]
= "0.3"
Quick start
Wrap any index and let repeated searches come from memory:
use CachedIndex;
use IndexCore;
use ;
// `stub_index()` stands in for a real `iqdb-flat` / `iqdb-hnsw` index.
let cached = new;
let params = new;
let cold = cached.search.expect;
let warm = cached.search.expect; // served from cache
assert_eq!;
let stats = cached.cache_stats;
assert_eq!;
assert_eq!;
Size the cache, or disable it entirely:
use CachedIndex;
// Hold the 4096 most-recent distinct searches.
let sized = with_capacity;
assert_eq!;
// Capacity 0 is a pure passthrough — useful for measuring the cache's effect.
let bypass = with_capacity;
assert!;
A write invalidates the cache, so the next search reflects it — never a stale result:
use Arc;
use CachedIndex;
use IndexCore;
use ;
let mut cached = new;
let params = new;
let before = cached.search.expect;
cached
.insert
.expect;
let after = cached.search.expect;
// The new vector is visible immediately; the cached result was discarded.
assert_eq!;
Give entries a time-to-live to bound staleness from changes made behind the wrapper's back — through a CacheConfig (the Tier-2 path):
use Duration;
use ;
let config = new
.capacity
.ttl; // results reused within 5 min are hits
let cached = with_config;
assert_eq!;
Errors
CachedIndex introduces no errors of its own: every fallible call forwards the
wrapped index's iqdb_types::Result unchanged. A search that errors is not
cached, so a later identical search re-runs against the index.
Status
v0.3.0 — the CachedIndex wrapper, the bounded LRU result cache
with mutation-exact invalidation, and an optional per-entry TTL (via clock-lib,
so expiry is tested deterministically with a mock clock). Every core invariant is
property-tested against a brute-force reference index (the cache is transparent; a
write is never stale), concurrent reads are covered, and the hit path is
benchmarked: on the reference machine a 10k-vector / dim-64 search costs ~234 µs
uncached versus ~238 ns from cache — a ~985× speedup — with a TTL
adding ~29 ns for the clock read. Additional eviction policies (LFU / FIFO /
ARC) and loom concurrency model-checks land across the rest of the 0.x series
per the ROADMAP. The full surface is
documented in docs/API.md.
Where It Fits
iqdb-cache sits above the index family and below the database. It builds on:
iqdb-types— core types (VectorId,Hit,SearchParams,DistanceMetric,Filter)iqdb-index— theIndexCoretrait it wrapsiqdb— exposes caching via the database builder
It is unblocked today: its first-party dependencies (iqdb-types, iqdb-index, and clock-lib for TTL) are all stable at 1.0.
Standards
Built to the iQDB Rust standard. See REPS.md (Rust Efficiency & Performance Standards) and dev/DIRECTIVES.md for the engineering law and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.