Skip to content

Commit 44c8db7

Browse files
committed
add(snapshots): implement snapshots for hnsw index
1 parent f9b5ae2 commit 44c8db7

8 files changed

Lines changed: 143 additions & 13 deletions

File tree

‎crates/api/src/lib.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ mod tests {
233233
let temp_dir = tempdir().unwrap();
234234
let config = DbConfig {
235235
storage_type: StorageType::RocksDb,
236-
index_type: IndexType::Flat,
236+
index_type: IndexType::HNSW,
237237
data_path: temp_dir.path().to_path_buf(),
238238
dimension: 3,
239239
similarity: Similarity::Cosine,

‎crates/defs/src/types.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub struct IndexedVector {
4747
pub vector: DenseVector,
4848
}
4949

50-
#[derive(Debug, Deserialize, Copy, Clone)]
50+
#[derive(Debug, Serialize , Deserialize, Copy, Clone)]
5151
pub enum Similarity {
5252
Euclidean,
5353
Manhattan,

‎crates/index/src/hnsw/index.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct HnswIndex {
1818
// Default query beam width (ef); recommended ef ≥ k at query time
1919
pub ef: usize,
2020
// In-memory vector cache owned by the index
21-
cache: HashMap<PointId, DenseVector>,
21+
pub cache: HashMap<PointId, DenseVector>,
2222
// Fixed metric for this index; used consistently in insert and search
2323
pub similarity: Similarity,
2424
}

‎crates/index/src/hnsw/mod.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ pub mod index;
55
pub mod search;
66
pub mod serialize;
77
pub mod types;
8-
8+
use defs::Magic;
99
pub use index::HnswIndex;
1010

11+
pub const HNSW_MAGIC_BYTES: Magic = [0x02, 0x01, 0x03, 0x00];
12+
1113
#[cfg(test)]
1214
mod tests;

‎crates/index/src/hnsw/serialize.rs‎

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,146 @@
1-
use defs::DbError;
1+
use std::collections::HashMap;
2+
3+
use defs::{DbError, Dimension, PointId, Similarity};
4+
use serde::{Deserialize, Serialize};
25
use storage::StorageEngine;
36

4-
use crate::{IndexSnapshot, SerializableIndex, hnsw::HnswIndex};
7+
use crate::{IndexSnapshot, IndexType, SerializableIndex, hnsw::{HNSW_MAGIC_BYTES, HnswIndex, types::{LevelGenerator, Node, PointIndexation}}};
8+
9+
#[repr(packed)]
10+
#[derive(Serialize, Deserialize)]
11+
pub struct HnswMetadataPack{
12+
pub ef_construction: usize,
13+
pub data_dimension: Dimension,
14+
pub ef: usize,
15+
pub similarity: Similarity,
16+
}
17+
18+
#[derive(Serialize, Deserialize)]
19+
pub struct HnswIndexPack {
20+
pub max_connections: usize,
21+
pub max_connections_0: usize,
22+
pub max_layer: usize,
23+
pub points_by_layer: Vec<Vec<PointId>>,
24+
pub nodes: Vec<Node>,
25+
pub entry_point: Option<PointId>,
26+
pub level_scale: f64,
27+
}
28+
529

630
impl SerializableIndex for HnswIndex {
731
fn serialize_topology(&self) -> Result<Vec<u8>, DbError> {
8-
return Err(DbError::SerializationError("not implemented".to_string()));
32+
let mut buffer = Vec::new();
33+
34+
let nodes: Vec<Node> = self.index.nodes.values().cloned().collect();
35+
let index_pack = HnswIndexPack {
36+
max_connections: self.index.max_connections,
37+
max_connections_0: self.index.max_connections_0,
38+
max_layer: self.index.max_layer,
39+
points_by_layer: self.index.points_by_layer.clone(),
40+
nodes,
41+
entry_point: self.index.entry_point,
42+
level_scale: self.index.level_generator.level_scale,
43+
};
44+
45+
let index_bytes = bincode::serialize(&index_pack).map_err(|e| DbError::SerializationError(e.to_string()))?;
46+
buffer.extend(index_bytes);
47+
48+
return Ok(buffer);
949
}
50+
1051
fn serialize_metadata(&self) -> Result<Vec<u8>, DbError> {
11-
return Err(DbError::SerializationError("not implemented".to_string()));
52+
let mut buffer = Vec::new();
53+
let index_pack = HnswMetadataPack {
54+
ef_construction: self.ef_construction,
55+
data_dimension: self.data_dimension,
56+
ef: self.ef,
57+
similarity: self.similarity
58+
};
59+
60+
let metadata_bytes = bincode::serialize(&index_pack).map_err(|e| DbError::SerializationError(e.to_string()))?;
61+
buffer.extend(metadata_bytes);
62+
return Ok(buffer);
1263
}
1364

1465
fn snapshot(&self) -> Result<IndexSnapshot, DbError> {
15-
return Err(DbError::SerializationError("not implemented".to_string()));
66+
let topology_bytes = self.serialize_topology()?;
67+
let metadata_bytes = self.serialize_metadata()?;
68+
Ok(IndexSnapshot {
69+
index_type: crate::IndexType::HNSW,
70+
magic: HNSW_MAGIC_BYTES,
71+
topology_b: topology_bytes,
72+
metadata_b: metadata_bytes,
73+
})
74+
}
75+
76+
fn populate_vectors(&mut self, storage: &dyn StorageEngine) -> Result<(), DbError> {
77+
// assumes index topology is restored
78+
for id in self.index.nodes.keys() {
79+
let vec = storage.get_vector(*id)?.ok_or(DbError::SerializationError(format!("Failed to locate vector for id: {} in storage", id)))?;
80+
self.cache.insert(*id, vec);
81+
}
82+
Ok(())
1683
}
84+
}
85+
86+
87+
88+
impl HnswIndex {
89+
pub fn deserialize(
90+
IndexSnapshot {
91+
index_type,
92+
magic,
93+
topology_b,
94+
metadata_b,
95+
}: &IndexSnapshot,
96+
) -> Result<HnswIndex, DbError> {
97+
if index_type != &IndexType::HNSW {
98+
return Err(DbError::SerializationError(
99+
"Invalid index type".to_string(),
100+
));
101+
}
102+
103+
if magic != &HNSW_MAGIC_BYTES {
104+
return Err(DbError::SerializationError(
105+
"Invalid magic bytes".to_string(),
106+
));
107+
}
108+
109+
let metadata: HnswMetadataPack = bincode::deserialize(metadata_b).map_err(|e| {
110+
DbError::SerializationError(format!("Failed to deserialize HNSW Metadata: {}", e))
111+
})?;
112+
113+
let index_pack : HnswIndexPack = bincode::deserialize(topology_b).map_err(|e| {
114+
DbError::SerializationError(format!("Failed to deserialize HNSW Index: {}", e))
115+
})?;
116+
117+
let mut hnsw_index_restored = PointIndexation {
118+
max_connections: index_pack.max_connections,
119+
max_connections_0: index_pack.max_connections_0,
120+
max_layer: index_pack.max_layer,
121+
points_by_layer: index_pack.points_by_layer,
122+
entry_point: index_pack.entry_point,
123+
nodes: HashMap::new(),
124+
level_generator: LevelGenerator{
125+
level_scale: index_pack.level_scale
126+
}
127+
};
128+
129+
// restore nodes hashmap
130+
for i in index_pack.nodes {
131+
hnsw_index_restored.nodes.insert(i.id, i);
132+
}
133+
134+
135+
let hnsw = HnswIndex {
136+
ef_construction : metadata.ef_construction,
137+
data_dimension: metadata.data_dimension,
138+
ef: metadata.ef,
139+
cache: HashMap::new(),
140+
similarity: metadata.similarity,
141+
index: hnsw_index_restored
142+
};
17143

18-
fn populate_vectors(&mut self, _storage: &dyn StorageEngine) -> Result<(), DbError> {
19-
return Err(DbError::SerializationError("not implemented".to_string()));
144+
Ok(hnsw)
20145
}
21146
}

‎crates/index/src/hnsw/types.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::HashMap;
22

33
use defs::PointId;
44
use rand::Rng;
5+
use serde::{Deserialize, Serialize};
56

67
// Compact storage for layered points and adjacency used by `HnswIndex`.
78
pub struct PointIndexation {
@@ -22,6 +23,7 @@ pub struct PointIndexation {
2223
}
2324

2425
// Node with highest level and per-level neighbor lists
26+
#[derive(Serialize, Deserialize, Clone)]
2527
pub struct Node {
2628
pub id: PointId,
2729
// Highest level (0-based; level 0 is the base layer)

‎crates/index/src/kd_tree/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ pub mod types;
88
#[cfg(test)]
99
mod tests;
1010

11-
pub const KD_TREE_MAGIC_BYTES: Magic = [0x00, 0x00, 0x00, 0x00];
11+
pub const KD_TREE_MAGIC_BYTES: Magic = [0x00, 0x01, 0x02, 0x00];

‎crates/snapshot/src/lib.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use chrono::{DateTime, Local};
1515
use defs::DbError;
1616
use flate2::read::GzDecoder;
1717
use index::{
18-
IndexSnapshot, IndexType, VectorIndex, flat::index::FlatIndex, kd_tree::index::KDTree,
18+
IndexSnapshot, IndexType, VectorIndex, flat::index::FlatIndex, hnsw::HnswIndex, kd_tree::index::KDTree
1919
};
2020
use semver::Version;
2121
use std::{
@@ -281,6 +281,7 @@ impl Snapshot {
281281
let vector_index: Arc<RwLock<dyn VectorIndex>> = match manifest.index_type {
282282
IndexType::Flat => Arc::new(RwLock::new(FlatIndex::deserialize(&index_snapshot)?)),
283283
IndexType::KDTree => Arc::new(RwLock::new(KDTree::deserialize(&index_snapshot)?)),
284+
IndexType::HNSW => Arc::new(RwLock::new(HnswIndex::deserialize(&index_snapshot)?)),
284285
_ => return Err(DbError::SnapshotError("Unsupported index type".to_string())),
285286
};
286287

0 commit comments

Comments
 (0)