iqdb_cache/lib.rs
1//! # iqdb-cache
2//!
3//! An in-process caching layer for the HiveDB **iqdb** vector-database spine.
4//! For indexes that do not fit in RAM, a well-tuned cache turns repeated reads
5//! into memory reads. [`CachedIndex`] wraps any
6//! [`IndexCore`](iqdb_index::IndexCore) and memoizes search results, while
7//! staying a drop-in `IndexCore` itself — so it slots in anywhere the wrapped
8//! index does, including behind `Box<dyn IndexCore>`.
9//!
10//! Caching is an opt-in optimization: a database is correct with no cache at
11//! all (the default), and wrapping an index never changes the *results* a
12//! search returns — only how fast a repeated search returns them.
13//!
14//! ## Tiers
15//!
16//! - **Tier 1 — the lazy path.** [`CachedIndex::new`] wraps an index with a
17//! sensible default capacity. That is the whole common case.
18//! - **Tier 2 — the configured path.** [`CachedIndex::with_capacity`] sizes the
19//! cache (or disables it with `0`), and [`CachedIndex::with_config`] takes a
20//! [`CacheConfig`] to set capacity and an optional TTL together.
21//! - **Tier 3 — the trait seam.** `CachedIndex<I>` implements
22//! [`IndexCore`](iqdb_index::IndexCore), so it composes with any index that
23//! does.
24//!
25//! ## Correctness
26//!
27//! The cache is invalidated on every mutation, so a search never observes a
28//! stale result. See [`CachedIndex`] for the exact contract.
29//!
30//! ## Example
31//!
32//! ```
33//! use iqdb_cache::CachedIndex;
34//! use iqdb_index::IndexCore;
35//! use iqdb_types::{DistanceMetric, SearchParams};
36//!
37//! // `stub_index()` stands in for a real `iqdb-flat` / `iqdb-hnsw` index.
38//! let mut cached = CachedIndex::new(iqdb_cache::doc_stub::stub_index());
39//! let params = SearchParams::new(3, DistanceMetric::Cosine);
40//!
41//! let a = cached.search(&[1.0, 0.0, 0.0], ¶ms).unwrap();
42//! let b = cached.search(&[1.0, 0.0, 0.0], ¶ms).unwrap(); // served from cache
43//! assert_eq!(a, b);
44//! assert_eq!(cached.cache_stats().hits, 1);
45//! ```
46
47#![deny(warnings)]
48#![deny(missing_docs)]
49#![deny(unsafe_op_in_unsafe_fn)]
50#![deny(unused_must_use)]
51#![deny(unused_results)]
52#![deny(clippy::unwrap_used)]
53#![deny(clippy::expect_used)]
54#![deny(clippy::todo)]
55#![deny(clippy::unimplemented)]
56#![deny(clippy::print_stdout)]
57#![deny(clippy::print_stderr)]
58#![deny(clippy::dbg_macro)]
59#![deny(clippy::unreachable)]
60#![deny(clippy::undocumented_unsafe_blocks)]
61#![forbid(unsafe_code)]
62#![cfg_attr(docsrs, feature(doc_cfg))]
63
64mod cached;
65mod config;
66mod key;
67mod lru;
68mod stats;
69
70pub use crate::cached::CachedIndex;
71pub use crate::config::CacheConfig;
72pub use crate::stats::CacheStats;
73
74/// The version of this crate, taken from `Cargo.toml` at compile time.
75///
76/// Exposed so a consumer can report the exact `iqdb-cache` build it links
77/// against — useful in diagnostics and version-skew checks across the iqdb
78/// crate family.
79///
80/// # Examples
81///
82/// ```
83/// let version = iqdb_cache::VERSION;
84/// assert_eq!(version.split('.').count(), 3);
85/// assert!(version.split('.').all(|part| !part.is_empty()));
86/// ```
87pub const VERSION: &str = env!("CARGO_PKG_VERSION");
88
89/// Documentation-only support: a tiny in-memory index used by the runnable
90/// examples in this crate's rustdoc. Not part of the public API and exempt from
91/// SemVer; do not depend on it.
92#[doc(hidden)]
93pub mod doc_stub {
94 use std::sync::Arc;
95
96 use iqdb_index::{Index, IndexCore, IndexStats};
97 use iqdb_types::{DistanceMetric, Hit, IqdbError, Metadata, Result, SearchParams, VectorId};
98
99 /// A minimal three-dimensional index that returns one zero-distance hit per
100 /// stored id. Enough to demonstrate the cache wrapper, nothing more.
101 pub struct DocStub {
102 ids: Vec<VectorId>,
103 }
104
105 /// Builds a [`DocStub`] preloaded with a single vector.
106 #[must_use]
107 pub fn stub_index() -> DocStub {
108 DocStub {
109 ids: vec![VectorId::from(1u64)],
110 }
111 }
112
113 impl IndexCore for DocStub {
114 fn insert(&mut self, id: VectorId, _v: Arc<[f32]>, _m: Option<Metadata>) -> Result<()> {
115 self.ids.push(id);
116 Ok(())
117 }
118 fn delete(&mut self, id: &VectorId) -> Result<()> {
119 match self.ids.iter().position(|x| x == id) {
120 Some(pos) => {
121 let _removed = self.ids.remove(pos);
122 Ok(())
123 }
124 None => Err(IqdbError::NotFound),
125 }
126 }
127 fn search(&self, _q: &[f32], params: &SearchParams) -> Result<Vec<Hit>> {
128 Ok(self
129 .ids
130 .iter()
131 .take(params.k)
132 .map(|id| Hit::new(id.clone(), 0.0))
133 .collect())
134 }
135 fn len(&self) -> usize {
136 self.ids.len()
137 }
138 fn dim(&self) -> usize {
139 3
140 }
141 fn metric(&self) -> DistanceMetric {
142 DistanceMetric::Cosine
143 }
144 fn flush(&mut self) -> Result<()> {
145 Ok(())
146 }
147 fn stats(&self) -> IndexStats {
148 IndexStats {
149 n_vectors: self.ids.len(),
150 index_type: "doc_stub",
151 ..IndexStats::default()
152 }
153 }
154 }
155
156 impl Index for DocStub {
157 type Config = ();
158 fn new(_dim: usize, _metric: DistanceMetric, _config: Self::Config) -> Result<Self> {
159 Ok(DocStub { ids: Vec::new() })
160 }
161 }
162}