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 ordered;
68mod policy;
69mod stats;
70
71pub use crate::cached::CachedIndex;
72pub use crate::config::{CacheConfig, EvictionPolicy};
73pub use crate::stats::CacheStats;
74
75/// The version of this crate, taken from `Cargo.toml` at compile time.
76///
77/// Exposed so a consumer can report the exact `iqdb-cache` build it links
78/// against — useful in diagnostics and version-skew checks across the iqdb
79/// crate family.
80///
81/// # Examples
82///
83/// ```
84/// let version = iqdb_cache::VERSION;
85/// assert_eq!(version.split('.').count(), 3);
86/// assert!(version.split('.').all(|part| !part.is_empty()));
87/// ```
88pub const VERSION: &str = env!("CARGO_PKG_VERSION");
89
90/// Documentation-only support: a tiny in-memory index used by the runnable
91/// examples in this crate's rustdoc. Not part of the public API and exempt from
92/// SemVer; do not depend on it.
93#[doc(hidden)]
94pub mod doc_stub {
95 use std::sync::Arc;
96
97 use iqdb_index::{Index, IndexCore, IndexStats};
98 use iqdb_types::{DistanceMetric, Hit, IqdbError, Metadata, Result, SearchParams, VectorId};
99
100 /// A minimal three-dimensional index that returns one zero-distance hit per
101 /// stored id. Enough to demonstrate the cache wrapper, nothing more.
102 pub struct DocStub {
103 ids: Vec<VectorId>,
104 }
105
106 /// Builds a [`DocStub`] preloaded with a single vector.
107 #[must_use]
108 pub fn stub_index() -> DocStub {
109 DocStub {
110 ids: vec![VectorId::from(1u64)],
111 }
112 }
113
114 impl IndexCore for DocStub {
115 fn insert(&mut self, id: VectorId, _v: Arc<[f32]>, _m: Option<Metadata>) -> Result<()> {
116 self.ids.push(id);
117 Ok(())
118 }
119 fn delete(&mut self, id: &VectorId) -> Result<()> {
120 match self.ids.iter().position(|x| x == id) {
121 Some(pos) => {
122 let _removed = self.ids.remove(pos);
123 Ok(())
124 }
125 None => Err(IqdbError::NotFound),
126 }
127 }
128 fn search(&self, _q: &[f32], params: &SearchParams) -> Result<Vec<Hit>> {
129 Ok(self
130 .ids
131 .iter()
132 .take(params.k)
133 .map(|id| Hit::new(id.clone(), 0.0))
134 .collect())
135 }
136 fn len(&self) -> usize {
137 self.ids.len()
138 }
139 fn dim(&self) -> usize {
140 3
141 }
142 fn metric(&self) -> DistanceMetric {
143 DistanceMetric::Cosine
144 }
145 fn flush(&mut self) -> Result<()> {
146 Ok(())
147 }
148 fn stats(&self) -> IndexStats {
149 IndexStats {
150 n_vectors: self.ids.len(),
151 index_type: "doc_stub",
152 ..IndexStats::default()
153 }
154 }
155 }
156
157 impl Index for DocStub {
158 type Config = ();
159 fn new(_dim: usize, _metric: DistanceMetric, _config: Self::Config) -> Result<Self> {
160 Ok(DocStub { ids: Vec::new() })
161 }
162 }
163}