nodedb_vector/vamana/graph.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Vamana graph structure — adjacency-only, no vector data stored here.
4//!
5//! Vectors are held in the codec layer (RAM, compressed) and on SSD
6//! (full-precision). The graph holds only the degree-bounded adjacency list
7//! and the external ID mapping.
8
9/// A single node in the Vamana graph.
10///
11/// The graph index (`usize`) is the node's position in `VamanaGraph::nodes`.
12/// `id` is the caller-supplied external identifier (e.g. row ID).
13/// `neighbors` is capped at `VamanaGraph::r` entries.
14pub struct VamanaNode {
15 /// External identifier supplied by the caller.
16 pub id: u64,
17 /// Adjacency list — indices into `VamanaGraph::nodes`, length ≤ `r`.
18 pub neighbors: Vec<u32>,
19}
20
21/// Vamana graph index.
22///
23/// Holds only adjacency data; vector values are not stored here.
24/// Use `build::build_vamana` to populate from a vector slice.
25pub struct VamanaGraph {
26 /// Vector dimensionality (informational; not enforced here).
27 pub dim: usize,
28 /// Maximum out-degree per node (typical: 64).
29 pub r: usize,
30 /// α-pruning factor (typical: 1.2). Stored for reference; pruning logic
31 /// lives in `prune::alpha_prune`.
32 pub alpha: f32,
33 /// Index of the entry-point node (typically the medoid).
34 pub entry: usize,
35 /// Dense node storage indexed by insertion order.
36 nodes: Vec<VamanaNode>,
37}
38
39impl VamanaGraph {
40 /// Create an empty graph with the given parameters.
41 pub fn new(dim: usize, r: usize, alpha: f32) -> Self {
42 Self {
43 dim,
44 r,
45 alpha,
46 entry: 0,
47 nodes: Vec::new(),
48 }
49 }
50
51 /// Number of nodes in the graph.
52 pub fn len(&self) -> usize {
53 self.nodes.len()
54 }
55
56 /// Returns `true` if the graph has no nodes.
57 pub fn is_empty(&self) -> bool {
58 self.nodes.is_empty()
59 }
60
61 /// Add a node with the given external ID; returns its internal index.
62 pub fn add_node(&mut self, id: u64) -> usize {
63 let idx = self.nodes.len();
64 self.nodes.push(VamanaNode {
65 id,
66 neighbors: Vec::new(),
67 });
68 idx
69 }
70
71 /// Replace the adjacency list for the node at `idx`.
72 ///
73 /// Truncates `neighbors` to at most `self.r` entries.
74 pub fn set_neighbors(&mut self, idx: usize, mut neighbors: Vec<u32>) {
75 neighbors.truncate(self.r);
76 self.nodes[idx].neighbors = neighbors;
77 }
78
79 /// Immutable view of the adjacency list for the node at `idx`.
80 pub fn neighbors(&self, idx: usize) -> &[u32] {
81 &self.nodes[idx].neighbors
82 }
83
84 /// External ID of the node at `idx`.
85 pub fn external_id(&self, idx: usize) -> u64 {
86 self.nodes[idx].id
87 }
88
89 /// Iterate over all nodes as `(internal_index, &VamanaNode)`.
90 pub fn iter(&self) -> impl Iterator<Item = (usize, &VamanaNode)> {
91 self.nodes.iter().enumerate()
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn add_nodes_len_matches() {
101 let mut g = VamanaGraph::new(8, 4, 1.2);
102 assert_eq!(g.len(), 0);
103 assert!(g.is_empty());
104
105 let i0 = g.add_node(100);
106 let i1 = g.add_node(200);
107 let i2 = g.add_node(300);
108
109 assert_eq!(g.len(), 3);
110 assert!(!g.is_empty());
111 assert_eq!(i0, 0);
112 assert_eq!(i1, 1);
113 assert_eq!(i2, 2);
114 }
115
116 #[test]
117 fn set_and_get_neighbors() {
118 let mut g = VamanaGraph::new(8, 4, 1.2);
119 g.add_node(10);
120 g.add_node(20);
121 g.add_node(30);
122
123 g.set_neighbors(0, vec![1, 2]);
124 assert_eq!(g.neighbors(0), &[1, 2]);
125 assert_eq!(g.neighbors(1), &[] as &[u32]);
126 }
127
128 #[test]
129 fn set_neighbors_respects_degree_bound() {
130 let r = 3;
131 let mut g = VamanaGraph::new(4, r, 1.2);
132 for id in 0..10 {
133 g.add_node(id);
134 }
135 // Provide more neighbors than R; they should be truncated.
136 g.set_neighbors(0, vec![1, 2, 3, 4, 5]);
137 assert_eq!(g.neighbors(0).len(), r);
138 }
139
140 #[test]
141 fn external_id_roundtrip() {
142 let mut g = VamanaGraph::new(4, 8, 1.2);
143 g.add_node(9999);
144 assert_eq!(g.external_id(0), 9999);
145 }
146}