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