use std::collections::HashMap;
use crate::llmtrim::stages::tools::{fnv1a, lex_words};
pub struct Item {
pub cost: usize,
pub relevance: f64,
pub features: Vec<u64>,
}
impl Item {
pub fn from_text(text: &str, cost: usize, relevance: f64) -> Self {
Item {
cost,
relevance,
features: bigram_hashes(text),
}
}
}
#[derive(Clone, Copy)]
pub struct Weights {
pub lambda: f64,
pub saturation: f64,
}
impl Default for Weights {
fn default() -> Self {
Weights {
lambda: 0.5,
saturation: 0.5,
}
}
}
fn bigram_hashes(text: &str) -> Vec<u64> {
let words = lex_words(text);
let mut seen = Vec::new();
let mut set = std::collections::HashSet::new();
let mut push = |h: u64, seen: &mut Vec<u64>| {
if set.insert(h) {
seen.push(h);
}
};
match words.as_slice() {
[] => {}
[single] => push(token_hash(single, ""), &mut seen),
_ => {
for pair in words.windows(2) {
push(token_hash(&pair[0], &pair[1]), &mut seen);
}
}
}
seen
}
fn token_hash(a: &str, b: &str) -> u64 {
fnv1a(a.bytes().chain(std::iter::once(0x1f)).chain(b.bytes()))
}
pub fn select(items: &[Item], budget: usize, weights: &Weights) -> Vec<usize> {
let n = items.len();
if n == 0 || budget == 0 {
return Vec::new();
}
let mut total: HashMap<u64, f64> = HashMap::new();
for it in items {
for &f in &it.features {
*total.entry(f).or_insert(0.0) += 1.0;
}
}
let cap: HashMap<u64, f64> = total
.into_iter()
.map(|(f, t)| (f, weights.saturation * t))
.collect();
let mut covered: HashMap<u64, f64> = HashMap::new();
let mut chosen: Vec<usize> = Vec::new();
let mut picked = vec![false; n];
let mut spent = 0usize;
let mut heap: std::collections::BinaryHeap<HeapEntry> = items
.iter()
.enumerate()
.filter(|(_, it)| it.cost > 0 && it.cost <= budget)
.map(|(i, it)| {
let gain = marginal(it, &covered, &cap, weights);
HeapEntry::new(gain / it.cost as f64, 0, i)
})
.collect();
let mut selections = 0u64;
while let Some(top) = heap.pop() {
let i = top.idx;
if picked[i] {
continue;
}
let it = &items[i];
if spent + it.cost > budget {
continue;
}
if top.valid_at == selections {
if top.ratio <= 0.0 {
break;
}
for &f in &it.features {
let c = covered.entry(f).or_insert(0.0);
*c = (*c + 1.0).min(cap.get(&f).copied().unwrap_or(0.0));
}
picked[i] = true;
chosen.push(i);
spent += it.cost;
selections += 1;
} else {
let gain = marginal(it, &covered, &cap, weights);
heap.push(HeapEntry::new(gain / it.cost as f64, selections, i));
}
}
chosen.sort_unstable();
chosen
}
fn marginal(
it: &Item,
covered: &HashMap<u64, f64>,
cap: &HashMap<u64, f64>,
weights: &Weights,
) -> f64 {
let cov_gain: f64 = it
.features
.iter()
.map(|f| {
let ceiling = cap.get(f).copied().unwrap_or(0.0);
let now = covered.get(f).copied().unwrap_or(0.0);
(now + 1.0).min(ceiling) - now.min(ceiling)
})
.sum();
weights.lambda * it.relevance + (1.0 - weights.lambda) * cov_gain
}
struct HeapEntry {
ratio: f64,
valid_at: u64,
idx: usize,
}
impl HeapEntry {
fn new(ratio: f64, valid_at: u64, idx: usize) -> Self {
HeapEntry {
ratio,
valid_at,
idx,
}
}
}
impl PartialEq for HeapEntry {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal
}
}
impl Eq for HeapEntry {}
impl PartialOrd for HeapEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HeapEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.ratio
.partial_cmp(&other.ratio)
.unwrap_or(std::cmp::Ordering::Equal)
.then(other.idx.cmp(&self.idx))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cost(text: &str) -> usize {
text.split_whitespace().count().max(1)
}
fn item(text: &str, relevance: f64) -> Item {
Item::from_text(text, cost(text), relevance)
}
#[test]
fn respects_the_token_budget() {
let items: Vec<Item> = (0..5)
.map(|i| item(&format!("alpha{i} beta{i} gamma{i} delta{i}"), 1.0))
.collect();
let picked = select(&items, 10, &Weights::default());
let spent: usize = picked.iter().map(|&i| items[i].cost).sum();
assert!(spent <= 10, "selection cost {spent} exceeds budget 10");
assert!(!picked.is_empty(), "something fits a budget of 10");
}
#[test]
fn empty_and_degenerate_inputs() {
assert!(select(&[], 100, &Weights::default()).is_empty(), "no items");
let items = vec![item("hello world", 1.0)];
assert!(
select(&items, 0, &Weights::default()).is_empty(),
"zero budget selects nothing"
);
let big = vec![Item::from_text("a b c d e", 5, 1.0)];
assert!(
select(&big, 3, &Weights::default()).is_empty(),
"an over-budget item can't be selected"
);
}
#[test]
fn returns_ascending_original_order() {
let items: Vec<Item> = (0..6)
.map(|i| item(&format!("token{i}a token{i}b token{i}c"), 1.0))
.collect();
let picked = select(&items, 100, &Weights::default());
let mut sorted = picked.clone();
sorted.sort_unstable();
assert_eq!(picked, sorted, "indices come back in ascending order");
}
#[test]
fn saturating_coverage_prefers_diverse_over_redundant() {
let items = vec![
item(
"quarterly revenue for logistics was four million dollars",
1.0,
),
item(
"quarterly revenue for logistics was four million dollars",
1.0,
),
item(
"parking available north visitor lot near south elevators",
1.0,
),
];
let picked = select(
&items,
16,
&Weights {
lambda: 0.3,
saturation: 0.5,
},
);
assert!(picked.contains(&2), "the novel item must be selected");
assert!(
!(picked.contains(&0) && picked.contains(&1)),
"the two near-duplicates must not both be chosen: {picked:?}"
);
}
#[test]
fn pure_relevance_lambda_one_is_cost_ratio_top_k() {
let items = vec![
item("aaa bbb ccc", 1.0),
item("ddd eee fff", 2.0),
item("ggg hhh iii", 9.0),
];
let picked = select(
&items,
3,
&Weights {
lambda: 1.0,
saturation: 0.5,
},
);
assert_eq!(
picked,
vec![2],
"highest relevance-per-token wins under λ=1"
);
}
#[test]
fn deterministic_across_runs() {
let items: Vec<Item> = (0..20)
.map(|i| {
let rel = ((i * 7) % 5) as f64; item(&format!("word{}a shared common token{}b", i % 3, i), rel)
})
.collect();
let first = select(&items, 25, &Weights::default());
for _ in 0..5 {
assert_eq!(
select(&items, 25, &Weights::default()),
first,
"selection must be deterministic"
);
}
}
#[test]
fn stops_instead_of_padding_with_zero_value_filler() {
let items = vec![
item("alpha beta gamma delta epsilon", 0.0),
item("alpha beta gamma delta epsilon", 0.0),
];
let picked = select(
&items,
100,
&Weights {
lambda: 0.0,
saturation: 0.5,
},
);
assert_eq!(
picked.len(),
1,
"no zero-value duplicate padding: {picked:?}"
);
}
#[test]
fn lazy_greedy_matches_naive_greedy() {
let texts = [
("red green blue", 3.0),
("red green blue extra", 1.0),
("yellow orange purple", 2.0),
("yellow orange", 0.5),
("cyan magenta", 4.0),
("cyan magenta black white", 2.5),
];
let items: Vec<Item> = texts.iter().map(|(t, r)| item(t, *r)).collect();
let w = Weights {
lambda: 0.4,
saturation: 0.6,
};
let lazy = select(&items, 9, &w);
let eager = naive_greedy(&items, 9, &w);
assert_eq!(lazy, eager, "CELF output diverged from naive greedy");
}
fn naive_greedy(items: &[Item], budget: usize, weights: &Weights) -> Vec<usize> {
let mut total: HashMap<u64, f64> = HashMap::new();
for it in items {
for &f in &it.features {
*total.entry(f).or_insert(0.0) += 1.0;
}
}
let cap: HashMap<u64, f64> = total
.into_iter()
.map(|(f, t)| (f, weights.saturation * t))
.collect();
let mut covered: HashMap<u64, f64> = HashMap::new();
let mut chosen = Vec::new();
let mut picked = vec![false; items.len()];
let mut spent = 0usize;
loop {
let mut best: Option<(f64, usize)> = None;
for (i, it) in items.iter().enumerate() {
if picked[i] || it.cost == 0 || spent + it.cost > budget {
continue;
}
let ratio = marginal(it, &covered, &cap, weights) / it.cost as f64;
if best.is_none_or(|(br, _)| ratio > br) {
best = Some((ratio, i));
}
}
match best {
Some((ratio, i)) if ratio > 0.0 => {
for &f in &items[i].features {
let c = covered.entry(f).or_insert(0.0);
*c = (*c + 1.0).min(cap.get(&f).copied().unwrap_or(0.0));
}
picked[i] = true;
chosen.push(i);
spent += items[i].cost;
}
_ => break,
}
}
chosen.sort_unstable();
chosen
}
}