Skip to content

Commit 3713af9

Browse files
authored
Add: Expose stats() in Rust SDK (#768)
Closes #702 Co-Authored-By: Tang Donghai <72755185+tang-hi@users.noreply.github.com>
1 parent c6d634c commit 3713af9

3 files changed

Lines changed: 97 additions & 1 deletion

File tree

rust/lib.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,28 @@ MemoryStats NativeIndex::memory_stats() const {
272272
return result;
273273
}
274274

275+
static IndexStats to_index_stats(index_dense_t::stats_t const& stats) {
276+
IndexStats result;
277+
result.nodes = stats.nodes;
278+
result.edges = stats.edges;
279+
result.max_edges = stats.max_edges;
280+
result.allocated_bytes = stats.allocated_bytes;
281+
return result;
282+
}
283+
284+
IndexStats NativeIndex::stats() const { return to_index_stats(index_->stats()); }
285+
286+
IndexStats NativeIndex::stats_for_level(size_t level) const { return to_index_stats(index_->stats(level)); }
287+
288+
IndexStats NativeIndex::stats_per_level(rust::Slice<IndexStats> stats_per_level, size_t max_level) const {
289+
std::vector<index_dense_t::stats_t> per_level(max_level + 1);
290+
index_dense_t::stats_t aggregate = index_->stats(per_level.data(), max_level);
291+
size_t exported = std::min(stats_per_level.size(), per_level.size());
292+
for (size_t i = 0; i != exported; ++i)
293+
stats_per_level[i] = to_index_stats(per_level[i]);
294+
return to_index_stats(aggregate);
295+
}
296+
275297
char const* NativeIndex::hardware_acceleration() const { return index_->metric().isa_name(); }
276298

277299
void NativeIndex::save_to_buffer(rust::Slice<uint8_t> buffer) const {

rust/lib.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct Matches;
66
struct IndexOptions;
77
struct IndexMetadata;
88
struct MemoryStats;
9+
struct IndexStats;
910
enum class MetricKind;
1011
enum class ScalarKind;
1112

@@ -122,6 +123,9 @@ class NativeIndex {
122123
void reset() const;
123124
size_t memory_usage() const;
124125
MemoryStats memory_stats() const;
126+
IndexStats stats() const;
127+
IndexStats stats_for_level(size_t level) const;
128+
IndexStats stats_per_level(rust::Slice<IndexStats> stats_per_level, size_t max_level) const;
125129
char const* hardware_acceleration() const;
126130

127131
void save_to_buffer(rust::Slice<uint8_t> buffer) const;

rust/lib.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)