1use crate::buffer_manager::BufferManager;
12use akar_common::error::StorageError;
13use akar_common::types::Value;
14use akar_vector::hnsw::{DistanceMetric, HnswIndex};
15
16const HEADER_SIZE: usize = 48;
26
27fn serialize_header(
28 num_vectors: u64,
29 entry_point: Option<usize>,
30 max_level: usize,
31 dimensions: u32,
32 metric: &DistanceMetric,
33) -> Vec<u8> {
34 let mut buf = vec![0u8; HEADER_SIZE];
35 buf[0..8].copy_from_slice(&0x484E5357u64.to_le_bytes()); buf[8..16].copy_from_slice(&num_vectors.to_le_bytes());
37 let ep = entry_point.map(|v| v as i64).unwrap_or(-1);
38 buf[16..24].copy_from_slice(&ep.to_le_bytes());
39 buf[24..28].copy_from_slice(&(max_level as u32).to_le_bytes());
40 buf[28..32].copy_from_slice(&dimensions.to_le_bytes());
41 let metric_byte = match metric {
42 DistanceMetric::Cosine => 0u8,
43 DistanceMetric::Euclidean => 1,
44 DistanceMetric::L1 => 2,
45 DistanceMetric::L2Squared => 3,
46 DistanceMetric::DotProduct => 4,
47 };
48 buf[32] = metric_byte;
49 buf
50}
51
52fn deserialize_header(buf: &[u8]) -> Option<(u64, Option<usize>, usize, u32, DistanceMetric)> {
53 if buf.len() < HEADER_SIZE {
54 return None;
55 }
56 let magic = u64::from_le_bytes(buf[0..8].try_into().ok()?);
57 if magic != 0x484E5357 {
58 return None;
59 }
60 let num_vectors = u64::from_le_bytes(buf[8..16].try_into().ok()?);
61 let ep_raw = i64::from_le_bytes(buf[16..24].try_into().ok()?);
62 let entry_point = if ep_raw < 0 { None } else { Some(ep_raw as usize) };
63 let max_level = u32::from_le_bytes(buf[24..28].try_into().ok()?) as usize;
64 let dimensions = u32::from_le_bytes(buf[28..32].try_into().ok()?);
65 let metric = match buf[32] {
66 0 => DistanceMetric::Cosine,
67 1 => DistanceMetric::Euclidean,
68 2 => DistanceMetric::L1,
69 3 => DistanceMetric::L2Squared,
70 4 => DistanceMetric::DotProduct,
71 _ => return None,
72 };
73 Some((num_vectors, entry_point, max_level, dimensions, metric))
74}
75
76#[derive(Debug, Clone)]
84pub struct VectorIndexTable {
85 pub index_id: u64,
86 pub name: String,
87 pub table_name: String,
88 pub column_name: String,
89 pub dimensions: u32,
90 pub hnsw: HnswIndex,
91 page_count: u64,
93 file_name: String,
95 dirty: bool,
97}
98
99impl VectorIndexTable {
100 pub fn new(
102 index_id: u64,
103 name: String,
104 table_name: String,
105 column_name: String,
106 metric: DistanceMetric,
107 dimensions: u32,
108 ) -> Self {
109 Self {
110 index_id,
111 name,
112 table_name,
113 column_name,
114 dimensions,
115 hnsw: HnswIndex::new(metric),
116 page_count: 1, file_name: format!("vi_{index_id}"),
118 dirty: false,
119 }
120 }
121
122 pub fn hnsw(&self) -> &HnswIndex {
124 &self.hnsw
125 }
126
127 pub fn hnsw_mut(&mut self) -> &mut HnswIndex {
129 self.dirty = true;
130 &mut self.hnsw
131 }
132
133 pub fn metric(&self) -> DistanceMetric {
135 self.hnsw.metric()
136 }
137
138 pub fn save(&mut self, bm: &mut BufferManager) -> Result<(), StorageError> {
144 if !bm.is_file_registered(&self.file_name) {
145 return Err(StorageError::Index(format!(
146 "Vector index file '{}' not registered with BufferManager",
147 self.file_name
148 )));
149 }
150
151 let nodes = self.hnsw.nodes();
152 let num_vectors = nodes.len() as u64;
153 let entry_point = self.hnsw.entry_point();
154 let max_level = self.hnsw.max_level();
155
156 let header = serialize_header(
158 num_vectors,
159 entry_point,
160 max_level,
161 self.dimensions,
162 &self.hnsw.metric(),
163 );
164
165 let frame = bm
167 .pin_mut(&self.file_name, 0)
168 .map_err(|e| StorageError::Index(format!("Failed to pin header page: {e}")))?;
169 let data = &mut frame.data;
170 let write_len = header.len().min(data.len());
171 data[..write_len].copy_from_slice(&header[..write_len]);
172 frame.is_dirty = true;
173 bm.unpin(&self.file_name, 0);
174
175 let data_page_start = 1;
177 let mut page_idx = data_page_start;
178 let mut offset = 0usize;
179
180 while offset < num_vectors as usize {
181 let frame = bm
182 .pin_mut(&self.file_name, page_idx)
183 .map_err(|e| StorageError::Index(format!("Failed to pin data page {page_idx}: {e}")))?;
184 let page_data = &mut frame.data;
185 let capacity = page_data.len();
186 page_data.fill(0u8);
187 let mut pos = 0usize;
188 let mut written_this_page = 0usize;
189
190 while offset < num_vectors as usize {
191 let (id, vec_data) = nodes[offset];
192 let vec_len = vec_data.len().saturating_mul(8);
193 let entry_len = 8 + 4 + vec_len;
194 if pos + entry_len > capacity {
195 if written_this_page == 0 {
199 bm.unpin(&self.file_name, page_idx);
200 return Err(StorageError::Index(format!(
201 "Vector at id {id} ({vec_len} bytes) does not fit in a {} byte page",
202 capacity
203 )));
204 }
205 break; }
207 page_data[pos..pos + 8].copy_from_slice(&(id as u64).to_le_bytes());
209 pos += 8;
210 page_data[pos..pos + 4].copy_from_slice(&(vec_len as u32).to_le_bytes());
212 pos += 4;
213 let mut vec_bytes = Vec::with_capacity(vec_len);
215 for &f in vec_data {
216 vec_bytes.extend_from_slice(&f.to_le_bytes());
217 }
218 page_data[pos..pos + vec_len].copy_from_slice(&vec_bytes);
219 pos += vec_len;
220 written_this_page += 1;
221 offset += 1;
222 }
223
224 frame.is_dirty = true;
225 bm.unpin(&self.file_name, page_idx);
226 page_idx += 1;
227 }
228
229 self.page_count = page_idx;
230 self.dirty = false;
231 Ok(())
232 }
233
234 pub fn load(&mut self, bm: &mut BufferManager) -> Result<(), StorageError> {
242 if !bm.is_file_registered(&self.file_name) {
243 return Err(StorageError::Index(format!(
244 "Vector index file '{}' not registered with BufferManager",
245 self.file_name
246 )));
247 }
248
249 let frame = bm
251 .pin(&self.file_name, 0)
252 .map_err(|e| StorageError::Index(format!("Failed to pin header page: {e}")))?;
253 let header_data = &frame.data;
254 let (num_vectors, _entry_point, _max_level, dimensions, metric) =
255 deserialize_header(header_data).ok_or(StorageError::Index("Invalid vector index header".into()))?;
256 self.dimensions = dimensions;
257 bm.unpin(&self.file_name, 0);
258
259 let mut new_hnsw = HnswIndex::new(metric);
261
262 let data_page_start = 1u64;
264 let mut page_idx = data_page_start;
265 let mut loaded = 0usize;
266
267 while (loaded as u64) < num_vectors {
268 let frame_result = bm.pin(&self.file_name, page_idx);
269 let frame = match frame_result {
270 Ok(f) => f,
271 Err(_) => break, };
273 let page_data = &frame.data;
274 let capacity = page_data.len();
275 let mut pos = 0usize;
276 let remaining = num_vectors - loaded as u64;
277 let mut loaded_this_page = 0u64;
278
279 while loaded_this_page < remaining && pos + 8 <= capacity {
280 let id = u64::from_le_bytes(page_data[pos..pos + 8].try_into().unwrap()) as usize;
281 pos += 8;
282
283 if pos + 4 > capacity {
284 break;
285 }
286 let vec_len = u32::from_le_bytes(page_data[pos..pos + 4].try_into().unwrap()) as usize;
287 pos += 4;
288
289 if pos + vec_len > capacity {
290 break;
291 }
292 if vec_len == 0 {
293 break;
295 }
296
297 let dims = vec_len / 8;
298 let mut vec_data = Vec::with_capacity(dims);
299 for i in 0..dims {
300 let f = f64::from_le_bytes(page_data[pos + i * 8..pos + (i + 1) * 8].try_into().unwrap());
301 vec_data.push(f);
302 }
303 pos += vec_len;
304
305 new_hnsw.insert(vec_data, id);
306 loaded_this_page += 1;
307 }
308
309 bm.unpin(&self.file_name, page_idx);
310 page_idx += 1;
311 loaded += loaded_this_page as usize;
312
313 if loaded_this_page == 0 {
314 break;
316 }
317 if page_idx > 1024 * 1024 {
319 break;
320 }
321 }
322
323 self.hnsw = new_hnsw;
324 self.dirty = false;
325 Ok(())
326 }
327
328 pub fn flush(&mut self, bm: &mut BufferManager) -> Result<(), StorageError> {
330 if self.dirty {
331 self.save(bm)?;
332 }
333 bm.flush_all()
334 .map_err(|e| StorageError::Index(format!("Failed to flush vector index: {e}")))
335 }
336
337 pub fn register_file(&self, bm: &mut BufferManager, db_path: &std::path::Path) {
339 let file_path = db_path.join(format!("{}.idx", self.file_name));
340 bm.register_file(&self.file_name, file_path);
341 }
342
343 pub fn is_dirty(&self) -> bool {
345 self.dirty
346 }
347}
348
349pub fn extract_f64_list_from_value(val: &Value) -> Result<Vec<f64>, StorageError> {
351 akar_common::types::extract_f64_list(val).map_err(|e| StorageError::TypeMismatch {
352 expected: "numeric vector".into(),
353 actual: e,
354 })
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::buffer_manager::BufferManagerConfig;
361 use crate::page::DEFAULT_PAGE_SIZE;
362 use akar_common::memory::MemoryManager;
363 use std::sync::Arc;
364
365 fn setup_bm(db_path: &std::path::Path) -> BufferManager {
366 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
367 BufferManager::new(db_path.to_path_buf(), mm, BufferManagerConfig::default())
368 }
369
370 #[test]
371 fn test_vector_index_save_load_roundtrip() {
372 let dir = tempfile::tempdir().unwrap();
373 let mut bm = setup_bm(dir.path());
374
375 let mut idx = VectorIndexTable::new(
376 1,
377 "vec_idx".into(),
378 "items".into(),
379 "embedding".into(),
380 DistanceMetric::Cosine,
381 3,
382 );
383 idx.register_file(&mut bm, dir.path());
384 idx.hnsw_mut().insert(vec![1.0, 2.0, 3.0], 0);
385 idx.hnsw_mut().insert(vec![4.0, 5.0, 6.0], 1);
386 idx.hnsw_mut().insert(vec![7.0, 8.0, 9.0], 2);
387 assert_eq!(idx.hnsw.len(), 3);
388 idx.save(&mut bm).unwrap();
389
390 let mut loaded = VectorIndexTable::new(
392 1,
393 "vec_idx".into(),
394 "items".into(),
395 "embedding".into(),
396 DistanceMetric::Cosine,
397 0,
398 );
399 loaded.register_file(&mut bm, dir.path());
400 loaded.load(&mut bm).unwrap();
401
402 assert_eq!(loaded.hnsw.len(), 3);
404 assert_eq!(loaded.dimensions, 3);
405 let hits = loaded.hnsw.search(&[1.0, 2.0, 3.0], 3);
406 assert_eq!(hits.len(), 3);
407 assert_eq!(hits[0].1, 0, "nearest vector to [1,2,3] must be the first inserted");
408 let mut ids: Vec<usize> = hits.iter().map(|&(_, id)| id).collect();
410 ids.sort_unstable();
411 assert_eq!(ids, vec![0, 1, 2]);
412 }
413
414 #[test]
415 fn test_vector_index_roundtrip_preserves_ids() {
416 let dir = tempfile::tempdir().unwrap();
419 let mut bm = setup_bm(dir.path());
420
421 let mut idx = VectorIndexTable::new(
422 2,
423 "sparse_idx".into(),
424 "items".into(),
425 "embedding".into(),
426 DistanceMetric::Euclidean,
427 2,
428 );
429 idx.register_file(&mut bm, dir.path());
430 idx.hnsw_mut().insert(vec![0.0, 0.0], 0);
431 idx.hnsw_mut().insert(vec![1.0, 1.0], 2); idx.hnsw_mut().insert(vec![2.0, 2.0], 5);
433 idx.save(&mut bm).unwrap();
434
435 let mut loaded = VectorIndexTable::new(
436 2,
437 "sparse_idx".into(),
438 "items".into(),
439 "embedding".into(),
440 DistanceMetric::Euclidean,
441 0,
442 );
443 loaded.register_file(&mut bm, dir.path());
444 loaded.load(&mut bm).unwrap();
445
446 assert_eq!(loaded.hnsw.len(), 3);
447 assert!(loaded.hnsw().get_vector(5).is_some(), "id 5 must be restored");
448 assert!(loaded.hnsw().get_vector(1).is_none());
449 let hits = loaded.hnsw().search(&[2.0, 2.0], 3);
450 assert_eq!(hits[0].1, 5, "nearest vector must still address row 5");
451 }
452
453 #[test]
454 fn test_vector_index_save_errors_when_vector_too_large() {
455 let dir = tempfile::tempdir().unwrap();
456 let mut bm = setup_bm(dir.path());
457
458 let mut idx = VectorIndexTable::new(
459 1,
460 "vec_idx".into(),
461 "items".into(),
462 "embedding".into(),
463 DistanceMetric::Cosine,
464 1,
465 );
466 idx.register_file(&mut bm, dir.path());
467 let huge = vec![1.0f64; DEFAULT_PAGE_SIZE];
470 idx.hnsw_mut().insert(huge, 0);
471 assert!(idx.save(&mut bm).is_err());
472 }
473}