Skip to main content

selene_graph/vector_index/
memory.rs

1//! Memory and structural diagnostics for vector indexes.
2
3const BASIS_POINTS_DENOMINATOR: usize = 10_000;
4
5/// Minimum pending IVF entries before a rebuild recommendation can fire.
6///
7/// The first retrain-policy bench showed that very small novel clusters can
8/// be too little mass for retrained width-2 partitions even when the ratio is
9/// non-zero. This floor keeps the diagnostic from recommending tiny retrains.
10pub const IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES: usize = 100;
11
12/// Minimum pending IVF retrain ratio, scaled by 10,000, for rebuild advice.
13pub const IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS: usize = 100;
14
15/// Estimated resident memory and cardinality details for one vector index.
16///
17/// This is intentionally an estimate rather than allocator-exact accounting.
18/// `estimated_index_bytes` counts index-owned structures and excludes primary
19/// graph vector component allocations that ANN indexes may share through `Arc`
20/// handles. `estimated_reachable_bytes` adds the component bytes referenced by
21/// derived entries and centroids as an upper-bound view; deleted ANN entries can
22/// retain old component storage until the derived index is rebuilt.
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub struct VectorIndexMemoryUsage {
25    /// Number of live rows currently admitted to the index.
26    pub indexed_rows: u64,
27    /// Estimated heap bytes owned by the row bitmap.
28    pub row_bitmap_bytes: usize,
29    /// Roaring serialized size for the row bitmap.
30    pub row_bitmap_serialized_bytes: usize,
31    /// Estimated heap bytes owned by the HNSW derived index, excluding vector components.
32    pub hnsw_index_bytes: usize,
33    /// Component bytes reachable through HNSW vector handles.
34    pub hnsw_referenced_vector_bytes: usize,
35    /// Total HNSW entries, including stale deleted row versions.
36    pub hnsw_entries: usize,
37    /// Live HNSW entries reachable from row membership.
38    pub hnsw_live_entries: usize,
39    /// Stale HNSW entries retained for traversability after update/delete.
40    pub hnsw_deleted_entries: usize,
41    /// Stored directed HNSW links across all layers.
42    pub hnsw_link_count: usize,
43    /// Stored directed HNSW links in the level-0 layer.
44    pub hnsw_level_zero_link_count: usize,
45    /// Stored directed HNSW links above the level-0 layer.
46    pub hnsw_upper_layer_link_count: usize,
47    /// Maximum HNSW layer count attached to any indexed entry.
48    pub hnsw_max_layer_count: usize,
49    /// Maximum directed HNSW links stored in a single entry layer.
50    pub hnsw_max_links_per_layer: usize,
51    /// Average directed HNSW links per entry, scaled by 10,000.
52    pub hnsw_average_links_per_entry_basis_points: usize,
53    /// Estimated heap bytes owned by the IVF derived index, excluding vector components.
54    pub ivf_index_bytes: usize,
55    /// Component bytes reachable through IVF vector handles.
56    pub ivf_referenced_vector_bytes: usize,
57    /// Total IVF entries, including stale deleted row versions.
58    pub ivf_entries: usize,
59    /// Live IVF entries reachable from row membership.
60    pub ivf_live_entries: usize,
61    /// Stale IVF entries retained until the derived index is rebuilt.
62    pub ivf_deleted_entries: usize,
63    /// Number of trained IVF centroids.
64    pub ivf_centroids: usize,
65    /// Number of IVF inverted lists.
66    pub ivf_list_count: usize,
67    /// Number of IVF inverted lists with at least one assigned live entry.
68    pub ivf_non_empty_list_count: usize,
69    /// Maximum assigned live entries in one IVF inverted list.
70    pub ivf_max_list_len: usize,
71    /// Average assigned live entries per IVF inverted list, scaled by 10,000.
72    pub ivf_average_list_len_basis_points: usize,
73    /// Non-stale IVF entries assigned to inverted lists.
74    pub ivf_assigned_entries: usize,
75    /// Live IVF entries whose current vector was inserted or replaced after centroid training.
76    pub ivf_pending_retrain_entries: usize,
77    /// Estimated heap bytes owned by the TurboQuant derived index, excluding vector components.
78    pub turbo_quant_index_bytes: usize,
79    /// Component bytes reachable through TurboQuant-owned full-vector handles.
80    pub turbo_quant_referenced_vector_bytes: usize,
81    /// Total TurboQuant compressed entries.
82    pub turbo_quant_entries: usize,
83    /// Live TurboQuant row entries.
84    pub turbo_quant_live_entries: usize,
85    /// Stale TurboQuant entries retained by the derived index.
86    ///
87    /// TurboQuant compacts deletes and replacements immediately, so this should
88    /// normally remain zero.
89    pub turbo_quant_deleted_entries: usize,
90    /// Packed TurboQuant coordinate-code bytes.
91    pub turbo_quant_code_bytes: usize,
92    /// TurboQuant scalar codebook bytes.
93    pub turbo_quant_codebook_bytes: usize,
94    /// TurboQuant per-dimension calibration bytes.
95    pub turbo_quant_calibration_bytes: usize,
96    /// Estimated bytes for index-owned structures, excluding referenced vector components.
97    pub estimated_index_bytes: usize,
98    /// Estimated upper-bound bytes reachable from the index including ANN vector components.
99    pub estimated_reachable_bytes: usize,
100}
101
102impl VectorIndexMemoryUsage {
103    /// Return pending IVF retrain entries divided by live IVF entries, scaled by 10,000.
104    #[must_use]
105    pub fn ivf_pending_retrain_basis_points(&self) -> usize {
106        self.ivf_pending_retrain_entries
107            .saturating_mul(BASIS_POINTS_DENOMINATOR)
108            .checked_div(self.ivf_live_entries)
109            .unwrap_or_default()
110    }
111
112    /// Return true when the IVF index should be rebuilt by maintenance soon.
113    ///
114    /// The recommendation is deliberately diagnostic only: reads never rebuild
115    /// indexes, and callers still decide when to run `selene.rebuild_vector_indexes`.
116    /// Deleted IVF entries are not part of this first trigger because delete
117    /// maintenance already unlinks them from inverted lists; existing reclaimed
118    /// counters expose that memory-only pressure separately.
119    #[must_use]
120    pub fn ivf_rebuild_recommended(&self) -> bool {
121        self.ivf_pending_retrain_entries >= IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES
122            && self.ivf_pending_retrain_basis_points() >= IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::{
129        IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES, IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS,
130        VectorIndexMemoryUsage,
131    };
132
133    #[test]
134    fn ivf_pending_retrain_ratio_uses_live_entries() {
135        let usage = VectorIndexMemoryUsage {
136            ivf_live_entries: 10_000,
137            ivf_pending_retrain_entries: 100,
138            ..VectorIndexMemoryUsage::default()
139        };
140
141        assert_eq!(
142            usage.ivf_pending_retrain_basis_points(),
143            IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS
144        );
145    }
146
147    #[test]
148    fn ivf_rebuild_recommendation_requires_ratio_and_floor() {
149        let below_floor = VectorIndexMemoryUsage {
150            ivf_live_entries: 1_000,
151            ivf_pending_retrain_entries: IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES - 1,
152            ..VectorIndexMemoryUsage::default()
153        };
154        let below_ratio = VectorIndexMemoryUsage {
155            ivf_live_entries: 20_000,
156            ivf_pending_retrain_entries: IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES,
157            ..VectorIndexMemoryUsage::default()
158        };
159        let recommended = VectorIndexMemoryUsage {
160            ivf_live_entries: 10_000,
161            ivf_pending_retrain_entries: IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES,
162            ..VectorIndexMemoryUsage::default()
163        };
164
165        assert!(!below_floor.ivf_rebuild_recommended());
166        assert!(!below_ratio.ivf_rebuild_recommended());
167        assert!(recommended.ivf_rebuild_recommended());
168    }
169}