Skip to main content

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], &params).unwrap();
42//! let b = cached.search(&[1.0, 0.0, 0.0], &params).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}