Skip to main content

reddb_server/storage/engine/
hnsw.rs

1//! HNSW (Hierarchical Navigable Small World) Index
2//!
3//! A from-scratch implementation of the HNSW algorithm for approximate
4//! nearest neighbor search. No external dependencies.
5//!
6//! # Algorithm Overview
7//!
8//! HNSW builds a multi-layer graph where:
9//! - Layer 0 contains all nodes
10//! - Higher layers contain progressively fewer nodes
11//! - Each layer is a navigable small world graph
12//!
13//! Search starts from the top layer and greedily descends,
14//! using each layer to quickly approach the target region.
15//!
16//! # References
17//!
18//! - Original paper: "Efficient and robust approximate nearest neighbor search
19//!   using Hierarchical Navigable Small World graphs" (Malkov & Yashunin, 2018)
20
21use std::cmp::Reverse;
22use std::collections::{BinaryHeap, HashMap, HashSet};
23use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
24
25use super::distance::{
26    cmp_f32, distance_simd, DistanceMetric, DistanceResult, ReverseDistanceResult,
27};
28
29/// Node identifier in the HNSW graph
30pub type NodeId = u64;
31
32/// HNSW index configuration parameters
33#[derive(Debug, Clone)]
34pub struct HnswConfig {
35    /// Maximum number of connections per node (except layer 0)
36    pub m: usize,
37    /// Maximum connections at layer 0 (typically 2*M)
38    pub m_max0: usize,
39    /// Size of dynamic candidate list during construction
40    pub ef_construction: usize,
41    /// Size of dynamic candidate list during search (can be adjusted)
42    pub ef_search: usize,
43    /// Normalization factor for layer assignment (1/ln(M))
44    pub ml: f64,
45    /// Distance metric to use
46    pub metric: DistanceMetric,
47}
48
49impl Default for HnswConfig {
50    fn default() -> Self {
51        let m = 16;
52        Self {
53            m,
54            m_max0: m * 2,
55            ef_construction: 100,
56            ef_search: 50,
57            ml: 1.0 / (m as f64).ln(),
58            metric: DistanceMetric::L2,
59        }
60    }
61}
62
63impl HnswConfig {
64    /// Create a new configuration with custom M value
65    pub fn with_m(m: usize) -> Self {
66        Self {
67            m,
68            m_max0: m * 2,
69            ml: 1.0 / (m as f64).ln(),
70            ..Default::default()
71        }
72    }
73
74    /// Set the distance metric
75    pub fn with_metric(mut self, metric: DistanceMetric) -> Self {
76        self.metric = metric;
77        self
78    }
79
80    /// Set ef_construction
81    pub fn with_ef_construction(mut self, ef: usize) -> Self {
82        self.ef_construction = ef;
83        self
84    }
85
86    /// Set ef_search
87    pub fn with_ef_search(mut self, ef: usize) -> Self {
88        self.ef_search = ef;
89        self
90    }
91}
92
93/// A node in the HNSW graph
94#[derive(Debug, Clone)]
95struct HnswNode {
96    /// The node's ID
97    id: NodeId,
98    /// The node's vector data
99    vector: Vec<f32>,
100    /// Maximum layer this node appears in
101    max_layer: usize,
102    /// Connections at each layer (layer index -> neighbor IDs)
103    connections: Vec<Vec<NodeId>>,
104}
105
106impl HnswNode {
107    fn new(id: NodeId, vector: Vec<f32>, max_layer: usize) -> Self {
108        let mut connections = Vec::with_capacity(max_layer + 1);
109        for _ in 0..=max_layer {
110            connections.push(Vec::new());
111        }
112        Self {
113            id,
114            vector,
115            max_layer,
116            connections,
117        }
118    }
119}
120
121/// HNSW Index for approximate nearest neighbor search
122pub struct HnswIndex {
123    /// Configuration parameters
124    config: HnswConfig,
125    /// All nodes in the index
126    nodes: HashMap<NodeId, HnswNode>,
127    /// Entry point (node with highest layer)
128    entry_point: Option<NodeId>,
129    /// Maximum layer in the graph
130    max_layer: usize,
131    /// Vector dimension
132    dimension: usize,
133    /// Next available node ID
134    next_id: AtomicU64,
135    /// Simple RNG state for layer assignment
136    rng_state: u64,
137}
138
139impl HnswIndex {
140    /// Create a new empty HNSW index
141    pub fn new(dimension: usize, config: HnswConfig) -> Self {
142        Self {
143            config,
144            nodes: HashMap::new(),
145            entry_point: None,
146            max_layer: 0,
147            dimension,
148            next_id: AtomicU64::new(0),
149            rng_state: 0x853c49e6748fea9b, // Random seed
150        }
151    }
152
153    /// Create with default configuration
154    pub fn with_dimension(dimension: usize) -> Self {
155        Self::new(dimension, HnswConfig::default())
156    }
157
158    /// Get the number of vectors in the index
159    pub fn len(&self) -> usize {
160        self.nodes.len()
161    }
162
163    /// Check if the index is empty
164    pub fn is_empty(&self) -> bool {
165        self.nodes.is_empty()
166    }
167
168    /// Get the vector for a node ID
169    pub fn get_vector(&self, id: NodeId) -> Option<&[f32]> {
170        self.nodes.get(&id).map(|n| n.vector.as_slice())
171    }
172
173    /// Insert a vector and return its assigned ID
174    pub fn insert(&mut self, vector: Vec<f32>) -> NodeId {
175        let id = self.next_id.fetch_add(1, AtomicOrdering::SeqCst);
176        self.insert_with_id(id, vector);
177        id
178    }
179
180    /// Insert a vector with a specific ID
181    pub fn insert_with_id(&mut self, id: NodeId, vector: Vec<f32>) {
182        assert_eq!(
183            vector.len(),
184            self.dimension,
185            "Vector dimension mismatch: expected {}, got {}",
186            self.dimension,
187            vector.len()
188        );
189
190        // Assign random layer using exponential distribution
191        let node_layer = self.random_layer();
192
193        // Create the node
194        let node = HnswNode::new(id, vector, node_layer);
195
196        if self.entry_point.is_none() {
197            // First node - just add it
198            self.nodes.insert(id, node);
199            self.entry_point = Some(id);
200            self.max_layer = node_layer;
201            return;
202        }
203
204        let entry_point = self.entry_point.unwrap();
205
206        // We need to insert the node first so we can access its vector
207        // But we need to find neighbors first... let's clone the vector
208        let vector = node.vector.clone();
209        self.nodes.insert(id, node);
210
211        // Find entry point for search
212        let mut current = entry_point;
213
214        // Traverse from top layer down to node_layer + 1
215        // This finds the best entry point for the insertion layer
216        for layer in (node_layer + 1..=self.max_layer).rev() {
217            current = self.search_layer_single(&vector, current, layer);
218        }
219
220        // For layers from node_layer down to 0, find and connect to neighbors
221        for layer in (0..=node_layer.min(self.max_layer)).rev() {
222            // Find ef_construction nearest neighbors at this layer
223            let neighbors = self.search_layer(&vector, current, self.config.ef_construction, layer);
224
225            // Select M best neighbors
226            let m = if layer == 0 {
227                self.config.m_max0
228            } else {
229                self.config.m
230            };
231            let selected: Vec<NodeId> = neighbors.into_iter().take(m).map(|r| r.id).collect();
232
233            // Connect node to selected neighbors
234            if let Some(node) = self.nodes.get_mut(&id) {
235                node.connections[layer] = selected.clone();
236            }
237
238            // Add bidirectional connections
239            for &neighbor_id in &selected {
240                self.add_connection(neighbor_id, id, layer);
241            }
242
243            // Update current for next layer
244            if let Some(&first) = selected.first() {
245                current = first;
246            }
247        }
248
249        // Update entry point if new node has higher layer
250        if node_layer > self.max_layer {
251            self.entry_point = Some(id);
252            self.max_layer = node_layer;
253        }
254    }
255
256    /// Search for k nearest neighbors
257    pub fn search(&self, query: &[f32], k: usize) -> Vec<DistanceResult> {
258        self.search_with_ef(query, k, self.config.ef_search)
259    }
260
261    /// Search with custom ef parameter
262    pub fn search_with_ef(&self, query: &[f32], k: usize, ef: usize) -> Vec<DistanceResult> {
263        if self.entry_point.is_none() {
264            return Vec::new();
265        }
266
267        let entry_point = self.entry_point.unwrap();
268        let mut current = entry_point;
269
270        // Traverse from top layer down to layer 1
271        for layer in (1..=self.max_layer).rev() {
272            current = self.search_layer_single(query, current, layer);
273        }
274
275        // Search layer 0 with ef candidates
276        let candidates = self.search_layer(query, current, ef.max(k), 0);
277
278        // Return top k
279        candidates.into_iter().take(k).collect()
280    }
281
282    /// Search with a filter (bitset of allowed IDs)
283    pub fn search_filtered(
284        &self,
285        query: &[f32],
286        k: usize,
287        filter: &HashSet<NodeId>,
288    ) -> Vec<DistanceResult> {
289        self.search_filtered_with_ef(query, k, filter, self.config.ef_search)
290    }
291
292    /// Search with filter and custom ef
293    pub fn search_filtered_with_ef(
294        &self,
295        query: &[f32],
296        k: usize,
297        filter: &HashSet<NodeId>,
298        ef: usize,
299    ) -> Vec<DistanceResult> {
300        if self.entry_point.is_none() || filter.is_empty() {
301            return Vec::new();
302        }
303
304        let entry_point = self.entry_point.unwrap();
305        let mut current = entry_point;
306
307        // Traverse from top layer down to layer 1
308        for layer in (1..=self.max_layer).rev() {
309            current = self.search_layer_single(query, current, layer);
310        }
311
312        // Search layer 0 with filter
313        // We need to search with higher ef to account for filtered results
314        let expanded_ef = (ef * 2).max(k * 4);
315        let candidates = self.search_layer(query, current, expanded_ef, 0);
316
317        // Filter and return top k
318        candidates
319            .into_iter()
320            .filter(|r| filter.contains(&r.id))
321            .take(k)
322            .collect()
323    }
324
325    // =========================================================================
326    // Private methods
327    // =========================================================================
328
329    /// Generate a random layer using exponential distribution
330    fn random_layer(&mut self) -> usize {
331        // Simple xorshift64 PRNG
332        self.rng_state ^= self.rng_state << 13;
333        self.rng_state ^= self.rng_state >> 7;
334        self.rng_state ^= self.rng_state << 17;
335
336        // Convert to uniform [0, 1)
337        let uniform = (self.rng_state as f64) / (u64::MAX as f64);
338
339        // Exponential distribution: -ln(uniform) * ml
340
341        (-uniform.ln() * self.config.ml).floor() as usize
342    }
343
344    /// Search a single layer for the closest node (greedy)
345    fn search_layer_single(&self, query: &[f32], entry: NodeId, layer: usize) -> NodeId {
346        let mut current = entry;
347        let mut current_dist = self.compute_distance(query, current);
348
349        loop {
350            let mut changed = false;
351
352            if let Some(node) = self.nodes.get(&current) {
353                if layer < node.connections.len() {
354                    for &neighbor in &node.connections[layer] {
355                        let dist = self.compute_distance(query, neighbor);
356                        if dist < current_dist {
357                            current_dist = dist;
358                            current = neighbor;
359                            changed = true;
360                        }
361                    }
362                }
363            }
364
365            if !changed {
366                break;
367            }
368        }
369
370        current
371    }
372
373    /// Search a layer for ef nearest neighbors
374    fn search_layer(
375        &self,
376        query: &[f32],
377        entry: NodeId,
378        ef: usize,
379        layer: usize,
380    ) -> Vec<DistanceResult> {
381        let entry_dist = self.compute_distance(query, entry);
382
383        // Candidates: min-heap of nodes to explore (closest first)
384        let mut candidates: BinaryHeap<Reverse<DistanceResult>> = BinaryHeap::new();
385        candidates.push(Reverse(DistanceResult::new(entry, entry_dist)));
386
387        // Results: max-heap of found neighbors (furthest first for pruning)
388        let mut results: BinaryHeap<ReverseDistanceResult> = BinaryHeap::new();
389        results.push(ReverseDistanceResult(DistanceResult::new(
390            entry, entry_dist,
391        )));
392
393        // Visited set
394        let mut visited: HashSet<NodeId> = HashSet::new();
395        visited.insert(entry);
396
397        while let Some(Reverse(current)) = candidates.pop() {
398            // Get furthest result distance for pruning
399            let furthest_dist = results.peek().map(|r| r.0.distance).unwrap_or(f32::MAX);
400
401            // If current is further than our furthest result, we're done
402            if current.distance > furthest_dist {
403                break;
404            }
405
406            // Explore neighbors
407            if let Some(node) = self.nodes.get(&current.id) {
408                if layer < node.connections.len() {
409                    for &neighbor_id in &node.connections[layer] {
410                        if visited.contains(&neighbor_id) {
411                            continue;
412                        }
413                        visited.insert(neighbor_id);
414
415                        let dist = self.compute_distance(query, neighbor_id);
416                        let furthest_dist =
417                            results.peek().map(|r| r.0.distance).unwrap_or(f32::MAX);
418
419                        // If this neighbor is closer than our furthest result, or we need more results
420                        if dist < furthest_dist || results.len() < ef {
421                            candidates.push(Reverse(DistanceResult::new(neighbor_id, dist)));
422                            results.push(ReverseDistanceResult(DistanceResult::new(
423                                neighbor_id,
424                                dist,
425                            )));
426
427                            // Prune results to ef size
428                            while results.len() > ef {
429                                results.pop();
430                            }
431                        }
432                    }
433                }
434            }
435        }
436
437        // Convert results to sorted vector (closest first)
438        let mut result_vec: Vec<DistanceResult> = results.into_iter().map(|r| r.0).collect();
439        result_vec.sort_by(|a, b| cmp_f32(a.distance, b.distance).then_with(|| a.id.cmp(&b.id)));
440        result_vec
441    }
442
443    /// Add a bidirectional connection, pruning if necessary
444    fn add_connection(&mut self, from: NodeId, to: NodeId, layer: usize) {
445        let max_connections = if layer == 0 {
446            self.config.m_max0
447        } else {
448            self.config.m
449        };
450
451        if let Some(node) = self.nodes.get_mut(&from) {
452            // Ensure we have enough layers
453            while node.connections.len() <= layer {
454                node.connections.push(Vec::new());
455            }
456
457            // Add connection if not already present
458            if !node.connections[layer].contains(&to) {
459                node.connections[layer].push(to);
460
461                // Prune if too many connections
462                if node.connections[layer].len() > max_connections {
463                    self.prune_connections(from, layer, max_connections);
464                }
465            }
466        }
467    }
468
469    /// Prune connections to max_connections using simple heuristic
470    fn prune_connections(&mut self, node_id: NodeId, layer: usize, max_connections: usize) {
471        let node_vector = self.nodes.get(&node_id).map(|n| n.vector.clone());
472        let node_vector = match node_vector {
473            Some(v) => v,
474            None => return,
475        };
476
477        // Get all neighbors with distances
478        let neighbors: Vec<NodeId> = self
479            .nodes
480            .get(&node_id)
481            .map(|n| n.connections[layer].clone())
482            .unwrap_or_default();
483
484        let mut scored: Vec<DistanceResult> = neighbors
485            .iter()
486            .map(|&neighbor_id| {
487                let dist = self.compute_distance(&node_vector, neighbor_id);
488                DistanceResult::new(neighbor_id, dist)
489            })
490            .collect();
491
492        // Sort by distance (closest first)
493        scored.sort_by(|a, b| cmp_f32(a.distance, b.distance).then_with(|| a.id.cmp(&b.id)));
494
495        // Keep only max_connections closest
496        let kept: Vec<NodeId> = scored
497            .into_iter()
498            .take(max_connections)
499            .map(|r| r.id)
500            .collect();
501
502        if let Some(node) = self.nodes.get_mut(&node_id) {
503            node.connections[layer] = kept;
504        }
505    }
506
507    /// Compute distance between query and a node (SIMD-accelerated)
508    fn compute_distance(&self, query: &[f32], node_id: NodeId) -> f32 {
509        match self.nodes.get(&node_id) {
510            Some(node) => distance_simd(query, &node.vector, self.config.metric),
511            None => f32::MAX,
512        }
513    }
514
515    // ========================================================================
516    // Batch Operations
517    // ========================================================================
518
519    /// Insert multiple vectors at once
520    ///
521    /// More efficient than individual inserts for large batches
522    pub fn insert_batch(&mut self, vectors: Vec<Vec<f32>>) -> Vec<NodeId> {
523        vectors.into_iter().map(|v| self.insert(v)).collect()
524    }
525
526    /// Insert multiple vectors with specific IDs
527    pub fn insert_batch_with_ids(&mut self, items: Vec<(NodeId, Vec<f32>)>) {
528        for (id, vector) in items {
529            self.insert_with_id(id, vector);
530        }
531    }
532
533    // ========================================================================
534    // Delete Operations
535    // ========================================================================
536
537    /// Remove a node from the index
538    ///
539    /// Note: This is a "soft" delete - the node is marked as deleted and
540    /// excluded from search results, but connections are not fully repaired.
541    /// For better performance after many deletes, rebuild the index.
542    pub fn delete(&mut self, id: NodeId) -> bool {
543        if self.nodes.remove(&id).is_none() {
544            return false;
545        }
546
547        // Update entry point if we deleted it
548        if self.entry_point == Some(id) {
549            self.entry_point = self.nodes.keys().next().copied();
550
551            // Update max_layer based on new entry point
552            if let Some(ep) = self.entry_point {
553                self.max_layer = self.nodes.get(&ep).map(|n| n.max_layer).unwrap_or(0);
554            } else {
555                self.max_layer = 0;
556            }
557        }
558
559        // Remove references to this node from other nodes' connections
560        for node in self.nodes.values_mut() {
561            for layer_connections in node.connections.iter_mut() {
562                layer_connections.retain(|&neighbor| neighbor != id);
563            }
564        }
565
566        true
567    }
568
569    /// Check if a node exists
570    pub fn contains(&self, id: NodeId) -> bool {
571        self.nodes.contains_key(&id)
572    }
573
574    // ========================================================================
575    // Adaptive Search
576    // ========================================================================
577
578    /// Search with adaptive ef based on index size
579    ///
580    /// Automatically adjusts ef_search based on the number of results needed
581    /// and the size of the index for better accuracy/speed tradeoff.
582    pub fn search_adaptive(&self, query: &[f32], k: usize) -> Vec<DistanceResult> {
583        // Adaptive ef: at least k, scales with log of index size
584        let n = self.nodes.len();
585        let adaptive_ef = if n < 100 {
586            k.max(10)
587        } else if n < 10000 {
588            k.max(50)
589        } else if n < 100000 {
590            k.max(100)
591        } else {
592            k.max(200)
593        };
594
595        self.search_with_ef(query, k, adaptive_ef)
596    }
597
598    // ========================================================================
599    // Index Statistics
600    // ========================================================================
601
602    /// Get statistics about the index
603    pub fn stats(&self) -> HnswStats {
604        let mut layer_counts = vec![0usize; self.max_layer + 1];
605        let mut total_connections = 0usize;
606        let mut max_connections = 0usize;
607        let mut min_connections = usize::MAX;
608
609        for node in self.nodes.values() {
610            for (layer, layer_count) in layer_counts.iter_mut().enumerate().take(node.max_layer + 1)
611            {
612                *layer_count += 1;
613                let conns = node.connections.get(layer).map(|c| c.len()).unwrap_or(0);
614                total_connections += conns;
615                max_connections = max_connections.max(conns);
616                if conns > 0 {
617                    min_connections = min_connections.min(conns);
618                }
619            }
620        }
621
622        if self.nodes.is_empty() {
623            min_connections = 0;
624        }
625
626        HnswStats {
627            node_count: self.nodes.len(),
628            dimension: self.dimension,
629            max_layer: self.max_layer,
630            layer_counts,
631            total_connections,
632            avg_connections: if self.nodes.is_empty() {
633                0.0
634            } else {
635                total_connections as f64 / self.nodes.len() as f64
636            },
637            max_connections,
638            min_connections,
639            entry_point: self.entry_point,
640        }
641    }
642
643    // ========================================================================
644    // Persistence
645    // ========================================================================
646
647    /// Serialize the index to bytes for storage.
648    ///
649    /// The `HNSW` payload byte layout is owned by `reddb-file` (ADR 0046); this
650    /// only projects the engine state into [`reddb_file::HnswIndexFrame`] and
651    /// maps the distance metric to its on-disk discriminant.
652    pub fn to_bytes(&self) -> Vec<u8> {
653        let metric = match self.config.metric {
654            DistanceMetric::L2 => 0,
655            DistanceMetric::Cosine => 1,
656            DistanceMetric::InnerProduct => 2,
657        };
658        let nodes = self
659            .nodes
660            .values()
661            .map(|node| reddb_file::HnswNodeFrame {
662                id: node.id,
663                max_layer: node.max_layer as u32,
664                vector: node.vector.clone(),
665                connections: node.connections.clone(),
666            })
667            .collect();
668        let frame = reddb_file::HnswIndexFrame {
669            dimension: self.dimension as u32,
670            m: self.config.m as u32,
671            m_max0: self.config.m_max0 as u32,
672            ef_construction: self.config.ef_construction as u32,
673            ef_search: self.config.ef_search as u32,
674            ml: self.config.ml,
675            metric,
676            max_layer: self.max_layer as u32,
677            entry_point: self.entry_point,
678            nodes,
679        };
680        reddb_file::encode_hnsw_index_frame(&frame)
681    }
682
683    /// Deserialize index from bytes via the `reddb-file` codec.
684    pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
685        let frame = reddb_file::decode_hnsw_index_frame(bytes).map_err(|e| e.to_string())?;
686
687        let metric = match frame.metric {
688            0 => DistanceMetric::L2,
689            1 => DistanceMetric::Cosine,
690            2 => DistanceMetric::InnerProduct,
691            _ => return Err("Invalid distance metric".to_string()),
692        };
693
694        let config = HnswConfig {
695            m: frame.m as usize,
696            m_max0: frame.m_max0 as usize,
697            ef_construction: frame.ef_construction as usize,
698            ef_search: frame.ef_search as usize,
699            ml: frame.ml,
700            metric,
701        };
702
703        let mut nodes = HashMap::new();
704        let mut max_id = 0u64;
705        for node in frame.nodes {
706            max_id = max_id.max(node.id);
707            nodes.insert(
708                node.id,
709                HnswNode {
710                    id: node.id,
711                    max_layer: node.max_layer as usize,
712                    vector: node.vector,
713                    connections: node.connections,
714                },
715            );
716        }
717
718        Ok(Self {
719            config,
720            nodes,
721            entry_point: frame.entry_point,
722            max_layer: frame.max_layer as usize,
723            dimension: frame.dimension as usize,
724            next_id: AtomicU64::new(max_id + 1),
725            rng_state: 12345, // Reset RNG
726        })
727    }
728}
729
730/// Statistics about an HNSW index
731#[derive(Debug, Clone)]
732pub struct HnswStats {
733    /// Number of vectors in the index
734    pub node_count: usize,
735    /// Vector dimension
736    pub dimension: usize,
737    /// Maximum layer in the graph
738    pub max_layer: usize,
739    /// Number of nodes per layer
740    pub layer_counts: Vec<usize>,
741    /// Total number of connections
742    pub total_connections: usize,
743    /// Average connections per node
744    pub avg_connections: f64,
745    /// Maximum connections on any node
746    pub max_connections: usize,
747    /// Minimum connections on any node
748    pub min_connections: usize,
749    /// Entry point node ID
750    pub entry_point: Option<NodeId>,
751}
752
753/// Bitset for efficient filtering
754#[derive(Debug, Clone)]
755pub struct Bitset {
756    bits: Vec<u64>,
757    len: usize,
758}
759
760impl Bitset {
761    /// Create a new bitset with capacity for n elements
762    pub fn with_capacity(n: usize) -> Self {
763        let num_words = n.div_ceil(64);
764        Self {
765            bits: vec![0; num_words],
766            len: n,
767        }
768    }
769
770    /// Create a bitset with all bits set
771    pub fn all(n: usize) -> Self {
772        let num_words = n.div_ceil(64);
773        let mut bits = vec![u64::MAX; num_words];
774
775        // Clear excess bits in last word
776        if !n.is_multiple_of(64) {
777            let last_idx = num_words - 1;
778            let valid_bits = n % 64;
779            bits[last_idx] = (1u64 << valid_bits) - 1;
780        }
781
782        Self { bits, len: n }
783    }
784
785    /// Set a bit
786    pub fn set(&mut self, idx: usize) {
787        if idx < self.len {
788            let word = idx / 64;
789            let bit = idx % 64;
790            self.bits[word] |= 1u64 << bit;
791        }
792    }
793
794    /// Clear a bit
795    pub fn clear(&mut self, idx: usize) {
796        if idx < self.len {
797            let word = idx / 64;
798            let bit = idx % 64;
799            self.bits[word] &= !(1u64 << bit);
800        }
801    }
802
803    /// Check if a bit is set
804    pub fn is_set(&self, idx: usize) -> bool {
805        if idx >= self.len {
806            return false;
807        }
808        let word = idx / 64;
809        let bit = idx % 64;
810        (self.bits[word] & (1u64 << bit)) != 0
811    }
812
813    /// Convert to HashSet for use with HNSW filter
814    pub fn to_hashset(&self) -> HashSet<NodeId> {
815        let mut set = HashSet::new();
816        for i in 0..self.len {
817            if self.is_set(i) {
818                set.insert(i as NodeId);
819            }
820        }
821        set
822    }
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828
829    fn random_vector(dim: usize, seed: u64) -> Vec<f32> {
830        let mut state = seed;
831        (0..dim)
832            .map(|_| {
833                state ^= state << 13;
834                state ^= state >> 7;
835                state ^= state << 17;
836                (state as f32) / (u64::MAX as f32)
837            })
838            .collect()
839    }
840
841    #[test]
842    fn test_empty_index() {
843        let index = HnswIndex::with_dimension(128);
844        assert!(index.is_empty());
845        assert_eq!(index.len(), 0);
846
847        let results = index.search(&vec![0.0; 128], 10);
848        assert!(results.is_empty());
849    }
850
851    #[test]
852    fn test_single_insert() {
853        let mut index = HnswIndex::with_dimension(3);
854        let id = index.insert(vec![1.0, 2.0, 3.0]);
855
856        assert_eq!(index.len(), 1);
857        assert!(!index.is_empty());
858        assert!(index.get_vector(id).is_some());
859    }
860
861    #[test]
862    fn test_exact_match() {
863        let mut index = HnswIndex::with_dimension(3);
864        index.insert(vec![1.0, 0.0, 0.0]);
865        index.insert(vec![0.0, 1.0, 0.0]);
866        index.insert(vec![0.0, 0.0, 1.0]);
867
868        // Search for exact match
869        let results = index.search(&[1.0, 0.0, 0.0], 1);
870        assert_eq!(results.len(), 1);
871        assert_eq!(results[0].distance, 0.0);
872    }
873
874    #[test]
875    fn test_nearest_neighbor() {
876        let mut index = HnswIndex::with_dimension(2);
877        index.insert_with_id(0, vec![0.0, 0.0]);
878        index.insert_with_id(1, vec![1.0, 0.0]);
879        index.insert_with_id(2, vec![2.0, 0.0]);
880        index.insert_with_id(3, vec![3.0, 0.0]);
881
882        // Search for something close to (0.9, 0.0)
883        let results = index.search(&[0.9, 0.0], 1);
884        assert_eq!(results.len(), 1);
885        assert_eq!(results[0].id, 1); // Should find (1.0, 0.0)
886    }
887
888    #[test]
889    fn test_k_nearest() {
890        let mut index = HnswIndex::with_dimension(2);
891        for i in 0..10 {
892            index.insert_with_id(i, vec![i as f32, 0.0]);
893        }
894
895        // Search for 3 nearest to (4.5, 0.0)
896        let results = index.search(&[4.5, 0.0], 3);
897        assert_eq!(results.len(), 3);
898
899        // Should find 4, 5, and either 3 or 6
900        let ids: HashSet<_> = results.iter().map(|r| r.id).collect();
901        assert!(ids.contains(&4));
902        assert!(ids.contains(&5));
903    }
904
905    #[test]
906    fn test_filtered_search() {
907        let mut index = HnswIndex::with_dimension(2);
908        for i in 0..10 {
909            index.insert_with_id(i, vec![i as f32, 0.0]);
910        }
911
912        // Only allow even IDs
913        let filter: HashSet<NodeId> = [0, 2, 4, 6, 8].iter().copied().collect();
914
915        // Search for nearest to (5.0, 0.0) with filter
916        let results = index.search_filtered(&[5.0, 0.0], 2, &filter);
917
918        // Should find 4 and 6 (closest even numbers to 5)
919        assert_eq!(results.len(), 2);
920        let ids: HashSet<_> = results.iter().map(|r| r.id).collect();
921        assert!(ids.contains(&4));
922        assert!(ids.contains(&6));
923    }
924
925    #[test]
926    fn test_cosine_distance() {
927        let config = HnswConfig::default().with_metric(DistanceMetric::Cosine);
928        let mut index = HnswIndex::new(3, config);
929
930        // Insert normalized vectors
931        index.insert_with_id(0, vec![1.0, 0.0, 0.0]);
932        index.insert_with_id(1, vec![0.0, 1.0, 0.0]);
933        index.insert_with_id(2, vec![0.707, 0.707, 0.0]); // 45 degrees
934
935        // Search for something at 45 degrees
936        let results = index.search(&[0.707, 0.707, 0.0], 1);
937        assert_eq!(results[0].id, 2);
938    }
939
940    #[test]
941    fn test_many_vectors() {
942        let dim = 64;
943        let n: usize = 1000;
944
945        let mut index = HnswIndex::with_dimension(dim);
946
947        // Insert many random vectors
948        for i in 0..n {
949            let vector = random_vector(dim, i as u64);
950            index.insert_with_id(i as u64, vector);
951        }
952
953        assert_eq!(index.len(), n);
954
955        // Search should return k results
956        let query = random_vector(dim, 12345);
957        let results = index.search(&query, 10);
958        assert_eq!(results.len(), 10);
959
960        // Results should be sorted by distance
961        for i in 1..results.len() {
962            assert!(results[i].distance >= results[i - 1].distance);
963        }
964    }
965
966    #[test]
967    fn test_bitset() {
968        let mut bs = Bitset::with_capacity(100);
969
970        bs.set(0);
971        bs.set(50);
972        bs.set(99);
973
974        assert!(bs.is_set(0));
975        assert!(bs.is_set(50));
976        assert!(bs.is_set(99));
977        assert!(!bs.is_set(1));
978        assert!(!bs.is_set(64));
979
980        bs.clear(50);
981        assert!(!bs.is_set(50));
982    }
983
984    #[test]
985    fn test_bitset_all() {
986        let bs = Bitset::all(100);
987
988        for i in 0..100 {
989            assert!(bs.is_set(i));
990        }
991        assert!(!bs.is_set(100)); // Out of bounds
992    }
993}