1use yo_common::num::DIGITS_MAX;
55
56use crate::elem::Elements;
57use crate::listpack::Entry;
58use crate::set::Set;
59use crate::zset::Zset;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub enum Aggregate {
64 #[default]
66 Sum,
67 Min,
69 Max,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Op {
76 Union,
78 Inter,
80 Diff,
82}
83
84#[derive(Debug, Clone, Copy)]
89pub enum Operand<'a> {
90 Zset(&'a Zset),
92 Set(&'a Set),
94 Missing,
96}
97
98impl Operand<'_> {
99 #[must_use]
101 pub fn len(&self) -> usize {
102 match self {
103 Operand::Zset(z) => z.len(),
104 Operand::Set(s) => s.len(),
105 Operand::Missing => 0,
106 }
107 }
108
109 #[must_use]
111 pub fn is_empty(&self) -> bool {
112 self.len() == 0
113 }
114
115 fn score(&self, member: &[u8]) -> Option<f64> {
117 match self {
118 Operand::Zset(z) => z.score(member),
119 Operand::Set(s) => s.contains(member).then_some(1.0),
120 Operand::Missing => None,
121 }
122 }
123
124 fn walk<F: FnMut(&[u8], f64)>(&self, mut f: F) {
126 let mut digits = [0u8; DIGITS_MAX];
127 match self {
128 Operand::Zset(z) => z.walk(0, z.len(), false, |m, s| f(bytes(m, &mut digits), s)),
129 Operand::Set(s) => {
130 for m in s.iter() {
131 f(bytes(m, &mut digits), 1.0);
132 }
133 }
134 Operand::Missing => {}
135 }
136 }
137}
138
139#[inline]
141fn bytes<'a>(m: Entry<'a>, digits: &'a mut [u8; DIGITS_MAX]) -> &'a [u8] {
142 match m {
143 Entry::Str(s) => s,
144 Entry::Int(n) => yo_common::num::i64_digits(digits, n),
145 }
146}
147
148#[inline]
154fn weighted(score: f64, weight: f64) -> f64 {
155 let v = score * weight;
156 if v.is_nan() { 0.0 } else { v }
157}
158
159#[inline]
161fn fold(now: f64, next: f64, agg: Aggregate) -> f64 {
162 match agg {
163 Aggregate::Sum => {
164 let v = now + next;
165 if v.is_nan() { 0.0 } else { v }
166 }
167 Aggregate::Min => {
168 if next < now {
169 next
170 } else {
171 now
172 }
173 }
174 Aggregate::Max => {
175 if next > now {
176 next
177 } else {
178 now
179 }
180 }
181 }
182}
183
184#[must_use]
194pub fn gather(op: Op, inputs: &[Operand<'_>], weights: &[f64], agg: Aggregate) -> Elements<f64> {
195 let weight = |i: usize| weights.get(i).copied().unwrap_or(1.0);
196 match op {
197 Op::Union => {
198 let mut out = Elements::with_capacity(hint(inputs, op));
199 for (i, input) in inputs.iter().enumerate() {
200 let w = weight(i);
201 input.walk(|member, score| {
202 let v = weighted(score, w);
203 match out.get_mut(member) {
206 Some(now) => *now = fold(*now, v, agg),
207 None => {
208 let _ = out.insert(member, v);
209 }
210 }
211 });
212 }
213 out
214 }
215 Op::Inter => {
216 let Some(small) = (0..inputs.len()).min_by_key(|&i| inputs[i].len()) else {
220 return Elements::with_capacity(0);
221 };
222 let mut out = Elements::with_capacity(inputs[small].len().clamp(16, 1 << 16));
223 if inputs.iter().any(Operand::is_empty) {
224 return out;
225 }
226 inputs[small].walk(|member, score| {
227 let mut total = weighted(score, weight(small));
228 for (i, other) in inputs.iter().enumerate() {
229 if i == small {
230 continue;
231 }
232 let Some(s) = other.score(member) else { return };
235 total = fold(total, weighted(s, weight(i)), agg);
236 }
237 let _ = out.insert(member, total);
238 });
239 out
240 }
241 Op::Diff => {
242 let Some((first, rest)) = inputs.split_first() else {
243 return Elements::with_capacity(0);
244 };
245 let mut out = Elements::with_capacity(first.len().clamp(16, 1 << 16));
246 first.walk(|member, score| {
247 if rest.iter().any(|o| o.score(member).is_some()) {
248 return;
249 }
250 let _ = out.insert(member, score);
253 });
254 out
255 }
256 }
257}
258
259#[must_use]
265pub fn intercard(inputs: &[Operand<'_>], limit: usize) -> usize {
266 if inputs.is_empty() || inputs.iter().any(Operand::is_empty) {
267 return 0;
268 }
269 let small = (0..inputs.len())
270 .min_by_key(|&i| inputs[i].len())
271 .expect("not empty");
272 let stop = if limit == 0 { usize::MAX } else { limit };
273 let mut found = 0;
274 inputs[small].walk(|member, _| {
275 if found >= stop {
276 return;
277 }
278 if inputs
279 .iter()
280 .enumerate()
281 .all(|(i, o)| i == small || o.score(member).is_some())
282 {
283 found += 1;
284 }
285 });
286 found
287}
288
289fn hint(inputs: &[Operand<'_>], op: Op) -> usize {
295 let total: usize = match op {
296 Op::Union => inputs.iter().map(Operand::len).sum(),
297 _ => inputs.first().map_or(0, Operand::len),
298 };
299 total.clamp(16, 1 << 20)
300}
301
302#[cfg(test)]
303mod tests {
304 use core::cmp::Ordering;
305
306 use super::*;
307 use crate::set::Limits as SetLimits;
308 use crate::zset::Limits;
309
310 fn cmp_key(a: (f64, &[u8]), b: (f64, &[u8])) -> Ordering {
318 match a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal) {
319 Ordering::Equal => a.1.cmp(b.1),
320 other => other,
321 }
322 }
323
324 fn zs(pairs: &[(&str, f64)]) -> Zset {
325 let mut z = Zset::new();
326 for (m, s) in pairs {
327 z.add(m.as_bytes(), *s, &Limits::DEFAULT);
328 }
329 z
330 }
331
332 fn plain(members: &[&str]) -> Set {
333 let mut s = Set::new();
334 for m in members {
335 s.add(m.as_bytes(), &SetLimits::DEFAULT);
336 }
337 s
338 }
339
340 fn ordered(got: Elements<f64>) -> Vec<(String, f64)> {
343 let mut out: Vec<(String, f64)> = (0..got.len())
344 .map(|i| got.at(i).expect("in range"))
345 .map(|(n, s)| (String::from_utf8(n.to_vec()).unwrap(), *s))
346 .collect();
347 out.sort_by(|a, b| cmp_key((a.1, a.0.as_bytes()), (b.1, b.0.as_bytes())));
348 out
349 }
350
351 fn named(got: Vec<(String, f64)>) -> Vec<String> {
352 got.into_iter().map(|(m, _)| m).collect()
353 }
354
355 #[test]
356 fn a_union_adds_the_scores_of_a_member_in_both() {
357 let a = zs(&[("x", 1.0), ("y", 2.0)]);
358 let b = zs(&[("y", 3.0), ("z", 4.0)]);
359 let got = ordered(gather(
360 Op::Union,
361 &[Operand::Zset(&a), Operand::Zset(&b)],
362 &[],
363 Aggregate::Sum,
364 ));
365 assert_eq!(
366 got,
367 [("x".into(), 1.0), ("z".into(), 4.0), ("y".into(), 5.0)]
368 );
369 }
370
371 #[test]
372 fn min_and_max_keep_one_score_rather_than_adding_them() {
373 let a = zs(&[("y", 2.0)]);
374 let b = zs(&[("y", 7.0)]);
375 let ops = [Operand::Zset(&a), Operand::Zset(&b)];
376 assert_eq!(
377 ordered(gather(Op::Union, &ops, &[], Aggregate::Min)),
378 [("y".to_string(), 2.0)]
379 );
380 assert_eq!(
381 ordered(gather(Op::Union, &ops, &[], Aggregate::Max)),
382 [("y".to_string(), 7.0)]
383 );
384 }
385
386 #[test]
387 fn weights_multiply_before_anything_is_aggregated() {
388 let a = zs(&[("x", 1.0), ("y", 2.0)]);
389 let b = zs(&[("y", 3.0)]);
390 let ops = [Operand::Zset(&a), Operand::Zset(&b)];
391 let got = ordered(gather(Op::Union, &ops, &[2.0, 10.0], Aggregate::Sum));
392 assert_eq!(got, [("x".into(), 2.0), ("y".into(), 34.0)]);
393 let got = ordered(gather(Op::Union, &ops, &[2.0, 0.5], Aggregate::Min));
396 assert_eq!(got, [("y".into(), 1.5), ("x".into(), 2.0)]);
397 }
398
399 #[test]
400 fn an_intersection_only_keeps_what_every_input_has() {
401 let a = zs(&[("x", 1.0), ("y", 2.0), ("z", 3.0)]);
402 let b = zs(&[("y", 10.0), ("z", 20.0)]);
403 let c = zs(&[("z", 100.0)]);
404 let ops = [Operand::Zset(&a), Operand::Zset(&b), Operand::Zset(&c)];
405 assert_eq!(
406 ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)),
407 [("z".to_string(), 123.0)]
408 );
409 assert_eq!(intercard(&ops, 0), 1);
410 let ops = [Operand::Zset(&a), Operand::Missing];
412 assert!(ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)).is_empty());
413 assert_eq!(intercard(&ops, 0), 0);
414 }
415
416 #[test]
417 fn a_difference_keeps_the_first_input_scores() {
418 let a = zs(&[("x", 1.0), ("y", 2.0), ("z", 3.0)]);
419 let b = zs(&[("y", 99.0)]);
420 let ops = [Operand::Zset(&a), Operand::Zset(&b)];
421 assert_eq!(
422 ordered(gather(Op::Diff, &ops, &[], Aggregate::Sum)),
423 [("x".into(), 1.0), ("z".into(), 3.0)]
424 );
425 assert!(
428 ordered(gather(
429 Op::Diff,
430 &[Operand::Missing, Operand::Zset(&a)],
431 &[],
432 Aggregate::Sum
433 ))
434 .is_empty()
435 );
436 let ops = [Operand::Zset(&a), Operand::Missing];
437 assert_eq!(
438 named(ordered(gather(Op::Diff, &ops, &[], Aggregate::Sum))),
439 ["x", "y", "z"]
440 );
441 }
442
443 #[test]
444 fn a_plain_set_counts_as_a_sorted_set_where_every_score_is_one() {
445 let a = zs(&[("x", 5.0), ("y", 6.0)]);
446 let b = plain(&["y", "z"]);
447 let ops = [Operand::Zset(&a), Operand::Set(&b)];
448 let got = ordered(gather(Op::Union, &ops, &[], Aggregate::Sum));
449 assert_eq!(
450 got,
451 [("z".into(), 1.0), ("x".into(), 5.0), ("y".into(), 7.0)]
452 );
453 assert_eq!(
454 ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)),
455 [("y".to_string(), 7.0)]
456 );
457 assert_eq!(
458 named(ordered(gather(Op::Diff, &ops, &[], Aggregate::Sum))),
459 ["x"]
460 );
461 }
462
463 #[test]
466 fn an_integer_member_crosses_between_a_set_and_a_sorted_set() {
467 let a = zs(&[("17", 5.0), ("42", 6.0)]);
468 let b = plain(&["42", "99"]);
469 assert_eq!(b.encoding().name(), "intset");
470 let ops = [Operand::Zset(&a), Operand::Set(&b)];
471 assert_eq!(
472 ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)),
473 [("42".to_string(), 7.0)]
474 );
475 assert_eq!(intercard(&ops, 0), 1);
476 assert_eq!(
477 named(ordered(gather(Op::Union, &ops, &[], Aggregate::Sum))),
478 ["99", "17", "42"]
479 );
480 }
481
482 #[test]
483 fn a_score_that_would_be_a_nan_becomes_a_zero() {
484 let a = zs(&[("x", f64::INFINITY)]);
486 let ops = [Operand::Zset(&a)];
487 assert_eq!(
488 ordered(gather(Op::Union, &ops, &[0.0], Aggregate::Sum)),
489 [("x".to_string(), 0.0)]
490 );
491 let b = zs(&[("x", f64::NEG_INFINITY)]);
493 let ops = [Operand::Zset(&a), Operand::Zset(&b)];
494 assert_eq!(
495 ordered(gather(Op::Union, &ops, &[], Aggregate::Sum)),
496 [("x".to_string(), 0.0)]
497 );
498 }
499
500 #[test]
501 fn a_limit_stops_a_cardinality_count_where_it_was_told_to() {
502 let a = zs(&[("a", 1.0), ("b", 1.0), ("c", 1.0), ("d", 1.0)]);
503 let b = zs(&[("a", 1.0), ("b", 1.0), ("c", 1.0), ("d", 1.0)]);
504 let ops = [Operand::Zset(&a), Operand::Zset(&b)];
505 assert_eq!(intercard(&ops, 0), 4);
506 assert_eq!(intercard(&ops, 2), 2);
507 assert_eq!(intercard(&ops, 99), 4);
508 assert_eq!(intercard(&[], 0), 0);
509 }
510
511 #[test]
512 fn a_union_of_thousands_agrees_with_the_slow_way_of_working_it_out() {
513 let one: Vec<(String, f64)> = (0..3_000)
514 .map(|i| (format!("m{i:05}"), f64::from(i)))
515 .collect();
516 let two: Vec<(String, f64)> = (1_500..4_500)
517 .map(|i| (format!("m{i:05}"), f64::from(i) * 2.0))
518 .collect();
519 let mut a = Zset::new();
520 for (m, s) in &one {
521 a.add(m.as_bytes(), *s, &Limits::DEFAULT);
522 }
523 let mut b = Zset::new();
524 for (m, s) in &two {
525 b.add(m.as_bytes(), *s, &Limits::DEFAULT);
526 }
527 let ops = [Operand::Zset(&a), Operand::Zset(&b)];
528
529 let mut want: std::collections::BTreeMap<String, f64> = std::collections::BTreeMap::new();
530 for (m, s) in one.iter().chain(two.iter()) {
531 *want.entry(m.clone()).or_insert(0.0) += s;
532 }
533 let mut want: Vec<(String, f64)> = want.into_iter().collect();
534 want.sort_by(|x, y| cmp_key((x.1, x.0.as_bytes()), (y.1, y.0.as_bytes())));
535 assert_eq!(ordered(gather(Op::Union, &ops, &[], Aggregate::Sum)), want);
536 assert_eq!(intercard(&ops, 0), 1_500);
537 }
538}