use std::collections::{HashMap, HashSet};
use std::num::NonZeroUsize;
use khive_fusion::reciprocal_rank_fusion;
use khive_score::DeterministicScore;
use serde::Serialize;
use uuid::Uuid;
use crate::{Result, RetrievalError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryVariantLimits {
max_generated_variants: usize,
max_variant_bytes: usize,
max_generated_bytes: usize,
max_output_tokens: usize,
max_fused_candidates: usize,
}
impl QueryVariantLimits {
pub fn try_new(
max_generated_variants: usize,
max_variant_bytes: usize,
max_generated_bytes: usize,
max_output_tokens: usize,
max_fused_candidates: usize,
) -> Result<Self> {
for (value, ceiling, message) in [
(
max_generated_variants,
3,
"max_generated_variants must be in 1..=3",
),
(
max_variant_bytes,
256,
"max_variant_bytes must be in 1..=256",
),
(
max_generated_bytes,
1_024,
"max_generated_bytes must be in 1..=1024",
),
(
max_output_tokens,
192,
"max_output_tokens must be in 1..=192",
),
(
max_fused_candidates,
127,
"max_fused_candidates must be in 1..=127",
),
] {
if value == 0 || value > ceiling {
return Err(RetrievalError::configuration(message));
}
}
Ok(Self {
max_generated_variants,
max_variant_bytes,
max_generated_bytes,
max_output_tokens,
max_fused_candidates,
})
}
pub fn max_generated_variants(&self) -> usize {
self.max_generated_variants
}
pub fn max_variant_bytes(&self) -> usize {
self.max_variant_bytes
}
pub fn max_generated_bytes(&self) -> usize {
self.max_generated_bytes
}
pub fn max_output_tokens(&self) -> usize {
self.max_output_tokens
}
pub fn max_fused_candidates(&self) -> usize {
self.max_fused_candidates
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum QueryVariantOrigin {
Original,
Generated,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct QueryVariant {
pub id: u8,
pub origin: QueryVariantOrigin,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryVariantRejection {
TooManyItems,
OutputTokenLimit,
VariantByteLimit,
AggregateByteLimit,
EmptyVariant,
NulByte,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryVariants {
variants: Vec<QueryVariant>,
rejection: Option<QueryVariantRejection>,
}
impl QueryVariants {
pub fn variants(&self) -> &[QueryVariant] {
&self.variants
}
pub fn rejection(&self) -> Option<QueryVariantRejection> {
self.rejection
}
}
pub fn normalize_query_variants(
original: &str,
generated: &[String],
generated_output_tokens: usize,
limits: &QueryVariantLimits,
) -> QueryVariants {
let mut result = QueryVariants {
variants: vec![QueryVariant {
id: 0,
origin: QueryVariantOrigin::Original,
text: original.to_owned(),
}],
rejection: None,
};
if let Err(reason) = validate_generated(generated, generated_output_tokens, limits) {
result.rejection = Some(reason);
return result;
}
for text in generated {
let text = trim_generated(text);
if !result.variants.iter().any(|variant| variant.text == text) {
result.variants.push(QueryVariant {
id: result.variants.len() as u8,
origin: QueryVariantOrigin::Generated,
text: text.to_owned(),
});
}
}
result
}
fn trim_generated(text: &str) -> &str {
text.trim_matches([' ', '\t', '\r', '\n'])
}
fn validate_generated(
generated: &[String],
output_tokens: usize,
limits: &QueryVariantLimits,
) -> std::result::Result<(), QueryVariantRejection> {
if generated.len() > limits.max_generated_variants {
return Err(QueryVariantRejection::TooManyItems);
}
if output_tokens > limits.max_output_tokens {
return Err(QueryVariantRejection::OutputTokenLimit);
}
let mut total_bytes = 0usize;
for text in generated {
if text.len() > limits.max_variant_bytes {
return Err(QueryVariantRejection::VariantByteLimit);
}
if text.contains('\0') {
return Err(QueryVariantRejection::NulByte);
}
total_bytes = total_bytes
.checked_add(text.len())
.ok_or(QueryVariantRejection::AggregateByteLimit)?;
if total_bytes > limits.max_generated_bytes {
return Err(QueryVariantRejection::AggregateByteLimit);
}
if trim_generated(text).is_empty() {
return Err(QueryVariantRejection::EmptyVariant);
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VariantSource {
Lexical,
Ann,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct VariantAttribution {
pub variant_id: u8,
pub source: VariantSource,
pub source_rank: NonZeroUsize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct VariantRankedList<'a> {
pub variant_id: u8,
pub ranked_ids: &'a [Uuid],
pub lexical_ids: &'a [Uuid],
pub ann_ids: &'a [Uuid],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AdmittedVariantHit {
pub id: Uuid,
pub variant_attribution: Vec<VariantAttribution>,
}
pub fn merge_query_variants(
variants: &QueryVariants,
lists: &[VariantRankedList<'_>],
limits: &QueryVariantLimits,
) -> Result<Option<Vec<AdmittedVariantHit>>> {
let mut ordered: [Option<&VariantRankedList<'_>>; 4] = [None; 4];
for list in lists {
let index = usize::from(list.variant_id);
if index >= variants.variants.len() {
return Err(RetrievalError::invalid_query("unknown query variant list"));
}
if ordered[index].replace(list).is_some() {
return Err(RetrievalError::invalid_query(
"duplicate query variant list",
));
}
if list.ranked_ids.len().checked_add(60).is_none() {
return Err(RetrievalError::invalid_query("query variant rank overflow"));
}
let trace_ids: HashSet<_> = list.lexical_ids.iter().chain(list.ann_ids).collect();
if list.ranked_ids.iter().any(|id| !trace_ids.contains(id)) {
return Err(RetrievalError::invalid_query(
"query variant ranked ID has no channel occurrence",
));
}
}
if ordered[0].is_none() {
return Err(RetrievalError::invalid_query(
"missing original query variant list",
));
}
if !ordered[1..]
.iter()
.flatten()
.any(|list| !list.ranked_ids.is_empty())
{
return Ok(None);
}
let sources = ordered
.iter()
.flatten()
.map(|list| {
list.ranked_ids
.iter()
.map(|id| (*id, DeterministicScore::ZERO))
.collect()
})
.collect();
let mut admitted: Vec<_> = reciprocal_rank_fusion(sources, 60)
.into_iter()
.take(limits.max_fused_candidates)
.map(|(id, _)| AdmittedVariantHit {
id,
variant_attribution: Vec::new(),
})
.collect();
let positions: HashMap<_, _> = admitted
.iter()
.enumerate()
.map(|(index, hit)| (hit.id, index))
.collect();
for list in ordered.iter().flatten() {
for (source, ids) in [
(VariantSource::Lexical, list.lexical_ids),
(VariantSource::Ann, list.ann_ids),
] {
for (index, id) in ids.iter().enumerate() {
if let Some(&position) = positions.get(id) {
let source_rank = NonZeroUsize::MIN.checked_add(index).ok_or_else(|| {
RetrievalError::invalid_query("query variant source rank overflow")
})?;
admitted[position]
.variant_attribution
.push(VariantAttribution {
variant_id: list.variant_id,
source,
source_rank,
});
}
}
}
}
for hit in &mut admitted {
hit.variant_attribution.sort_unstable();
hit.variant_attribution.dedup();
}
Ok(Some(admitted))
}