sochdb_vector/query/
controller.rs1use crate::config::QueryConfig;
6use crate::types::*;
7
8pub struct AdaptiveController {
10 config: QueryConfig,
11}
12
13impl AdaptiveController {
14 pub fn new(config: QueryConfig) -> Self {
16 Self { config }
17 }
18
19 pub fn compute_confidence(&self, results: &[ScoredCandidate], k: usize) -> ConfidenceSignals {
21 if results.is_empty() {
22 return ConfidenceSignals {
23 score_gap: 0.0,
24 entropy: 0.0,
25 coverage: 0.0,
26 confidence: 0.0,
27 };
28 }
29
30 let score_gap = if results.len() > k {
32 let k_score = results.get(k - 1).map(|c| c.score).unwrap_or(0.0);
33 let two_k_score = results.get(2 * k - 1).map(|c| c.score).unwrap_or(0.0);
34 (k_score - two_k_score).abs()
35 } else {
36 0.0
38 };
39
40 let entropy = self.compute_entropy(&results[..k.min(results.len())]);
42
43 let coverage = (results.len() as f32) / (k as f32).max(1.0);
45
46 let confidence = self.combine_signals(score_gap, entropy, coverage);
48
49 ConfidenceSignals {
50 score_gap,
51 entropy,
52 coverage,
53 confidence,
54 }
55 }
56
57 fn compute_entropy(&self, results: &[ScoredCandidate]) -> f32 {
59 if results.is_empty() {
60 return 0.0;
61 }
62
63 let scores: Vec<f32> = results.iter().map(|c| c.score.max(0.001)).collect();
64 let sum: f32 = scores.iter().sum();
65
66 if sum <= 0.0 {
67 return 0.0;
68 }
69
70 let probs: Vec<f32> = scores.iter().map(|s| s / sum).collect();
71 let entropy: f32 = probs
72 .iter()
73 .filter(|&&p| p > 0.0)
74 .map(|&p| -p * p.ln())
75 .sum();
76
77 let max_entropy = (results.len() as f32).ln();
79 if max_entropy > 0.0 {
80 entropy / max_entropy
81 } else {
82 0.0
83 }
84 }
85
86 fn combine_signals(&self, score_gap: f32, entropy: f32, coverage: f32) -> f32 {
88 let gap_signal = (score_gap / self.config.score_gap_threshold).min(1.0);
90
91 let entropy_signal = 1.0 - entropy;
93
94 let coverage_signal = coverage.min(1.0);
96
97 0.4 * gap_signal + 0.3 * entropy_signal + 0.3 * coverage_signal
99 }
100
101 pub fn should_widen(&self, confidence: f32) -> bool {
103 confidence < 0.5 }
105
106 pub fn compute_widening(
108 &self,
109 signals: &ConfidenceSignals,
110 _params: &QueryParams,
111 ) -> WideningParams {
112 if signals.confidence >= 0.5 {
113 return WideningParams::none();
114 }
115
116 let factor = self.config.widening_factor;
117
118 if signals.confidence < 0.2 {
120 WideningParams {
122 l_a_factor: factor * 2.0,
123 l_b_factor: factor * 2.0,
124 r_factor: factor,
125 router_probes_factor: 2.0,
126 }
127 } else if signals.confidence < 0.35 {
128 WideningParams {
130 l_a_factor: factor,
131 l_b_factor: factor * 1.5,
132 r_factor: 1.0,
133 router_probes_factor: 1.5,
134 }
135 } else {
136 WideningParams {
138 l_a_factor: 1.0,
139 l_b_factor: factor,
140 r_factor: 1.0,
141 router_probes_factor: 1.0,
142 }
143 }
144 }
145
146 pub fn apply_filter_widening(
148 &self,
149 params: &mut QueryParams,
150 selectivity: f32,
151 max_factor: f32,
152 ) {
153 if selectivity >= 1.0 || selectivity <= 0.0 {
154 return;
155 }
156
157 let factor = (1.0 / selectivity).min(max_factor);
158 params.l_a = ((params.l_a as f32) * factor) as usize;
159 params.l_b = ((params.l_b as f32) * factor) as usize;
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct ConfidenceSignals {
166 pub score_gap: f32,
168 pub entropy: f32,
170 pub coverage: f32,
172 pub confidence: f32,
174}
175
176#[derive(Debug, Clone)]
178pub struct WideningParams {
179 pub l_a_factor: f32,
180 pub l_b_factor: f32,
181 pub r_factor: f32,
182 pub router_probes_factor: f32,
183}
184
185impl WideningParams {
186 pub fn none() -> Self {
187 Self {
188 l_a_factor: 1.0,
189 l_b_factor: 1.0,
190 r_factor: 1.0,
191 router_probes_factor: 1.0,
192 }
193 }
194
195 pub fn is_identity(&self) -> bool {
196 self.l_a_factor == 1.0
197 && self.l_b_factor == 1.0
198 && self.r_factor == 1.0
199 && self.router_probes_factor == 1.0
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_confidence_high() {
209 let config = QueryConfig::default();
210 let controller = AdaptiveController::new(config);
211
212 let results: Vec<ScoredCandidate> = (0..20)
214 .map(|i| ScoredCandidate {
215 id: i as u32,
216 score: 1.0 - (i as f32) * 0.05,
217 })
218 .collect();
219
220 let signals = controller.compute_confidence(&results, 10);
221 assert!(
222 signals.confidence > 0.5,
223 "Expected high confidence: {}",
224 signals.confidence
225 );
226 assert!(!controller.should_widen(signals.confidence));
227 }
228
229 #[test]
230 fn test_confidence_low() {
231 let config = QueryConfig::default();
232 let controller = AdaptiveController::new(config);
233
234 let results: Vec<ScoredCandidate> = (0..20)
236 .map(|i| ScoredCandidate {
237 id: i as u32,
238 score: 1.0, })
240 .collect();
241
242 let signals = controller.compute_confidence(&results, 10);
243 assert!(
245 signals.entropy > 0.8,
246 "Expected high entropy: {}",
247 signals.entropy
248 );
249 }
250
251 #[test]
252 fn test_widening_params() {
253 let config = QueryConfig::default();
254 let controller = AdaptiveController::new(config);
255 let params = QueryParams::default();
256
257 let low_confidence = ConfidenceSignals {
258 score_gap: 0.01,
259 entropy: 0.9,
260 coverage: 0.5,
261 confidence: 0.2,
262 };
263
264 let widening = controller.compute_widening(&low_confidence, ¶ms);
265 assert!(widening.l_b_factor > 1.0);
266 assert!(widening.l_a_factor > 1.0);
267 }
268
269 #[test]
270 fn test_filter_widening() {
271 let config = QueryConfig::default();
272 let controller = AdaptiveController::new(config);
273
274 let mut params = QueryParams {
275 k: 10,
276 l_a: 1000,
277 l_b: 2000,
278 ..Default::default()
279 };
280
281 controller.apply_filter_widening(&mut params, 0.1, 5.0);
283 assert_eq!(params.l_a, 5000);
284 assert_eq!(params.l_b, 10000);
285 }
286}