velesdb_core/collection/query_cost/
mod.rs1#![allow(clippy::cast_precision_loss)]
37#![allow(clippy::cast_possible_truncation)]
38#![allow(clippy::cast_sign_loss)]
39
40use std::fmt;
41
42pub mod calibration;
43pub mod cost_factors;
44pub mod cost_model;
45pub(crate) mod feedback;
46pub mod plan_generator;
47pub mod query_executor;
48
49#[cfg(test)]
50mod plan_generator_tests;
51#[cfg(test)]
52mod tests;
53
54pub use cost_model::{CostEstimator, OperationCost, OperationCostFactors};
55pub(crate) use feedback::CboFeedbackLoop;
56pub use plan_generator::{CandidatePlan, PlanGenerator, QueryCharacteristics};
57pub use query_executor::{ExecutionContext, PlanCache, QueryOptimizer};
58
59#[derive(Debug, Clone)]
61pub struct QueryParams {
62 pub dataset_size: usize,
64 pub ef_search: usize,
66 pub top_k: usize,
68 pub filter_selectivity: Option<f64>,
71}
72
73impl Default for QueryParams {
74 fn default() -> Self {
75 Self {
76 dataset_size: 10_000,
77 ef_search: 128,
78 top_k: 10,
79 filter_selectivity: None,
80 }
81 }
82}
83
84impl QueryParams {
85 #[must_use]
87 pub fn new(dataset_size: usize, ef_search: usize, top_k: usize) -> Self {
88 Self {
89 dataset_size,
90 ef_search,
91 top_k,
92 filter_selectivity: None,
93 }
94 }
95
96 #[must_use]
98 pub fn with_filter_selectivity(mut self, selectivity: f64) -> Self {
99 self.filter_selectivity = Some(selectivity.clamp(0.001, 1.0));
100 self
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct CostFactors {
107 pub dataset_size_factor: f64,
109 pub ef_search_factor: f64,
111 pub filter_selectivity_factor: f64,
113 pub top_k_factor: f64,
115}
116
117impl Default for CostFactors {
118 fn default() -> Self {
119 Self {
120 dataset_size_factor: 1.0,
121 ef_search_factor: 1.0,
122 filter_selectivity_factor: 1.0,
123 top_k_factor: 1.0,
124 }
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct QueryCostEstimate {
131 pub total_cost: f64,
133 pub estimated_latency_ms: f64,
135 pub factors: CostFactors,
137}
138
139impl QueryCostEstimate {
140 #[must_use]
142 pub fn new(total_cost: f64, estimated_latency_ms: f64, factors: CostFactors) -> Self {
143 Self {
144 total_cost,
145 estimated_latency_ms,
146 factors,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
153pub struct CostCalibration {
154 pub base_cost: f64,
156 pub reference_ef_search: f64,
158 pub reference_top_k: f64,
160 pub ms_per_cost_unit: f64,
162 pub filter_exponent: f64,
164}
165
166impl Default for CostCalibration {
167 fn default() -> Self {
168 Self {
169 base_cost: 1.0,
170 reference_ef_search: 100.0,
171 reference_top_k: 10.0,
172 ms_per_cost_unit: 0.1,
173 filter_exponent: 0.3,
174 }
175 }
176}
177
178impl CostCalibration {
179 #[must_use]
181 pub fn fast_system() -> Self {
182 Self {
183 ms_per_cost_unit: 0.05,
184 ..Default::default()
185 }
186 }
187
188 #[must_use]
190 pub fn slow_system() -> Self {
191 Self {
192 ms_per_cost_unit: 0.2,
193 ..Default::default()
194 }
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct QueryCostExceeded {
201 pub estimated: f64,
203 pub max_allowed: f64,
205}
206
207impl fmt::Display for QueryCostExceeded {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 write!(
210 f,
211 "Query cost {:.1} exceeds limit {:.1}",
212 self.estimated, self.max_allowed
213 )
214 }
215}
216
217impl std::error::Error for QueryCostExceeded {}
218
219#[derive(Debug, Clone)]
221pub struct QueryCostEstimator {
222 calibration: CostCalibration,
224 max_cost: Option<f64>,
226}
227
228impl Default for QueryCostEstimator {
229 fn default() -> Self {
230 Self::new(CostCalibration::default())
231 }
232}
233
234impl QueryCostEstimator {
235 #[must_use]
237 pub fn new(calibration: CostCalibration) -> Self {
238 Self {
239 calibration,
240 max_cost: None,
241 }
242 }
243
244 #[must_use]
246 pub fn with_max_cost(mut self, max_cost: f64) -> Self {
247 self.max_cost = Some(max_cost);
248 self
249 }
250
251 pub fn set_max_cost(&mut self, max_cost: Option<f64>) {
253 self.max_cost = max_cost;
254 }
255
256 #[must_use]
258 pub fn max_cost(&self) -> Option<f64> {
259 self.max_cost
260 }
261
262 #[must_use]
272 pub fn with_feedback(mut self, ms_per_cost_unit: f64) -> Self {
273 self.calibration.ms_per_cost_unit = ms_per_cost_unit;
274 self
275 }
276
277 #[must_use]
289 pub fn estimate(&self, params: &QueryParams) -> QueryCostEstimate {
290 let cal = &self.calibration;
291
292 let dataset_size_factor = if params.dataset_size > 0 {
294 (params.dataset_size as f64 + 1.0).log2()
295 } else {
296 1.0
297 };
298
299 let ef_search_factor = params.ef_search as f64 / cal.reference_ef_search;
301
302 let top_k_factor = (params.top_k as f64 / cal.reference_top_k).sqrt();
304
305 let selectivity = params.filter_selectivity.unwrap_or(1.0).max(0.001);
307 let filter_selectivity_factor = (1.0 / selectivity).powf(cal.filter_exponent);
308
309 let total_cost = cal.base_cost
311 * dataset_size_factor
312 * ef_search_factor
313 * top_k_factor
314 * filter_selectivity_factor;
315
316 let estimated_latency_ms = total_cost * cal.ms_per_cost_unit;
318
319 let factors = CostFactors {
320 dataset_size_factor,
321 ef_search_factor,
322 filter_selectivity_factor,
323 top_k_factor,
324 };
325
326 QueryCostEstimate::new(total_cost, estimated_latency_ms, factors)
327 }
328
329 pub fn check_cost_limit(
335 &self,
336 params: &QueryParams,
337 max_cost: f64,
338 ) -> Result<QueryCostEstimate, QueryCostExceeded> {
339 let estimate = self.estimate(params);
340
341 if estimate.total_cost > max_cost {
342 Err(QueryCostExceeded {
343 estimated: estimate.total_cost,
344 max_allowed: max_cost,
345 })
346 } else {
347 Ok(estimate)
348 }
349 }
350
351 pub fn check_collection_limit(
357 &self,
358 params: &QueryParams,
359 ) -> Result<QueryCostEstimate, QueryCostExceeded> {
360 let estimate = self.estimate(params);
361
362 if let Some(max) = self.max_cost {
363 if estimate.total_cost > max {
364 return Err(QueryCostExceeded {
365 estimated: estimate.total_cost,
366 max_allowed: max,
367 });
368 }
369 }
370
371 Ok(estimate)
372 }
373
374 #[must_use]
376 pub fn explain(&self, params: &QueryParams) -> String {
377 let estimate = self.estimate(params);
378
379 format!(
380 "Query Cost Estimate\n\
381 ===================\n\
382 Total Cost: {:.2}\n\
383 Estimated Latency: {:.2}ms\n\n\
384 Cost Breakdown:\n\
385 - Dataset Size Factor (log2({})): {:.2}\n\
386 - ef_search Factor ({}/{}): {:.2}\n\
387 - top_k Factor (sqrt({}/10)): {:.2}\n\
388 - Filter Selectivity Factor: {:.2}\n",
389 estimate.total_cost,
390 estimate.estimated_latency_ms,
391 params.dataset_size,
392 estimate.factors.dataset_size_factor,
393 params.ef_search,
394 self.calibration.reference_ef_search as usize,
395 estimate.factors.ef_search_factor,
396 params.top_k,
397 estimate.factors.top_k_factor,
398 estimate.factors.filter_selectivity_factor,
399 )
400 }
401}
402
403#[derive(Debug, Default)]
405pub struct QueryParamsBuilder {
406 params: QueryParams,
407}
408
409impl QueryParamsBuilder {
410 #[must_use]
412 pub fn new() -> Self {
413 Self::default()
414 }
415
416 #[must_use]
418 pub fn dataset_size(mut self, size: usize) -> Self {
419 self.params.dataset_size = size;
420 self
421 }
422
423 #[must_use]
425 pub fn ef_search(mut self, ef: usize) -> Self {
426 self.params.ef_search = ef;
427 self
428 }
429
430 #[must_use]
432 pub fn top_k(mut self, k: usize) -> Self {
433 self.params.top_k = k;
434 self
435 }
436
437 #[must_use]
439 pub fn filter_selectivity(mut self, selectivity: f64) -> Self {
440 self.params.filter_selectivity = Some(selectivity.clamp(0.001, 1.0));
441 self
442 }
443
444 #[must_use]
446 pub fn build(self) -> QueryParams {
447 self.params
448 }
449}