use kevy_resp::{encode_array_len, encode_bulk, encode_error};
use kevy_rt::ExtensionReduced;
use super::chunk::{read_kbytes, read_u32, value_repr};
pub(super) fn reduce_agg(argv: &[Vec<u8>], chunks: &[Vec<u8>]) -> ExtensionReduced {
let mut out = Vec::new();
let single = argv[2].eq_ignore_ascii_case(b"GROUP");
if single {
return ExtensionReduced::Reply(reduce_agg_single(chunks));
}
let Some((by, limit)) = crate::cmd_index_query::parse_groups_args(argv) else {
encode_error(&mut out, "ERR bad IDX arguments");
return ExtensionReduced::Reply(out);
};
let additive = matches!(by, kevy_index::AggBy::Count | kevy_index::AggBy::Sum);
let (observed, taus) = collect_partials(chunks, by);
let mut ranked: Vec<(Vec<u8>, kevy_index::GroupStats)> = observed.into_iter().collect();
ranked.sort_by(|a, b| score(&b.1, by).total_cmp(&score(&a.1, by)).then_with(|| a.0.cmp(&b.0)));
let theta = ranked.get(limit - 1).map_or(f64::NEG_INFINITY, |(_, st)| score(st, by));
let unseen_bound = if additive {
taus.iter().sum::<f64>()
} else {
taus.iter().copied().fold(f64::NEG_INFINITY, f64::max)
};
let depth = groups_depth(argv);
if unseen_bound > theta && depth != 0 {
let mut argv2: Vec<Vec<u8>> = argv.to_vec();
set_groups_depth(&mut argv2, if depth == 1 { 4 } else { 0 });
return continuation(argv2);
}
let cands = fetch_candidates(&ranked, &taus, theta, by, additive, unseen_bound, limit);
let mut argv2: Vec<Vec<u8>> = vec![
b"AGG.FETCH".to_vec(),
argv[1].clone(),
format!("BY={} LIMIT={}", tag_of(by), limit).into_bytes(),
];
argv2.extend(cands);
continuation(argv2)
}
fn reduce_agg_single(chunks: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
let mut st = kevy_index::GroupStats { count: 0, sum: 0.0, min: None, max: None };
for c in chunks {
for (_g, part) in decode_agg_chunk(c) {
kevy_index::merge_group(&mut st, &part);
}
}
encode_array_len(&mut out, 5);
encode_bulk(&mut out, st.count.to_string().as_bytes());
encode_bulk(&mut out, format!("{}", st.sum).as_bytes());
for v in [&st.min, &st.max] {
match v {
Some(x) => encode_bulk(&mut out, &value_repr(x)),
None => out.extend_from_slice(b"$-1\r\n"),
}
}
match st.avg() {
Some(a) => encode_bulk(&mut out, format!("{a}").as_bytes()),
None => out.extend_from_slice(b"$-1\r\n"),
}
out
}
fn collect_partials(
chunks: &[Vec<u8>],
by: kevy_index::AggBy,
) -> (std::collections::HashMap<Vec<u8>, kevy_index::GroupStats>, Vec<f64>) {
let mut observed: std::collections::HashMap<Vec<u8>, kevy_index::GroupStats> =
std::collections::HashMap::new();
let mut taus: Vec<f64> = Vec::new(); for c in chunks {
let rows = decode_agg_chunk(&c[..c.len().saturating_sub(1)]);
let exhausted = c.last() == Some(&1);
if !exhausted {
let tau = rows.last().map_or(f64::NEG_INFINITY, |(_, st)| score(st, by));
taus.push(tau);
}
for (g, part) in rows {
match observed.get_mut(&g) {
Some(st) => kevy_index::merge_group(st, &part),
None => {
observed.insert(g, part);
}
}
}
}
(observed, taus)
}
fn fetch_candidates(
ranked: &[(Vec<u8>, kevy_index::GroupStats)],
taus: &[f64],
theta: f64,
by: kevy_index::AggBy,
additive: bool,
unseen_bound: f64,
limit: usize,
) -> Vec<Vec<u8>> {
let mut cands: Vec<Vec<u8>> = ranked
.iter()
.filter(|(_, st)| {
let upper = if additive {
score(st, by) + taus.iter().sum::<f64>()
} else {
score(st, by).max(unseen_bound)
};
upper >= theta
})
.map(|(g, _)| g.clone())
.collect();
cands.truncate((limit * 32).max(256)); cands
}
pub(super) fn reduce_agg_fetch(argv: &[Vec<u8>], chunks: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
let meta = argv.get(2).map(|m| String::from_utf8_lossy(m).into_owned()).unwrap_or_default();
let by = meta
.split_whitespace()
.find_map(|t| t.strip_prefix("BY=").and_then(|b| kevy_index::AggBy::parse(b.as_bytes())))
.unwrap_or_default();
let limit: usize = meta
.split_whitespace()
.find_map(|t| t.strip_prefix("LIMIT=").and_then(|n| n.parse().ok()))
.unwrap_or(100);
let mut merged: std::collections::HashMap<Vec<u8>, kevy_index::GroupStats> =
std::collections::HashMap::new();
for c in chunks {
for (g, part) in decode_agg_chunk(c) {
match merged.get_mut(&g) {
Some(st) => kevy_index::merge_group(st, &part),
None => {
merged.insert(g, part);
}
}
}
}
let mut ranked: Vec<(Vec<u8>, kevy_index::GroupStats)> =
merged.into_iter().filter(|(_, st)| st.count > 0).collect();
kevy_index::sort_groups(&mut ranked, by);
ranked.truncate(limit);
encode_array_len(&mut out, ranked.len() as i64);
for (g, st) in &ranked {
encode_array_len(&mut out, 5);
encode_bulk(&mut out, g);
encode_bulk(&mut out, st.count.to_string().as_bytes());
encode_bulk(&mut out, format!("{}", st.sum).as_bytes());
for v in [&st.min, &st.max] {
match v {
Some(x) => encode_bulk(&mut out, &value_repr(x)),
None => out.extend_from_slice(b"$-1\r\n"),
}
}
}
out
}
fn score(st: &kevy_index::GroupStats, by: kevy_index::AggBy) -> f64 {
match by {
kevy_index::AggBy::Count => st.count as f64,
kevy_index::AggBy::Sum => st.sum,
kevy_index::AggBy::Max => st.max.as_ref().map_or(f64::NEG_INFINITY, |v| v.as_f64()),
kevy_index::AggBy::Min => st.min.as_ref().map_or(f64::NEG_INFINITY, |v| -v.as_f64()),
}
}
fn tag_of(by: kevy_index::AggBy) -> &'static str {
match by {
kevy_index::AggBy::Count => "count",
kevy_index::AggBy::Sum => "sum",
kevy_index::AggBy::Min => "min",
kevy_index::AggBy::Max => "max",
}
}
fn groups_depth(argv: &[Vec<u8>]) -> usize {
argv.iter()
.find_map(|a| std::str::from_utf8(a).ok()?.strip_prefix("DEPTH=")?.parse().ok())
.unwrap_or(1)
}
fn set_groups_depth(argv: &mut Vec<Vec<u8>>, depth: usize) {
for a in argv.iter_mut() {
if a.starts_with(b"DEPTH=") {
*a = format!("DEPTH={depth}").into_bytes();
return;
}
}
argv.push(format!("DEPTH={depth}").into_bytes());
}
fn continuation(argv: Vec<Vec<u8>>) -> ExtensionReduced {
ExtensionReduced::Continue(argv)
}
fn decode_agg_chunk(c: &[u8]) -> Vec<(Vec<u8>, kevy_index::GroupStats)> {
let mut rows = Vec::new();
let mut pos = 1usize;
let Some(n) = read_u32(c, &mut pos) else { return rows };
for _ in 0..n {
let Some(g) = read_kbytes(c, &mut pos) else { break };
let Some(cb) = c.get(pos..pos + 8) else { break };
let count = u64::from_le_bytes(cb.try_into().expect("8"));
pos += 8;
let Some(sb) = c.get(pos..pos + 8) else { break };
let sum = f64::from_le_bytes(sb.try_into().expect("8"));
pos += 8;
let Some(ml) = c.get(pos..pos + 4) else { break };
let ml = u32::from_le_bytes(ml.try_into().expect("4")) as usize;
pos += 4;
let Some(mm) = c.get(pos..pos + ml) else { break };
pos += ml;
let mut mpos = 0usize;
let mut vals = [None, None];
for slot in &mut vals {
match mm.get(mpos).copied() {
Some(1) => {
mpos += 1;
*slot = crate::cmd_index_query::decode_value(mm, &mut mpos);
}
_ => mpos += 1,
}
}
rows.push((
g,
kevy_index::GroupStats { count, sum, min: vals[0].clone(), max: vals[1].clone() },
));
}
rows
}