hermes_core/structures/vector/ivf/
soar.rs1use serde::{Deserialize, Serialize};
12
13const DEFAULT_SELECTIVE_SPILL_FRACTION: f32 = 0.30;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SoarConfig {
18 pub num_secondary: usize,
22 pub selective: bool,
24 pub spill_threshold: f32,
29}
30
31impl Default for SoarConfig {
32 fn default() -> Self {
33 Self {
34 num_secondary: 1,
35 selective: true,
36 spill_threshold: -DEFAULT_SELECTIVE_SPILL_FRACTION,
37 }
38 }
39}
40
41impl SoarConfig {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn with_secondary(num_secondary: usize) -> Self {
49 Self {
50 num_secondary: num_secondary.min(1),
51 ..Default::default()
52 }
53 }
54
55 pub fn selective(mut self, enabled: bool) -> Self {
57 self.selective = enabled;
58 self
59 }
60
61 pub fn threshold(mut self, threshold: f32) -> Self {
63 self.spill_threshold = threshold.max(0.0);
64 self
65 }
66
67 pub fn target_spill_fraction(mut self, fraction: f32) -> Self {
70 self.selective = true;
71 self.spill_threshold = -fraction.clamp(0.0, 1.0);
72 self
73 }
74
75 pub fn full() -> Self {
77 Self {
78 num_secondary: 1,
79 selective: false,
80 spill_threshold: 0.0,
81 }
82 }
83
84 pub fn aggressive() -> Self {
88 Self {
89 num_secondary: 1,
90 selective: false,
91 spill_threshold: 0.0,
92 }
93 }
94
95 pub(crate) fn calibration_target(&self) -> Option<f32> {
96 (self.selective && self.spill_threshold.is_sign_negative())
97 .then(|| (-self.spill_threshold).clamp(0.0, 1.0))
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct MultiAssignment {
104 pub primary_cluster: u32,
106 pub secondary_clusters: Vec<u32>,
108}
109
110impl MultiAssignment {
111 pub fn primary_only(cluster: u32) -> Self {
113 Self {
114 primary_cluster: cluster,
115 secondary_clusters: Vec::new(),
116 }
117 }
118
119 pub fn all_clusters(&self) -> impl Iterator<Item = u32> + '_ {
121 std::iter::once(self.primary_cluster).chain(self.secondary_clusters.iter().copied())
122 }
123
124 pub fn num_assignments(&self) -> usize {
126 1 + self.secondary_clusters.len()
127 }
128
129 pub fn is_spilled(&self) -> bool {
131 !self.secondary_clusters.is_empty()
132 }
133}
134
135#[allow(dead_code)]
137#[derive(Debug, Clone, Default)]
138pub struct SoarStats {
139 pub total_vectors: usize,
141 pub spilled_vectors: usize,
143 pub total_assignments: usize,
145}
146
147#[allow(dead_code)]
148impl SoarStats {
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 pub fn record(&mut self, assignment: &MultiAssignment) {
155 self.total_vectors += 1;
156 self.total_assignments += assignment.num_assignments();
157 if assignment.is_spilled() {
158 self.spilled_vectors += 1;
159 }
160 }
161
162 pub fn spill_ratio(&self) -> f32 {
164 if self.total_vectors == 0 {
165 0.0
166 } else {
167 self.spilled_vectors as f32 / self.total_vectors as f32
168 }
169 }
170
171 pub fn avg_assignments(&self) -> f32 {
173 if self.total_vectors == 0 {
174 0.0
175 } else {
176 self.total_assignments as f32 / self.total_vectors as f32
177 }
178 }
179
180 pub fn storage_factor(&self) -> f32 {
182 self.avg_assignments()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_soar_config_default() {
192 let config = SoarConfig::default();
193 assert_eq!(config.num_secondary, 1);
194 assert!(config.selective);
195 assert_eq!(config.calibration_target(), Some(0.30));
196 }
197
198 #[test]
199 fn explicit_threshold_and_target_budget_have_distinct_tags() {
200 let threshold = SoarConfig::new().threshold(0.42);
201 assert_eq!(threshold.calibration_target(), None);
202 assert_eq!(threshold.spill_threshold, 0.42);
203
204 let target = SoarConfig::new().target_spill_fraction(0.25);
205 assert_eq!(target.calibration_target(), Some(0.25));
206 }
207
208 #[test]
209 fn test_multi_assignment() {
210 let assignment = MultiAssignment {
211 primary_cluster: 5,
212 secondary_clusters: vec![2, 7],
213 };
214
215 assert_eq!(assignment.num_assignments(), 3);
216 assert!(assignment.is_spilled());
217
218 let all: Vec<u32> = assignment.all_clusters().collect();
219 assert_eq!(all, vec![5, 2, 7]);
220 }
221
222 #[test]
223 fn test_soar_stats() {
224 let mut stats = SoarStats::new();
225
226 stats.record(&MultiAssignment::primary_only(0));
228
229 stats.record(&MultiAssignment {
231 primary_cluster: 1,
232 secondary_clusters: vec![2],
233 });
234
235 assert_eq!(stats.total_vectors, 2);
236 assert_eq!(stats.spilled_vectors, 1);
237 assert_eq!(stats.total_assignments, 3);
238 assert_eq!(stats.spill_ratio(), 0.5);
239 assert_eq!(stats.avg_assignments(), 1.5);
240 }
241}