Skip to main content

agentdb/vectors/
hnsw.rs

1use crate::error::{AgentDbError, Result};
2use rand::RngExt;
3use serde::{Deserialize, Serialize};
4use std::cmp::Reverse;
5use std::collections::{BinaryHeap, HashMap, HashSet};
6
7#[derive(Clone, Copy)]
8struct OrdF32(f32);
9impl PartialEq for OrdF32 {
10    fn eq(&self, other: &Self) -> bool {
11        self.0.total_cmp(&other.0) == std::cmp::Ordering::Equal
12    }
13}
14impl Eq for OrdF32 {}
15impl PartialOrd for OrdF32 {
16    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
17        Some(self.cmp(other))
18    }
19}
20impl Ord for OrdF32 {
21    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
22        self.0.total_cmp(&other.0)
23    }
24}
25
26/// Distance metric used for vector similarity
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum DistanceMetric {
29    Cosine,
30    Euclidean,
31    DotProduct,
32}
33
34fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
35    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
36    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
37    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
38    if norm_a == 0.0 || norm_b == 0.0 {
39        return 1.0;
40    }
41    1.0 - (dot / (norm_a * norm_b))
42}
43
44fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
45    a.iter()
46        .zip(b.iter())
47        .map(|(x, y)| (x - y).powi(2))
48        .sum::<f32>()
49        .sqrt()
50}
51
52fn dot_product_distance(a: &[f32], b: &[f32]) -> f32 {
53    // Raw dot product (no normalisation). A higher dot product means closer,
54    // so we negate to convert similarity into a distance.
55    // Callers that want cosine behaviour should use DistanceMetric::Cosine.
56    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
57    -dot
58}
59
60pub fn dist(a: &[f32], b: &[f32], metric: &DistanceMetric) -> f32 {
61    match metric {
62        DistanceMetric::Cosine => cosine_distance(a, b),
63        DistanceMetric::Euclidean => euclidean_distance(a, b),
64        DistanceMetric::DotProduct => dot_product_distance(a, b),
65    }
66}
67
68/// Pure-Rust HNSW approximate nearest-neighbour index.
69#[derive(Serialize, Deserialize)]
70pub struct HnswIndex {
71    m: usize,
72    ef_construction: usize,
73    vectors: Vec<Vec<f32>>,
74    id_map: HashMap<String, usize>,
75    rev_map: Vec<String>,
76    layers: Vec<HashMap<usize, Vec<usize>>>,
77    entry_point: Option<usize>,
78    metric: DistanceMetric,
79}
80
81impl HnswIndex {
82    pub fn new(m: usize, ef_construction: usize, metric: DistanceMetric) -> Self {
83        Self {
84            m,
85            ef_construction,
86            vectors: Vec::new(),
87            id_map: HashMap::new(),
88            rev_map: Vec::new(),
89            layers: Vec::new(),
90            entry_point: None,
91            metric,
92        }
93    }
94
95    fn random_level(&self) -> usize {
96        let mut rng = rand::rng();
97        let m_l = 1.0 / (self.m as f64).ln();
98        let level = (-rng.random::<f64>().ln() * m_l).floor() as usize;
99        level.min(16)
100    }
101
102    pub fn insert(&mut self, id: &str, vector: Vec<f32>) {
103        if let Some(&idx) = self.id_map.get(id) {
104            self.vectors[idx] = vector;
105            return;
106        }
107        let idx = self.vectors.len();
108        self.vectors.push(vector);
109        self.id_map.insert(id.to_string(), idx);
110        self.rev_map.push(id.to_string());
111
112        let level = self.random_level();
113        while self.layers.len() <= level {
114            self.layers.push(HashMap::new());
115        }
116        for l in 0..=level {
117            self.layers[l].insert(idx, Vec::new());
118        }
119
120        if let Some(ep) = self.entry_point {
121            let max_l = level.min(self.layers.len().saturating_sub(1));
122            for l in (0..=max_l).rev() {
123                let neighbours = self.search_layer_for(idx, ep, self.m, l);
124                if let Some(layer) = self.layers.get_mut(l) {
125                    if let Some(nn) = layer.get_mut(&idx) {
126                        *nn = neighbours.iter().map(|&(i, _)| i).collect();
127                    }
128                    for &(ni, _) in &neighbours {
129                        if let Some(nlist) = layer.get_mut(&ni) {
130                            nlist.push(idx);
131                            if nlist.len() > self.m * 2 {
132                                nlist.truncate(self.m * 2);
133                            }
134                        }
135                    }
136                }
137            }
138        }
139
140        if self.entry_point.is_none() || level >= self.layers.len().saturating_sub(1) {
141            self.entry_point = Some(idx);
142        }
143    }
144
145    fn search_layer_for(
146        &self,
147        query_idx: usize,
148        entry: usize,
149        k: usize,
150        level: usize,
151    ) -> Vec<(usize, f32)> {
152        let query = self.vectors[query_idx].clone();
153        self.search_layer_vec(&query, entry, k, level)
154    }
155
156    fn search_layer_vec(
157        &self,
158        query: &[f32],
159        entry: usize,
160        k: usize,
161        level: usize,
162    ) -> Vec<(usize, f32)> {
163        let mut visited: HashSet<usize> = HashSet::new();
164        // Min-heap of candidates (closest first)
165        let mut candidates: BinaryHeap<Reverse<(OrdF32, usize)>> = BinaryHeap::new();
166        // Max-heap of results (worst/farthest at top for eviction)
167        let mut result: BinaryHeap<(OrdF32, usize)> = BinaryHeap::new();
168
169        let d0 = dist(query, &self.vectors[entry], &self.metric);
170        candidates.push(Reverse((OrdF32(d0), entry)));
171        result.push((OrdF32(d0), entry));
172        visited.insert(entry);
173
174        while let Some(Reverse((OrdF32(d_curr), curr))) = candidates.pop() {
175            if let Some(&(OrdF32(worst), _)) = result.peek() {
176                if d_curr > worst && result.len() >= k {
177                    break;
178                }
179            }
180            if let Some(layer) = self.layers.get(level) {
181                if let Some(neighbours) = layer.get(&curr) {
182                    for &nb in neighbours {
183                        if visited.insert(nb) {
184                            let nd = dist(query, &self.vectors[nb], &self.metric);
185                            candidates.push(Reverse((OrdF32(nd), nb)));
186                            result.push((OrdF32(nd), nb));
187                            while result.len() > k * 2 {
188                                result.pop();
189                            }
190                        }
191                    }
192                }
193            }
194        }
195
196        let mut out: Vec<(usize, f32)> = result.into_iter().map(|(OrdF32(d), i)| (i, d)).collect();
197        out.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
198        out.truncate(k);
199        out
200    }
201
202    pub fn search(&self, query: &[f32], k: usize) -> Vec<(String, f32)> {
203        let mut ep = match self.entry_point {
204            Some(e) => e,
205            None => return vec![],
206        };
207        let num_layers = self.layers.len();
208        if num_layers == 0 {
209            return vec![];
210        }
211
212        for l in (1..num_layers).rev() {
213            let mut improved = true;
214            while improved {
215                improved = false;
216                if let Some(layer) = self.layers.get(l) {
217                    if let Some(neighbours) = layer.get(&ep) {
218                        let d_ep = dist(query, &self.vectors[ep], &self.metric);
219                        for &nb in neighbours {
220                            let d_nb = dist(query, &self.vectors[nb], &self.metric);
221                            if d_nb < d_ep {
222                                ep = nb;
223                                improved = true;
224                                break;
225                            }
226                        }
227                    }
228                }
229            }
230        }
231
232        let ef = k.max(self.ef_construction);
233        let raw = self.search_layer_vec(query, ep, ef, 0);
234
235        raw.into_iter()
236            .take(k)
237            .map(|(idx, d)| (self.rev_map[idx].clone(), d))
238            .collect()
239    }
240
241    pub fn serialize(&self) -> Result<Vec<u8>> {
242        bincode::serde::encode_to_vec(self, bincode::config::standard())
243            .map_err(|e| AgentDbError::Serialization(e.to_string()))
244    }
245
246    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
247        bincode::serde::decode_from_slice(bytes, bincode::config::standard())
248            .map(|(val, _)| val)
249            .map_err(|e| AgentDbError::Serialization(e.to_string()))
250    }
251
252    pub fn len(&self) -> usize {
253        self.vectors.len()
254    }
255
256    pub fn is_empty(&self) -> bool {
257        self.vectors.is_empty()
258    }
259}