1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt::Debug;
6
7use crate::singularity::Concept;
8use csm_core_lib::error::Result;
9use csm_core_lib::hyperdim::{HVec10240, Hypervector};
10
11pub mod brute_force;
12#[cfg(feature = "ann-hnsw")]
13pub mod hnsw;
14#[cfg(feature = "ann-lsh")]
15pub mod lsh;
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct IndexStats {
20 pub backend: String,
21 pub count: usize,
22 pub memory_usage_bytes: usize,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
27pub enum IndexBackend {
28 #[default]
30 BruteForce,
31 #[cfg(feature = "ann-hnsw")]
33 Hnsw {
34 m: usize,
36 ef_construction: usize,
38 ef_search: usize,
40 },
41 #[cfg(feature = "ann-lsh")]
43 Lsh {
44 num_tables: usize,
46 hash_bits: usize,
48 },
49}
50
51pub trait AnnIndex<H: Hypervector = HVec10240>: Send + Sync + Debug + 'static {
53 fn insert(&mut self, id: String, vec: &H) -> Result<()>;
55
56 fn delete(&mut self, id: &str) -> Result<()>;
58
59 fn search(&self, query: &H, top_k: usize) -> Result<Vec<(String, f32)>>;
61
62 fn search_filtered(
64 &self,
65 query: &H,
66 top_k: usize,
67 filter: &crate::metadata_filter::MetadataFilter,
68 concepts: &std::collections::HashMap<String, crate::singularity::Concept<H>>,
69 ) -> Result<Vec<(String, f32)>>;
70
71 fn rebuild(&mut self, concepts: &HashMap<String, Concept<H>>) -> Result<()>;
73
74 fn stats(&self) -> IndexStats;
76
77 fn serialize(&self) -> Result<Vec<u8>>;
79
80 fn deserialize(&mut self, data: &[u8]) -> Result<()>;
82}
83
84pub fn create_index<H: Hypervector + 'static>(
86 backend: &IndexBackend,
87) -> Result<Box<dyn AnnIndex<H>>> {
88 let index: Box<dyn AnnIndex<H>> = match backend {
89 IndexBackend::BruteForce => Box::new(brute_force::BruteForce::new()),
90 #[cfg(feature = "ann-hnsw")]
91 IndexBackend::Hnsw {
92 m,
93 ef_construction,
94 ef_search,
95 } => Box::new(hnsw::HnswIndex::new(*m, *ef_construction, *ef_search)?),
96 #[cfg(feature = "ann-lsh")]
97 IndexBackend::Lsh {
98 num_tables,
99 hash_bits,
100 } => Box::new(lsh::LshIndex::new(*num_tables, *hash_bits)?),
101 #[allow(unreachable_patterns)]
102 _ => Box::new(brute_force::BruteForce::new()),
103 };
104 Ok(index)
105}
106
107pub fn validate_index_backend(backend: &IndexBackend) -> Result<()> {
114 let _index: Box<dyn AnnIndex<HVec10240>> = create_index(backend)?;
115 Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
121 use super::*;
122 #[cfg(any(feature = "ann-hnsw", feature = "ann-lsh"))]
123 use csm_core_lib::error::MemoryError;
124
125 #[test]
126 fn create_index_bruteforce_ok() {
127 let idx = create_index::<HVec10240>(&IndexBackend::BruteForce);
128 assert!(idx.is_ok());
129 assert!(validate_index_backend(&IndexBackend::BruteForce).is_ok());
130 }
131
132 #[cfg(feature = "ann-hnsw")]
133 #[test]
134 fn create_index_hnsw_valid_ok() {
135 let backend = IndexBackend::Hnsw {
136 m: 16,
137 ef_construction: 200,
138 ef_search: 50,
139 };
140 assert!(create_index::<HVec10240>(&backend).is_ok());
141 assert!(validate_index_backend(&backend).is_ok());
142 }
143
144 #[cfg(feature = "ann-hnsw")]
145 #[test]
146 fn create_index_hnsw_m_zero_is_invalid_input() {
147 let backend = IndexBackend::Hnsw {
148 m: 0,
149 ef_construction: 200,
150 ef_search: 50,
151 };
152 match create_index::<HVec10240>(&backend) {
153 Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "m"),
154 other => panic!("expected InvalidInput for m=0, got {other:?}"),
155 }
156 assert!(matches!(
157 validate_index_backend(&backend),
158 Err(MemoryError::InvalidInput { .. })
159 ));
160 }
161
162 #[cfg(feature = "ann-hnsw")]
163 #[test]
164 fn create_index_hnsw_m_too_large_is_invalid_input() {
165 let backend = IndexBackend::Hnsw {
166 m: 257,
167 ef_construction: 200,
168 ef_search: 50,
169 };
170 match create_index::<HVec10240>(&backend) {
171 Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "m"),
172 other => panic!("expected InvalidInput for m=257, got {other:?}"),
173 }
174 }
175
176 #[cfg(feature = "ann-lsh")]
177 #[test]
178 fn create_index_lsh_valid_ok() {
179 let backend = IndexBackend::Lsh {
180 num_tables: 4,
181 hash_bits: 8,
182 };
183 assert!(create_index::<HVec10240>(&backend).is_ok());
184 assert!(validate_index_backend(&backend).is_ok());
185 }
186
187 #[cfg(feature = "ann-lsh")]
188 #[test]
189 fn create_index_lsh_zero_tables_is_invalid_input() {
190 let backend = IndexBackend::Lsh {
191 num_tables: 0,
192 hash_bits: 8,
193 };
194 match create_index::<HVec10240>(&backend) {
195 Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "num_tables"),
196 other => panic!("expected InvalidInput for num_tables=0, got {other:?}"),
197 }
198 assert!(matches!(
199 validate_index_backend(&backend),
200 Err(MemoryError::InvalidInput { .. })
201 ));
202 }
203}