1use std::path::Path;
4
5use parking_lot::RwLock;
6
7use astraea_core::error::Result;
8use astraea_core::traits::VectorIndex;
9use astraea_core::types::{DistanceMetric, NodeId, SimilarityResult};
10
11use crate::hnsw::HnswIndex;
12
13const DEFAULT_M: usize = 16;
15const DEFAULT_EF_CONSTRUCTION: usize = 200;
17const DEFAULT_EF_SEARCH: usize = 50;
19
20pub struct HnswVectorIndex {
25 inner: RwLock<HnswIndex>,
26 ef_search: usize,
27}
28
29impl HnswVectorIndex {
30 pub fn new(dimension: usize, metric: DistanceMetric) -> Self {
36 Self {
37 inner: RwLock::new(HnswIndex::new(
38 dimension,
39 metric,
40 DEFAULT_M,
41 DEFAULT_EF_CONSTRUCTION,
42 )),
43 ef_search: DEFAULT_EF_SEARCH,
44 }
45 }
46
47 pub fn with_params(
49 dimension: usize,
50 metric: DistanceMetric,
51 m: usize,
52 ef_construction: usize,
53 ef_search: usize,
54 ) -> Self {
55 Self {
56 inner: RwLock::new(HnswIndex::new(dimension, metric, m, ef_construction)),
57 ef_search,
58 }
59 }
60
61 pub fn with_seed(dimension: usize, metric: DistanceMetric, seed: u64) -> Self {
66 Self {
67 inner: RwLock::new(HnswIndex::with_seed(
68 dimension,
69 metric,
70 DEFAULT_M,
71 DEFAULT_EF_CONSTRUCTION,
72 seed,
73 )),
74 ef_search: DEFAULT_EF_SEARCH,
75 }
76 }
77
78 pub fn save_to_file(&self, path: &Path) -> Result<()> {
83 let idx = self.inner.read();
84 idx.save(path)
85 }
86
87 pub fn load_from_file(path: &Path) -> Result<Self> {
92 let idx = HnswIndex::load(path)?;
93 Ok(Self {
94 inner: RwLock::new(idx),
95 ef_search: DEFAULT_EF_SEARCH,
96 })
97 }
98}
99
100impl VectorIndex for HnswVectorIndex {
101 fn insert(&self, node_id: NodeId, embedding: &[f32]) -> Result<()> {
102 let mut idx = self.inner.write();
103 idx.insert(node_id, embedding)
104 }
105
106 fn remove(&self, node_id: NodeId) -> Result<bool> {
107 let mut idx = self.inner.write();
108 idx.remove(node_id)
109 }
110
111 fn search(&self, query: &[f32], k: usize) -> Result<Vec<SimilarityResult>> {
112 let idx = self.inner.read();
113 let raw_results = idx.search(query, k, self.ef_search)?;
114 Ok(raw_results
115 .into_iter()
116 .map(|(node_id, distance)| SimilarityResult { node_id, distance })
117 .collect())
118 }
119
120 fn dimension(&self) -> usize {
121 let idx = self.inner.read();
122 idx.dimension()
123 }
124
125 fn metric(&self) -> DistanceMetric {
126 let idx = self.inner.read();
127 idx.metric()
128 }
129
130 fn len(&self) -> usize {
131 let idx = self.inner.read();
132 idx.len()
133 }
134
135 fn node_ids(&self) -> Vec<NodeId> {
136 let idx = self.inner.read();
137 idx.node_ids()
138 }
139
140 fn save_to_path(&self, path: &Path) -> Result<()> {
141 self.save_to_file(path)
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_vector_index_trait_basic() {
151 let idx = HnswVectorIndex::new(3, DistanceMetric::Euclidean);
152 assert_eq!(idx.dimension(), 3);
153 assert_eq!(idx.metric(), DistanceMetric::Euclidean);
154 assert!(idx.is_empty());
155 assert_eq!(idx.len(), 0);
156
157 idx.insert(NodeId(1), &[1.0, 0.0, 0.0]).unwrap();
158 idx.insert(NodeId(2), &[0.0, 1.0, 0.0]).unwrap();
159 idx.insert(NodeId(3), &[0.0, 0.0, 1.0]).unwrap();
160
161 assert_eq!(idx.len(), 3);
162 assert!(!idx.is_empty());
163
164 let results = idx.search(&[1.0, 0.0, 0.0], 2).unwrap();
165 assert_eq!(results.len(), 2);
166 assert_eq!(results[0].node_id, NodeId(1));
167 assert!(results[0].distance < 1e-6);
168 }
169
170 #[test]
171 fn test_vector_index_trait_remove() {
172 let idx = HnswVectorIndex::new(2, DistanceMetric::Cosine);
173 idx.insert(NodeId(1), &[1.0, 0.0]).unwrap();
174 idx.insert(NodeId(2), &[0.0, 1.0]).unwrap();
175
176 assert!(idx.remove(NodeId(1)).unwrap());
177 assert_eq!(idx.len(), 1);
178 assert!(!idx.remove(NodeId(99)).unwrap());
179 }
180
181 #[test]
182 fn test_vector_index_custom_params() {
183 let idx = HnswVectorIndex::with_params(4, DistanceMetric::DotProduct, 8, 100, 30);
184 assert_eq!(idx.dimension(), 4);
185 assert_eq!(idx.metric(), DistanceMetric::DotProduct);
186 }
187
188 #[test]
192 fn test_node_ids_via_trait() {
193 let idx: Box<dyn VectorIndex> =
194 Box::new(HnswVectorIndex::new(2, DistanceMetric::Euclidean));
195
196 assert!(idx.node_ids().is_empty());
197
198 idx.insert(NodeId(10), &[1.0, 0.0]).unwrap();
199 idx.insert(NodeId(20), &[0.0, 1.0]).unwrap();
200
201 let mut ids = idx.node_ids();
202 ids.sort();
203 assert_eq!(ids, vec![NodeId(10), NodeId(20)]);
204
205 idx.remove(NodeId(10)).unwrap();
206 let ids = idx.node_ids();
207 assert_eq!(ids, vec![NodeId(20)]);
208 }
209
210 #[test]
212 fn test_save_to_path_via_trait() {
213 use astraea_core::traits::VectorIndex as VTrait;
214
215 let tmp = tempfile::NamedTempFile::new().unwrap();
216 let path = tmp.path().to_owned();
217 drop(tmp);
219
220 let original: Box<dyn VTrait> =
221 Box::new(HnswVectorIndex::new(3, DistanceMetric::Euclidean));
222 original.insert(NodeId(1), &[1.0, 0.0, 0.0]).unwrap();
223 original.insert(NodeId(2), &[0.0, 1.0, 0.0]).unwrap();
224
225 original.save_to_path(&path).unwrap();
227
228 let loaded = HnswVectorIndex::load_from_file(&path).unwrap();
230 let mut ids = loaded.node_ids();
231 ids.sort();
232 assert_eq!(ids, vec![NodeId(1), NodeId(2)]);
233 assert_eq!(loaded.dimension(), 3);
234 }
235}