@@ -349,6 +349,20 @@ pub mod ffi {
349349 vectors_reserved : usize ,
350350 }
351351
352+ /// Graph statistics aggregated across all levels of the HNSW index:
353+ /// node and edge counts together with the memory used by the graph structure.
354+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Default ) ]
355+ struct IndexStats {
356+ /// Number of nodes (members) present in the graph.
357+ nodes : usize ,
358+ /// Total number of edges (neighbor links) across all levels.
359+ edges : usize ,
360+ /// Theoretical maximum number of edges given the connectivity, i.e. the edge capacity.
361+ max_edges : usize ,
362+ /// Memory allocated for the graph structure (node tapes), in bytes.
363+ allocated_bytes : usize ,
364+ }
365+
352366 /// The index options used to configure the dense index during creation.
353367 /// It contains the number of dimensions, the metric kind, the scalar kind,
354368 /// the connectivity, the expansion values, and the multi-flag.
@@ -544,6 +558,9 @@ pub mod ffi {
544558 pub fn compact ( self : & NativeIndex ) -> Result < ( ) > ;
545559 pub fn memory_usage ( self : & NativeIndex ) -> usize ;
546560 pub fn memory_stats ( self : & NativeIndex ) -> MemoryStats ;
561+ pub fn stats ( self : & NativeIndex ) -> IndexStats ;
562+ pub fn stats_for_level ( self : & NativeIndex , level : usize ) -> IndexStats ;
563+ pub fn stats_per_level ( self : & NativeIndex , stats_per_level : & mut [ IndexStats ] , max_level : usize ) -> IndexStats ;
547564 pub fn hardware_acceleration ( self : & NativeIndex ) -> * const c_char ;
548565
549566 pub fn save_to_buffer ( self : & NativeIndex , buffer : & mut [ u8 ] ) -> Result < ( ) > ;
@@ -553,7 +570,7 @@ pub mod ffi {
553570}
554571
555572// Re-export the FFI structs and enums at the crate root for easy access
556- pub use ffi:: { IndexMetadata , IndexOptions , MemoryStats , MetricKind , ScalarKind } ;
573+ pub use ffi:: { IndexMetadata , IndexOptions , MemoryStats , IndexStats , MetricKind , ScalarKind } ;
557574
558575/// Represents custom metric functions for calculating distances between vectors in various formats.
559576///
@@ -1743,6 +1760,27 @@ impl Index {
17431760 self . inner . memory_stats ( )
17441761 }
17451762
1763+ /// Returns graph statistics aggregated across all levels: node and edge
1764+ /// counts together with the memory used by the graph structure.
1765+ pub fn stats ( self : & Index ) -> ffi:: IndexStats {
1766+ self . inner . stats ( )
1767+ }
1768+
1769+ /// Returns graph statistics for the nodes present at the given zero-based
1770+ /// `level`, where `0` is the base level.
1771+ pub fn stats_for_level ( self : & Index , level : usize ) -> ffi:: IndexStats {
1772+ self . inner . stats_for_level ( level)
1773+ }
1774+
1775+ /// Returns per-level graph statistics for levels `0..=max_level`, where `0`
1776+ /// is the base level. The returned vector has `max_level + 1` entries, one
1777+ /// per level.
1778+ pub fn stats_per_level ( self : & Index , max_level : usize ) -> Vec < ffi:: IndexStats > {
1779+ let mut per_level = vec ! [ ffi:: IndexStats :: default ( ) ; max_level + 1 ] ;
1780+ self . inner . stats_per_level ( & mut per_level, max_level) ;
1781+ per_level
1782+ }
1783+
17461784 /// Saves the index to a specified file.
17471785 ///
17481786 /// # Arguments
@@ -2162,6 +2200,38 @@ mod tests {
21622200 assert_eq ! ( index. size( ) , 0 ) ;
21632201 }
21642202
2203+ #[ test]
2204+ fn stats_variants ( ) {
2205+ let options = IndexOptions {
2206+ dimensions : 4 ,
2207+ ..Default :: default ( )
2208+ } ;
2209+ let index = Index :: new ( & options) . unwrap ( ) ;
2210+ index. reserve ( 10 ) . unwrap ( ) ;
2211+ index. add ( 1 , & [ 0.1 , 0.2 , 0.3 , 0.4 ] ) . unwrap ( ) ;
2212+ index. add ( 2 , & [ 0.2 , 0.1 , 0.4 , 0.3 ] ) . unwrap ( ) ;
2213+ index. add ( 3 , & [ 0.3 , 0.4 , 0.1 , 0.2 ] ) . unwrap ( ) ;
2214+
2215+ // Aggregate statistics across all levels.
2216+ let all = index. stats ( ) ;
2217+ assert_eq ! ( all. nodes, 3 , "three members were added" ) ;
2218+ assert ! ( all. edges > 0 , "connected members should have edges" ) ;
2219+ assert ! ( all. edges <= all. max_edges, "edges never exceed the capacity" ) ;
2220+ assert ! ( all. allocated_bytes > 0 , "graph should occupy memory" ) ;
2221+
2222+ // Every member always lives on the base level (0).
2223+ let base = index. stats_for_level ( 0 ) ;
2224+ assert_eq ! ( base. nodes, 3 , "every member lives on the base level" ) ;
2225+
2226+ // Per-level breakdown for levels 0..=2 has `max_level + 1` entries.
2227+ let per_level = index. stats_per_level ( 2 ) ;
2228+ assert_eq ! ( per_level. len( ) , 3 , "max_level + 1 entries" ) ;
2229+ assert_eq ! ( per_level[ 0 ] . nodes, 3 , "base level holds all members" ) ;
2230+ // Higher levels are subsets, so node counts are non-increasing.
2231+ assert ! ( per_level[ 0 ] . nodes >= per_level[ 1 ] . nodes) ;
2232+ assert ! ( per_level[ 1 ] . nodes >= per_level[ 2 ] . nodes) ;
2233+ }
2234+
21652235 #[ test]
21662236 fn integration ( ) {
21672237 let mut options = IndexOptions {
0 commit comments