use kevy_resp::{encode_array_len, encode_bulk, encode_error};
use kevy_rt::ExtensionReduced;
use super::chunk::{read_highlight, read_hydration, read_kbytes, read_u32};
#[path = "ranked_chunk.rs"]
mod chunk;
use chunk::{collect_facets, collect_hits};
use crate::cmd_index_query::{HitSpans, Hydrated};
type ShardCorpus = (u64, u64, Vec<(Vec<u8>, u32)>);
pub(super) fn reduce_ranked(argv: &[Vec<u8>], chunks: &[Vec<u8>], ascending: bool) -> Vec<u8> {
let mut out = Vec::new();
let Some((limit, fields)) = crate::cmd_index_query::KnnArgs::parse(argv)
.map(|q| (q.limit, q.fields))
.filter(|_| ascending)
else {
encode_error(&mut out, "ERR bad IDX arguments");
return out;
};
merge_ranked(
chunks,
Merge {
limit,
fields: &fields,
ascending,
highlight: false,
offset: 0,
sort_desc: None,
grouped: false,
facets: &[],
},
)
}
pub(super) fn reduce_match_score(argv: &[Vec<u8>], chunks: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
let Some(q) = crate::cmd_index_query::parse_match_score(argv) else {
encode_error(&mut out, "ERR bad IDX arguments");
return out;
};
let sort_desc = q.sort.as_ref().map(|(_, desc)| *desc);
merge_ranked(
chunks,
Merge {
limit: q.limit,
fields: &q.fields,
ascending: false,
highlight: q.highlight.is_some(),
offset: q.offset,
sort_desc,
grouped: q.distinct.is_some(),
facets: &q.facets,
},
)
}
fn emit_rows(
out: &mut Vec<u8>,
all: &[Hit],
fields: &[Vec<u8>],
highlight: bool,
facets: &[Vec<u8>],
buckets: &[Vec<Bucket>],
) {
encode_array_len(out, (all.len() + usize::from(!facets.is_empty())) as i64);
for h in all {
let base = 2 + fields.len() * 2 + usize::from(highlight);
encode_array_len(out, base as i64);
encode_bulk(out, &h.key);
encode_bulk(out, format!("{:.4}", h.score).as_bytes());
for (f, val) in fields.iter().zip(h.fields.iter().chain(std::iter::repeat(&None))) {
encode_bulk(out, f);
match val {
Some(b) => encode_bulk(out, b),
None => out.extend_from_slice(b"$-1\r\n"),
}
}
if highlight {
encode_highlights(out, &h.spans);
}
}
emit_facets(out, facets, buckets);
}
fn emit_facets(out: &mut Vec<u8>, facets: &[Vec<u8>], buckets: &[Vec<Bucket>]) {
if facets.is_empty() {
return;
}
encode_array_len(out, (facets.len() * 2) as i64);
for (name, field) in facets.iter().zip(buckets) {
encode_bulk(out, name);
let mut sorted = field.clone();
sorted.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
encode_array_len(out, (sorted.len() * 2) as i64);
for (_, label, n) in &sorted {
encode_bulk(out, label);
encode_bulk(out, n.to_string().as_bytes());
}
}
}
fn push_filters(argv2: &mut Vec<Vec<u8>>, filters: Vec<crate::cmd_index_query::FilterArg>) {
for f in filters {
argv2.push(b"FILTER".to_vec());
argv2.push(f.field);
match f.shape {
crate::cmd_index_query::FilterShape::Range { min, max } => {
argv2.push(b"RANGE".to_vec());
argv2.push(min);
argv2.push(max);
}
crate::cmd_index_query::FilterShape::Eq { value } => {
argv2.push(b"EQ".to_vec());
argv2.push(value);
}
}
}
}
fn collapse_union(all: &mut Vec<Hit>) {
let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
all.retain(|h| match &h.dkey {
Some(k) => seen.insert(k.clone()),
None => true,
});
}
pub(super) type Bucket = (Vec<u8>, Vec<u8>, u64);
struct Hit {
score: f64,
key: Vec<u8>,
fields: Hydrated,
spans: HitSpans,
okey: Option<Vec<u8>>,
dkey: Option<Vec<u8>>,
}
struct Merge<'a> {
limit: usize,
fields: &'a [Vec<u8>],
ascending: bool,
highlight: bool,
offset: usize,
sort_desc: Option<bool>,
grouped: bool,
facets: &'a [Vec<u8>],
}
fn merge_ranked(chunks: &[Vec<u8>], m: Merge<'_>) -> Vec<u8> {
let Merge { limit, fields, ascending, highlight, offset, sort_desc, grouped, facets } = m;
let mut buckets = vec![Vec::new(); facets.len()];
let mut out = Vec::new();
let mut all: Vec<Hit> = Vec::new();
for c in chunks {
let read = collect_hits(c, highlight, sort_desc.is_some(), grouped, &mut all);
collect_facets(c, read, facets.len(), &mut buckets);
}
match sort_desc {
Some(desc) => all.sort_by(|a, b| {
kevy_text::sorted_order((a.okey.as_deref(), &a.key), (b.okey.as_deref(), &b.key), desc)
}),
None if ascending => {
all.sort_by(|a, b| a.score.total_cmp(&b.score).then_with(|| a.key.cmp(&b.key)));
}
None => all.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key))),
}
if grouped {
collapse_union(&mut all);
}
if offset > 0 {
all.drain(..offset.min(all.len()));
}
all.truncate(limit);
emit_rows(&mut out, &all, fields, highlight, facets, &buckets);
out
}
fn encode_highlights(out: &mut Vec<u8>, hl: &HitSpans) {
encode_array_len(out, hl.len() as i64);
for (name, ranges) in hl {
encode_array_len(out, (1 + ranges.len() * 2) as i64);
encode_bulk(out, name);
for (s, e) in ranges {
encode_bulk(out, s.to_string().as_bytes());
encode_bulk(out, e.to_string().as_bytes());
}
}
}
pub(super) fn reduce_match_stats(argv: &[Vec<u8>], chunks: &[Vec<u8>]) -> ExtensionReduced {
let mut out = Vec::new();
let Some(mut m) = crate::cmd_index_query::MatchArgs::parse(argv) else {
encode_error(&mut out, "ERR bad IDX arguments");
return ExtensionReduced::Reply(out);
};
let (mut n_docs, mut total_len) = (0u64, 0u64);
let mut df: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
for c in chunks {
let Some((nd, tl, tokdf)) = decode_stats_chunk(c) else { continue };
n_docs += nd;
total_len += tl;
for (tok, d) in tokdf {
*df.entry(tok).or_insert(0) += d;
}
}
let avgdl = if n_docs > 0 { total_len as f64 / n_docs as f64 } else { 0.0 };
let blob = encode_gstats_arg(n_docs as f64, avgdl, &df);
let mut argv2: Vec<Vec<u8>> = vec![
b"MATCH.SCORE".to_vec(),
std::mem::take(&mut m.name),
std::mem::take(&mut m.text),
format!("LIMIT={}", m.limit).into_bytes(),
blob,
];
push_clauses(&mut argv2, m);
ExtensionReduced::Continue(argv2)
}
fn push_clauses(argv2: &mut Vec<Vec<u8>>, m: crate::cmd_index_query::MatchArgs) {
if !m.fields.is_empty() {
argv2.push(b"FIELDS".to_vec());
argv2.extend(m.fields);
}
if let Some(hl) = m.highlight {
argv2.push(b"HIGHLIGHT".to_vec());
argv2.extend(hl);
}
if m.typo > 0 {
argv2.push(b"TYPO".to_vec());
argv2.push(m.typo.to_string().into_bytes());
}
if !m.scope.is_empty() {
argv2.push(b"IN".to_vec());
argv2.extend(m.scope);
}
if m.offset > 0 {
argv2.push(b"OFFSET".to_vec());
argv2.push(m.offset.to_string().into_bytes());
}
push_filters(argv2, m.filters);
push_order(argv2, m.sort, m.distinct);
push_facets(argv2, m.facets);
}
fn push_order(argv2: &mut Vec<Vec<u8>>, sort: Option<(Vec<u8>, bool)>, distinct: Option<Vec<u8>>) {
if let Some((field, desc)) = sort {
argv2.push(b"SORT".to_vec());
argv2.push(field);
argv2.push(if desc { b"DESC".to_vec() } else { b"ASC".to_vec() });
}
if let Some(field) = distinct {
argv2.push(b"DISTINCT".to_vec());
argv2.push(field);
}
}
fn push_facets(argv2: &mut Vec<Vec<u8>>, facets: Vec<Vec<u8>>) {
if !facets.is_empty() {
argv2.push(b"FACET".to_vec());
argv2.extend(facets);
}
}
fn encode_gstats_arg(
n_docs: f64,
avgdl: f64,
df: &std::collections::HashMap<Vec<u8>, u32>,
) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&n_docs.to_le_bytes());
b.extend_from_slice(&avgdl.to_le_bytes());
b.extend_from_slice(&(df.len() as u32).to_le_bytes());
for (tok, d) in df {
b.extend_from_slice(&(tok.len() as u32).to_le_bytes());
b.extend_from_slice(tok);
b.extend_from_slice(&d.to_le_bytes());
}
b
}
fn decode_stats_chunk(c: &[u8]) -> Option<ShardCorpus> {
let n_docs = u64::from_le_bytes(c.get(1..9)?.try_into().ok()?);
let total_len = u64::from_le_bytes(c.get(9..17)?.try_into().ok()?);
let ntok = u32::from_le_bytes(c.get(17..21)?.try_into().ok()?) as usize;
let mut pos = 21usize;
let mut tokdf = Vec::with_capacity(ntok);
for _ in 0..ntok {
let tlen = u32::from_le_bytes(c.get(pos..pos + 4)?.try_into().ok()?) as usize;
pos += 4;
let tok = c.get(pos..pos + tlen)?.to_vec();
pos += tlen;
let d = u32::from_le_bytes(c.get(pos..pos + 4)?.try_into().ok()?);
pos += 4;
tokdf.push((tok, d));
}
Some((n_docs, total_len, tokdf))
}
fn read_ranked_segment(c: &[u8], pos: &mut usize) -> Vec<(f64, Vec<u8>, Hydrated)> {
let mut out = Vec::new();
let Some(n) = read_u32(c, pos) else { return out };
for _ in 0..n {
let Some(key) = read_kbytes(c, pos) else { break };
let Some(sb) = c.get(*pos..*pos + 8) else { break };
let v = f64::from_le_bytes(sb.try_into().expect("8 bytes"));
*pos += 8;
let Some(fv) = read_hydration(c, pos) else { break };
out.push((v, key, fv));
}
out
}
pub(super) fn reduce_hybrid(argv: &[Vec<u8>], chunks: &[Vec<u8>]) -> Vec<u8> {
use std::collections::HashMap;
let mut out = Vec::new();
let Some(q) = crate::cmd_index_query::HybridArgs::parse(argv) else {
encode_error(&mut out, "ERR bad IDX arguments");
return out;
};
let mut matches: Vec<(f64, Vec<u8>, Hydrated)> = Vec::new();
let mut knns: Vec<(f64, Vec<u8>, Hydrated)> = Vec::new();
for c in chunks {
let mut pos = 1usize;
matches.extend(read_ranked_segment(c, &mut pos));
knns.extend(read_ranked_segment(c, &mut pos));
}
matches.sort_by(|a, b| b.0.total_cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
knns.sort_by(|a, b| a.0.total_cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
let mut fused: HashMap<Vec<u8>, (f64, Hydrated)> = HashMap::new();
for (rank, (_, key, fv)) in matches.into_iter().enumerate() {
let s = 1.0 / (q.rrf_k + rank as f64 + 1.0);
let e = fused.entry(key).or_insert((0.0, fv));
e.0 += s;
}
for (rank, (_, key, fv)) in knns.into_iter().enumerate() {
let s = 1.0 / (q.rrf_k + rank as f64 + 1.0);
let e = fused.entry(key).or_insert((0.0, fv));
e.0 += s;
}
let mut all: Vec<(f64, Vec<u8>, Hydrated)> =
fused.into_iter().map(|(k, (s, fv))| (s, k, fv)).collect();
all.sort_by(|a, b| b.0.total_cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
all.truncate(q.limit);
encode_array_len(&mut out, all.len() as i64);
for (v, key, fv) in &all {
let base = 2 + q.fields.len() * 2;
encode_array_len(&mut out, base as i64);
encode_bulk(&mut out, key);
encode_bulk(&mut out, format!("{v:.6}").as_bytes());
for (f, val) in q.fields.iter().zip(fv.iter().chain(std::iter::repeat(&None))) {
encode_bulk(&mut out, f);
match val {
Some(v) => encode_bulk(&mut out, v),
None => out.extend_from_slice(b"$-1\r\n"),
}
}
}
out
}