1#![allow(
33 clippy::redundant_field_names,
34 clippy::similar_names,
35 clippy::manual_let_else,
36 clippy::explicit_iter_loop
37)]
38
39use std::collections::BTreeMap;
40use std::sync::Arc;
41
42use uqa_core::{Payload, PostingEntry, PostingList, Value};
43use uqa_storage::{StorageBackendError, StorageBackendResult};
44
45use crate::base::{missing_backend, ExecutionContext, Operator, OperatorResult};
46
47#[derive(Debug, Clone, Copy, Default, PartialEq)]
49pub struct AvgState {
50 pub sum: f64,
51 pub count: u64,
52}
53
54#[derive(Debug, Clone, PartialEq)]
58pub enum AggState {
59 Count(u64),
60 Sum(f64),
61 Avg(AvgState),
62 Min(f64),
63 Max(f64),
64 Values(Vec<f64>),
65}
66
67impl AggState {
68 pub fn as_f64(&self) -> Option<f64> {
69 match self {
70 AggState::Count(n) => Some(*n as f64),
71 AggState::Sum(v) | AggState::Min(v) | AggState::Max(v) => Some(*v),
72 AggState::Avg(s) => {
73 if s.count == 0 {
74 Some(0.0)
75 } else {
76 Some(s.sum / s.count as f64)
77 }
78 }
79 AggState::Values(_) => None,
80 }
81 }
82}
83
84pub trait AggregationMonoid: Send + Sync {
87 fn identity(&self) -> AggState;
88 fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState>;
89 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState>;
90 fn finalize(&self, state: AggState) -> StorageBackendResult<Value>;
91}
92
93fn invalid_state(operation: &str, expected: &str, actual: &AggState) -> StorageBackendError {
94 StorageBackendError::Other(format!(
95 "{operation} aggregation expected {expected} state, got {actual:?}"
96 ))
97}
98
99fn numeric_value(operation: &str, value: &Value) -> StorageBackendResult<Option<f64>> {
100 let numeric = match value {
101 Value::Null => return Ok(None),
102 Value::Int(integer) => *integer as f64,
103 Value::Float(float) => *float,
104 Value::Bool(true) => 1.0,
105 Value::Bool(false) => 0.0,
106 _ => {
107 return Err(StorageBackendError::Other(format!(
108 "{operation} aggregation requires a numeric value, got {value:?}"
109 )))
110 }
111 };
112 if !numeric.is_finite() {
113 return Err(StorageBackendError::Other(format!(
114 "{operation} aggregation requires a finite numeric value, got {numeric}"
115 )));
116 }
117 Ok(Some(numeric))
118}
119
120fn finite_result(operation: &str, value: f64) -> StorageBackendResult<f64> {
121 if value.is_finite() {
122 Ok(value)
123 } else {
124 Err(StorageBackendError::Other(format!(
125 "{operation} aggregation overflowed the finite numeric range"
126 )))
127 }
128}
129
130#[derive(Debug, Clone, Copy, Default)]
131pub struct CountMonoid;
132
133impl AggregationMonoid for CountMonoid {
134 fn identity(&self) -> AggState {
135 AggState::Count(0)
136 }
137 fn accumulate(&self, state: AggState, _value: &Value) -> StorageBackendResult<AggState> {
138 let AggState::Count(count) = state else {
139 return Err(invalid_state("count", "Count", &state));
140 };
141 Ok(AggState::Count(count.checked_add(1).ok_or_else(|| {
142 StorageBackendError::Other("count aggregation overflowed u64".to_string())
143 })?))
144 }
145 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
146 let (AggState::Count(left), AggState::Count(right)) = (&a, &b) else {
147 return Err(StorageBackendError::Other(format!(
148 "count aggregation expected Count states, got {a:?} and {b:?}"
149 )));
150 };
151 Ok(AggState::Count(left.checked_add(*right).ok_or_else(
152 || StorageBackendError::Other("count aggregation overflowed u64".to_string()),
153 )?))
154 }
155 fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
156 let AggState::Count(count) = state else {
157 return Err(invalid_state("count", "Count", &state));
158 };
159 let count = i64::try_from(count).map_err(|_| {
160 StorageBackendError::Other(format!(
161 "count aggregation result {count} exceeds the Value::Int range"
162 ))
163 })?;
164 Ok(Value::Int(count))
165 }
166}
167
168#[derive(Debug, Clone, Copy, Default)]
169pub struct SumMonoid;
170
171impl AggregationMonoid for SumMonoid {
172 fn identity(&self) -> AggState {
173 AggState::Sum(0.0)
174 }
175 fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
176 let Some(delta) = numeric_value("sum", value)? else {
177 return Ok(state);
178 };
179 let AggState::Sum(sum) = state else {
180 return Err(invalid_state("sum", "Sum", &state));
181 };
182 Ok(AggState::Sum(finite_result("sum", sum + delta)?))
183 }
184 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
185 let (AggState::Sum(left), AggState::Sum(right)) = (&a, &b) else {
186 return Err(StorageBackendError::Other(format!(
187 "sum aggregation expected Sum states, got {a:?} and {b:?}"
188 )));
189 };
190 Ok(AggState::Sum(finite_result("sum", left + right)?))
191 }
192 fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
193 let AggState::Sum(sum) = state else {
194 return Err(invalid_state("sum", "Sum", &state));
195 };
196 Ok(Value::Float(finite_result("sum", sum)?))
197 }
198}
199
200#[derive(Debug, Clone, Copy, Default)]
201pub struct AvgMonoid;
202
203impl AggregationMonoid for AvgMonoid {
204 fn identity(&self) -> AggState {
205 AggState::Avg(AvgState::default())
206 }
207 fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
208 let Some(delta) = numeric_value("avg", value)? else {
209 return Ok(state);
210 };
211 let AggState::Avg(average) = state else {
212 return Err(invalid_state("avg", "Avg", &state));
213 };
214 Ok(AggState::Avg(AvgState {
215 sum: finite_result("avg", average.sum + delta)?,
216 count: average.count.checked_add(1).ok_or_else(|| {
217 StorageBackendError::Other("avg aggregation count overflowed u64".to_string())
218 })?,
219 }))
220 }
221 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
222 let (AggState::Avg(left), AggState::Avg(right)) = (&a, &b) else {
223 return Err(StorageBackendError::Other(format!(
224 "avg aggregation expected Avg states, got {a:?} and {b:?}"
225 )));
226 };
227 Ok(AggState::Avg(AvgState {
228 sum: finite_result("avg", left.sum + right.sum)?,
229 count: left.count.checked_add(right.count).ok_or_else(|| {
230 StorageBackendError::Other("avg aggregation count overflowed u64".to_string())
231 })?,
232 }))
233 }
234 fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
235 let AggState::Avg(average) = state else {
236 return Err(invalid_state("avg", "Avg", &state));
237 };
238 if average.count == 0 {
239 Ok(Value::Float(0.0))
240 } else {
241 Ok(Value::Float(finite_result(
242 "avg",
243 average.sum / average.count as f64,
244 )?))
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy, Default)]
250pub struct MinMonoid;
251
252impl AggregationMonoid for MinMonoid {
253 fn identity(&self) -> AggState {
254 AggState::Min(f64::INFINITY)
255 }
256 fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
257 let Some(value) = numeric_value("min", value)? else {
258 return Ok(state);
259 };
260 let AggState::Min(minimum) = state else {
261 return Err(invalid_state("min", "Min", &state));
262 };
263 Ok(AggState::Min(minimum.min(value)))
264 }
265 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
266 let (AggState::Min(left), AggState::Min(right)) = (&a, &b) else {
267 return Err(StorageBackendError::Other(format!(
268 "min aggregation expected Min states, got {a:?} and {b:?}"
269 )));
270 };
271 Ok(AggState::Min(left.min(*right)))
272 }
273 fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
274 let AggState::Min(minimum) = state else {
275 return Err(invalid_state("min", "Min", &state));
276 };
277 if minimum == f64::INFINITY {
278 Ok(Value::Null)
279 } else {
280 Ok(Value::Float(finite_result("min", minimum)?))
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, Default)]
286pub struct MaxMonoid;
287
288impl AggregationMonoid for MaxMonoid {
289 fn identity(&self) -> AggState {
290 AggState::Max(f64::NEG_INFINITY)
291 }
292 fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
293 let Some(value) = numeric_value("max", value)? else {
294 return Ok(state);
295 };
296 let AggState::Max(maximum) = state else {
297 return Err(invalid_state("max", "Max", &state));
298 };
299 Ok(AggState::Max(maximum.max(value)))
300 }
301 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
302 let (AggState::Max(left), AggState::Max(right)) = (&a, &b) else {
303 return Err(StorageBackendError::Other(format!(
304 "max aggregation expected Max states, got {a:?} and {b:?}"
305 )));
306 };
307 Ok(AggState::Max(left.max(*right)))
308 }
309 fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
310 let AggState::Max(maximum) = state else {
311 return Err(invalid_state("max", "Max", &state));
312 };
313 if maximum == f64::NEG_INFINITY {
314 Ok(Value::Null)
315 } else {
316 Ok(Value::Float(finite_result("max", maximum)?))
317 }
318 }
319}
320
321#[derive(Debug, Clone, Copy)]
324pub struct QuantileMonoid {
325 quantile: f64,
326}
327
328impl QuantileMonoid {
329 pub fn new(quantile: f64) -> StorageBackendResult<Self> {
330 if !quantile.is_finite() || !(0.0..=1.0).contains(&quantile) {
331 return Err(StorageBackendError::Other(format!(
332 "quantile must be finite and in [0, 1], got {quantile}"
333 )));
334 }
335 Ok(Self { quantile })
336 }
337}
338
339impl AggregationMonoid for QuantileMonoid {
340 fn identity(&self) -> AggState {
341 AggState::Values(Vec::new())
342 }
343 fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
344 let Some(value) = numeric_value("quantile", value)? else {
345 return Ok(state);
346 };
347 let AggState::Values(mut values) = state else {
348 return Err(invalid_state("quantile", "Values", &state));
349 };
350 values.push(value);
351 Ok(AggState::Values(values))
352 }
353 fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
354 let (AggState::Values(mut left), AggState::Values(right)) = (a, b) else {
355 return Err(StorageBackendError::Other(
356 "quantile aggregation expected Values states".to_string(),
357 ));
358 };
359 left.extend(right);
360 Ok(AggState::Values(left))
361 }
362 fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
363 let mut buf = match state {
364 AggState::Values(v) => v,
365 other => return Err(invalid_state("quantile", "Values", &other)),
366 };
367 if buf.is_empty() {
368 return Ok(Value::Null);
369 }
370 if buf.iter().any(|value| !value.is_finite()) {
371 return Err(StorageBackendError::Other(
372 "quantile aggregation state contains a non-finite value".to_string(),
373 ));
374 }
375 buf.sort_by(f64::total_cmp);
376 let n = buf.len();
377 let idx = self.quantile * (n - 1) as f64;
378 let lower = idx.floor() as usize;
379 let upper = (lower + 1).min(n - 1);
380 let frac = idx - lower as f64;
381 Ok(Value::Float(buf[lower] * (1.0 - frac) + buf[upper] * frac))
382 }
383}
384
385pub struct AggregateOperator {
394 pub source: Option<Arc<dyn Operator>>,
395 pub field: String,
396 pub monoid: Arc<dyn AggregationMonoid>,
397}
398
399impl AggregateOperator {
400 pub fn new(
401 source: Option<Arc<dyn Operator>>,
402 field: impl Into<String>,
403 monoid: Arc<dyn AggregationMonoid>,
404 ) -> Self {
405 Self {
406 source,
407 field: field.into(),
408 monoid,
409 }
410 }
411}
412
413impl Operator for AggregateOperator {
414 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
415 let doc_ids: Vec<u64> = if let Some(op) = &self.source {
416 op.execute(ctx)?.iter().map(|e| e.doc_id).collect()
417 } else {
418 let store = ctx
419 .document_store
420 .as_ref()
421 .ok_or_else(|| missing_backend("document-store", "field aggregation"))?;
422 let mut ids = store.doc_ids()?;
423 ids.sort_unstable();
424 ids
425 };
426
427 let mut state = self.monoid.identity();
428 let store = ctx
429 .document_store
430 .as_ref()
431 .ok_or_else(|| missing_backend("document-store", "field aggregation"))?;
432 for doc_id in doc_ids {
433 if store.get(doc_id)?.is_none() {
434 return Err(StorageBackendError::Other(format!(
435 "field aggregate candidate {doc_id} is missing from the document store"
436 )));
437 }
438 if let Some(value) = store.get_field(doc_id, &self.field)? {
439 state = self.monoid.accumulate(state, &value)?;
440 }
441 }
442
443 let result = self.monoid.finalize(state)?;
444 let score = match &result {
445 Value::Int(i) => *i as f64,
446 Value::Float(f) => *f,
447 _ => 0.0,
448 };
449 let mut fields: BTreeMap<String, Value> = BTreeMap::new();
450 fields.insert("_aggregate_field".into(), Value::Str(self.field.clone()));
451 fields.insert("_aggregate".into(), result);
452 Ok(PostingList::from_sorted_unchecked(vec![PostingEntry::new(
453 0,
454 Payload {
455 score,
456 fields,
457 ..Default::default()
458 },
459 )]))
460 }
461}
462
463pub struct GroupByOperator {
465 pub source: Arc<dyn Operator>,
466 pub group_field: String,
467 pub agg_field: String,
468 pub monoid: Arc<dyn AggregationMonoid>,
469}
470
471impl GroupByOperator {
472 pub fn new(
473 source: Arc<dyn Operator>,
474 group_field: impl Into<String>,
475 agg_field: impl Into<String>,
476 monoid: Arc<dyn AggregationMonoid>,
477 ) -> Self {
478 Self {
479 source,
480 group_field: group_field.into(),
481 agg_field: agg_field.into(),
482 monoid,
483 }
484 }
485}
486
487impl Operator for GroupByOperator {
488 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
489 let source_pl = self.source.execute(ctx)?;
490 let store = ctx
491 .document_store
492 .as_ref()
493 .ok_or_else(|| missing_backend("document-store", "group-by aggregation"))?;
494
495 let mut groups: BTreeMap<String, AggState> = BTreeMap::new();
496 for entry in source_pl.iter() {
497 if store.get(entry.doc_id)?.is_none() {
498 return Err(StorageBackendError::Other(format!(
499 "group-by candidate {} is missing from the document store",
500 entry.doc_id
501 )));
502 }
503 let Some(group_val) = store.get_field(entry.doc_id, &self.group_field)? else {
504 continue;
505 };
506 let key = value_to_key(&group_val);
507 let state = groups.entry(key).or_insert_with(|| self.monoid.identity());
508 if let Some(agg_val) = store.get_field(entry.doc_id, &self.agg_field)? {
509 let new_state = self
510 .monoid
511 .accumulate(std::mem::replace(state, self.monoid.identity()), &agg_val)?;
512 *state = new_state;
513 }
514 }
515
516 let mut entries: Vec<PostingEntry> = Vec::with_capacity(groups.len());
517 for (i, (group_key, state)) in groups.into_iter().enumerate() {
518 let result = self.monoid.finalize(state)?;
519 let score = match &result {
520 Value::Int(i) => *i as f64,
521 Value::Float(f) => *f,
522 _ => 0.0,
523 };
524 let mut fields: BTreeMap<String, Value> = BTreeMap::new();
525 fields.insert("_group_key".into(), Value::Str(group_key));
526 fields.insert("_group_field".into(), Value::Str(self.group_field.clone()));
527 fields.insert("_aggregate_result".into(), result);
528 entries.push(PostingEntry::new(
529 u64::try_from(i).map_err(|_| {
530 StorageBackendError::Other(format!(
531 "group-by bucket index {i} exceeds the document-id range"
532 ))
533 })?,
534 Payload {
535 score,
536 fields,
537 ..Default::default()
538 },
539 ));
540 }
541 Ok(PostingList::from_sorted_unchecked(entries))
542 }
543}
544
545fn value_to_key(v: &Value) -> String {
546 match v {
547 Value::Null => "\x00".into(),
548 Value::Int(i) => i.to_string(),
549 Value::Float(f) => format!("{f:.17}"),
550 Value::Str(s) => s.clone(),
551 Value::Bool(b) => b.to_string(),
552 other => format!("{other:?}"),
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn count_monoid_combines_partial_folds() {
562 let m = CountMonoid;
563 let mut a = m.identity();
564 let mut b = m.identity();
565 a = m.accumulate(a, &Value::Null).unwrap();
566 a = m.accumulate(a, &Value::Null).unwrap();
567 b = m.accumulate(b, &Value::Null).unwrap();
568 let merged = m.combine(a, b).unwrap();
569 assert_eq!(m.finalize(merged).unwrap(), Value::Int(3));
570 }
571
572 #[test]
573 fn sum_monoid_rejects_non_numeric() {
574 let m = SumMonoid;
575 let mut s = m.identity();
576 s = m.accumulate(s, &Value::Float(1.5)).unwrap();
577 s = m.accumulate(s, &Value::Int(2)).unwrap();
578 assert_eq!(m.finalize(s).unwrap(), Value::Float(3.5));
579
580 let error = m
581 .accumulate(m.identity(), &Value::Str("nope".into()))
582 .unwrap_err();
583 assert!(error.to_string().contains("requires a numeric value"));
584 }
585
586 #[test]
587 fn avg_monoid_divides_by_count() {
588 let m = AvgMonoid;
589 let mut s = m.identity();
590 for v in [Value::Int(2), Value::Int(4), Value::Int(6)] {
591 s = m.accumulate(s, &v).unwrap();
592 }
593 assert_eq!(m.finalize(s).unwrap(), Value::Float(4.0));
594 }
595
596 #[test]
597 fn min_max_track_extremes() {
598 let mn = MinMonoid;
599 let mx = MaxMonoid;
600 let mut a = mn.identity();
601 let mut b = mx.identity();
602 for v in [Value::Float(3.0), Value::Float(1.0), Value::Float(2.0)] {
603 a = mn.accumulate(a, &v).unwrap();
604 b = mx.accumulate(b, &v).unwrap();
605 }
606 assert_eq!(mn.finalize(a).unwrap(), Value::Float(1.0));
607 assert_eq!(mx.finalize(b).unwrap(), Value::Float(3.0));
608 }
609
610 #[test]
611 fn quantile_median_interpolates() {
612 let q = QuantileMonoid::new(0.5).unwrap();
613 let mut s = q.identity();
614 for v in [
615 Value::Float(1.0),
616 Value::Float(2.0),
617 Value::Float(3.0),
618 Value::Float(4.0),
619 ] {
620 s = q.accumulate(s, &v).unwrap();
621 }
622 assert_eq!(q.finalize(s).unwrap(), Value::Float(2.5));
624 }
625
626 #[test]
627 fn quantile_constructor_rejects_invalid_values() {
628 for quantile in [f64::NAN, -0.1, 1.1] {
629 let error = QuantileMonoid::new(quantile).unwrap_err();
630 assert!(error.to_string().contains("finite and in [0, 1]"));
631 }
632 }
633
634 #[test]
635 fn aggregation_state_mismatches_are_errors() {
636 let count = CountMonoid;
637 assert!(count
638 .accumulate(AggState::Sum(0.0), &Value::Int(1))
639 .unwrap_err()
640 .to_string()
641 .contains("expected Count state"));
642 assert!(count
643 .combine(AggState::Count(1), AggState::Sum(2.0))
644 .unwrap_err()
645 .to_string()
646 .contains("expected Count states"));
647 assert!(count
648 .finalize(AggState::Values(Vec::new()))
649 .unwrap_err()
650 .to_string()
651 .contains("expected Count state"));
652 }
653
654 #[test]
655 fn aggregation_counters_and_value_widths_are_checked() {
656 let count = CountMonoid;
657 assert!(count
658 .accumulate(AggState::Count(u64::MAX), &Value::Null)
659 .unwrap_err()
660 .to_string()
661 .contains("overflowed u64"));
662 assert!(count
663 .combine(AggState::Count(u64::MAX), AggState::Count(1))
664 .unwrap_err()
665 .to_string()
666 .contains("overflowed u64"));
667 assert!(count
668 .finalize(AggState::Count(i64::MAX as u64 + 1))
669 .unwrap_err()
670 .to_string()
671 .contains("Value::Int range"));
672
673 let avg = AvgMonoid;
674 assert!(avg
675 .accumulate(
676 AggState::Avg(AvgState {
677 sum: 1.0,
678 count: u64::MAX,
679 }),
680 &Value::Int(1),
681 )
682 .unwrap_err()
683 .to_string()
684 .contains("count overflowed"));
685 }
686
687 #[test]
688 fn numeric_aggregations_reject_invalid_types_and_non_finite_values() {
689 for result in [
690 AvgMonoid.accumulate(AvgMonoid.identity(), &Value::Str("bad".into())),
691 MinMonoid.accumulate(MinMonoid.identity(), &Value::Float(f64::NAN)),
692 MaxMonoid.accumulate(MaxMonoid.identity(), &Value::Float(f64::INFINITY)),
693 ] {
694 assert!(result.is_err());
695 }
696 assert_eq!(
697 MinMonoid.finalize(MinMonoid.identity()).unwrap(),
698 Value::Null
699 );
700 assert_eq!(
701 MaxMonoid.finalize(MaxMonoid.identity()).unwrap(),
702 Value::Null
703 );
704 let quantile = QuantileMonoid::new(0.5).unwrap();
705 assert_eq!(quantile.finalize(quantile.identity()).unwrap(), Value::Null);
706 }
707}