ruvector_hyperbolic_hnsw/lib.rs
1//! Hyperbolic Embeddings with HNSW Integration for RuVector
2//!
3//! This crate provides hyperbolic (Poincaré ball) embeddings integrated with
4//! HNSW (Hierarchical Navigable Small World) graphs for hierarchy-aware
5//! vector search.
6//!
7//! # Overview
8//!
9//! Hierarchies compress naturally in hyperbolic space. Taxonomies, catalogs,
10//! ICD trees, product facets, org charts, and long-tail tags all fit better
11//! than in Euclidean space, which means higher recall on deep leaves without
12//! blowing up memory or latency.
13//!
14//! # Key Features
15//!
16//! - **Poincaré Ball Model**: Store vectors in the Poincaré ball with proper
17//! geometric operations (Möbius addition, exp/log maps)
18//! - **Tangent Space Pruning**: Prune HNSW candidates with cheap Euclidean
19//! distance in tangent space before exact hyperbolic ranking
20//! - **Per-Shard Curvature**: Different parts of the hierarchy can have
21//! different optimal curvatures
22//! - **Dual-Space Index**: Keep a synchronized Euclidean index for fallback
23//! and mutual ranking fusion
24//!
25//! # Quick Start
26//!
27//! ```rust
28//! use ruvector_hyperbolic_hnsw::{HyperbolicHnsw, HyperbolicHnswConfig};
29//!
30//! // Create index with default settings
31//! let mut index = HyperbolicHnsw::default_config();
32//!
33//! // Insert vectors (automatically projected to Poincaré ball)
34//! index.insert(vec![0.1, 0.2, 0.3]).unwrap();
35//! index.insert(vec![-0.1, 0.15, 0.25]).unwrap();
36//! index.insert(vec![0.2, -0.1, 0.1]).unwrap();
37//!
38//! // Search for nearest neighbors
39//! let results = index.search(&[0.15, 0.1, 0.2], 2).unwrap();
40//! for r in results {
41//! println!("ID: {}, Distance: {:.4}", r.id, r.distance);
42//! }
43//! ```
44//!
45//! # HNSW Speed Trick
46//!
47//! The core optimization is:
48//! 1. Precompute `u = log_c(x)` at a shard centroid `c`
49//! 2. During neighbor selection, use Euclidean `||u_q - u_p||` to prune
50//! 3. Run exact Poincaré distance only on top N candidates before final ranking
51//!
52//! ```rust
53//! use ruvector_hyperbolic_hnsw::{HyperbolicHnsw, HyperbolicHnswConfig};
54//!
55//! let mut config = HyperbolicHnswConfig::default();
56//! config.use_tangent_pruning = true;
57//! config.prune_factor = 10; // Consider 10x candidates in tangent space
58//!
59//! let mut index = HyperbolicHnsw::new(config);
60//! // ... insert vectors ...
61//!
62//! // Build tangent cache for pruning optimization
63//! # index.insert(vec![0.1, 0.2]).unwrap();
64//! index.build_tangent_cache().unwrap();
65//!
66//! // Search with pruning
67//! let results = index.search_with_pruning(&[0.1, 0.15], 5).unwrap();
68//! ```
69//!
70//! # Sharded Index with Per-Shard Curvature
71//!
72//! ```rust
73//! use ruvector_hyperbolic_hnsw::{ShardedHyperbolicHnsw, ShardStrategy};
74//!
75//! let mut manager = ShardedHyperbolicHnsw::new(1.0);
76//!
77//! // Insert with hierarchy depth information
78//! manager.insert(vec![0.1, 0.2], Some(0)).unwrap(); // Root level
79//! manager.insert(vec![0.3, 0.1], Some(3)).unwrap(); // Deeper level
80//!
81//! // Update curvature for specific shard
82//! manager.update_curvature("radius_1", 0.5).unwrap();
83//!
84//! // Search across all shards
85//! let results = manager.search(&[0.2, 0.15], 5).unwrap();
86//! ```
87//!
88//! # Mathematical Operations
89//!
90//! The `poincare` module provides low-level hyperbolic geometry operations:
91//!
92//! ```rust
93//! use ruvector_hyperbolic_hnsw::poincare::{
94//! mobius_add, exp_map, log_map, poincare_distance, project_to_ball
95//! };
96//!
97//! let x = vec![0.3, 0.2];
98//! let y = vec![-0.1, 0.4];
99//! let c = 1.0; // Curvature
100//!
101//! // Möbius addition (hyperbolic vector addition)
102//! let z = mobius_add(&x, &y, c);
103//!
104//! // Geodesic distance in hyperbolic space
105//! let d = poincare_distance(&x, &y, c);
106//!
107//! // Map to tangent space at x
108//! let v = log_map(&y, &x, c);
109//!
110//! // Map back to manifold
111//! let y_recovered = exp_map(&v, &x, c);
112//! ```
113//!
114//! # Numerical Stability
115//!
116//! All operations include numerical safeguards:
117//! - Norm clamping with `eps = 1e-5`
118//! - Projection after every update
119//! - Stable `acosh` and `log1p` implementations
120//!
121//! # Feature Flags
122//!
123//! - `simd`: Enable SIMD acceleration (default)
124//! - `parallel`: Enable parallel processing with rayon (default)
125//! - `wasm`: Enable WebAssembly compatibility
126
127pub mod error;
128pub mod hnsw;
129pub mod poincare;
130pub mod shard;
131pub mod tangent;
132
133// Re-exports
134pub use error::{HyperbolicError, HyperbolicResult};
135pub use hnsw::{
136 DistanceMetric, DualSpaceIndex, HnswNode, HyperbolicHnsw, HyperbolicHnswConfig, SearchResult,
137};
138pub use poincare::{
139 conformal_factor, conformal_factor_from_norm_sq, dot, exp_map, frechet_mean, fused_norms,
140 hyperbolic_midpoint, log_map, log_map_at_centroid, mobius_add, mobius_add_inplace,
141 mobius_scalar_mult, norm, norm_squared, parallel_transport, poincare_distance,
142 poincare_distance_batch, poincare_distance_from_norms, poincare_distance_squared,
143 project_to_ball, project_to_ball_inplace, PoincareConfig, DEFAULT_CURVATURE, EPS,
144};
145pub use shard::{
146 CurvatureRegistry, HierarchyMetrics, HyperbolicShard, ShardCurvature, ShardStrategy,
147 ShardedHyperbolicHnsw,
148};
149pub use tangent::{tangent_micro_update, PrunedCandidate, TangentCache, TangentPruner};
150
151/// Library version
152pub const VERSION: &str = env!("CARGO_PKG_VERSION");
153
154/// Prelude for common imports
155pub mod prelude {
156 pub use crate::error::{HyperbolicError, HyperbolicResult};
157 pub use crate::hnsw::{HyperbolicHnsw, HyperbolicHnswConfig, SearchResult};
158 pub use crate::poincare::{exp_map, log_map, mobius_add, poincare_distance, project_to_ball};
159 pub use crate::shard::{ShardedHyperbolicHnsw, ShardStrategy};
160 pub use crate::tangent::{TangentCache, TangentPruner};
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_basic_workflow() {
169 // Create index
170 let mut index = HyperbolicHnsw::default_config();
171
172 // Insert vectors
173 for i in 0..10 {
174 let v = vec![0.1 * i as f32, 0.05 * i as f32, 0.02 * i as f32];
175 index.insert(v).unwrap();
176 }
177
178 // Search
179 let query = vec![0.35, 0.175, 0.07];
180 let results = index.search(&query, 3).unwrap();
181
182 assert_eq!(results.len(), 3);
183 // Results should be sorted by distance
184 for i in 1..results.len() {
185 assert!(results[i - 1].distance <= results[i].distance);
186 }
187 }
188
189 #[test]
190 fn test_hierarchy_preservation() {
191 // Create points at different "depths"
192 let points: Vec<Vec<f32>> = (0..20)
193 .map(|i| {
194 // Points further from origin represent deeper hierarchy
195 let depth = i / 4;
196 let radius = 0.1 + 0.15 * depth as f32;
197 let angle = (i % 4) as f32 * std::f32::consts::PI / 2.0;
198 vec![radius * angle.cos(), radius * angle.sin()]
199 })
200 .collect();
201
202 let depths: Vec<usize> = (0..20).map(|i| i / 4).collect();
203
204 // Compute metrics
205 let metrics = HierarchyMetrics::compute(&points, &depths, 1.0).unwrap();
206
207 // Radius should correlate positively with depth
208 assert!(metrics.radius_depth_correlation > 0.5);
209 }
210}