hermes_core/query/vector/
combiner.rs1#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum MultiValueCombiner {
6 Sum,
8 Max,
10 Avg,
12 LogSumExp {
16 temperature: f32,
18 },
19 WeightedTopK {
22 k: usize,
24 decay: f32,
26 },
27}
28
29impl Default for MultiValueCombiner {
30 fn default() -> Self {
31 MultiValueCombiner::LogSumExp { temperature: 1.5 }
34 }
35}
36
37impl MultiValueCombiner {
38 pub(crate) fn validate(self) -> Result<(), String> {
39 match self {
40 Self::LogSumExp { temperature } if !temperature.is_finite() || temperature <= 0.0 => {
41 Err(format!(
42 "LogSumExp temperature must be finite and greater than zero, got {temperature}"
43 ))
44 }
45 Self::WeightedTopK { k: 0, .. } => {
46 Err("WeightedTopK k must be greater than zero".to_string())
47 }
48 Self::WeightedTopK { decay, .. }
49 if !decay.is_finite() || !(0.0..=1.0).contains(&decay) =>
50 {
51 Err(format!(
52 "WeightedTopK decay must be finite and in [0, 1], got {decay}"
53 ))
54 }
55 _ => Ok(()),
56 }
57 }
58
59 pub fn log_sum_exp() -> Self {
61 Self::LogSumExp { temperature: 1.5 }
62 }
63
64 pub fn log_sum_exp_with_temperature(temperature: f32) -> Self {
66 Self::LogSumExp { temperature }
67 }
68
69 pub fn weighted_top_k() -> Self {
71 Self::WeightedTopK { k: 5, decay: 0.7 }
72 }
73
74 pub fn weighted_top_k_with_params(k: usize, decay: f32) -> Self {
76 Self::WeightedTopK { k, decay }
77 }
78
79 pub fn combine(&self, scores: &[(u32, f32)]) -> f32 {
81 if scores.is_empty() {
82 return 0.0;
83 }
84
85 match self {
86 MultiValueCombiner::Sum => scores.iter().map(|(_, s)| s).sum(),
87 MultiValueCombiner::Max => scores
88 .iter()
89 .map(|(_, s)| *s)
90 .max_by(|a, b| a.total_cmp(b))
91 .unwrap_or(0.0),
92 MultiValueCombiner::Avg => {
93 let sum: f32 = scores.iter().map(|(_, s)| s).sum();
94 sum / scores.len() as f32
95 }
96 MultiValueCombiner::LogSumExp { temperature } => {
97 let t = *temperature;
100 let max_score = scores
101 .iter()
102 .map(|(_, s)| *s)
103 .max_by(|a, b| a.total_cmp(b))
104 .unwrap_or(0.0);
105
106 let sum_exp: f32 = scores
107 .iter()
108 .map(|(_, s)| (t * (s - max_score)).exp())
109 .sum();
110
111 max_score + sum_exp.ln() / t
112 }
113 MultiValueCombiner::WeightedTopK { k, decay } => {
114 let mut sorted: Vec<f32> = scores.iter().map(|(_, s)| *s).collect();
116 sorted.sort_unstable_by(|a, b| b.total_cmp(a));
117 sorted.truncate(*k);
118
119 let mut weight = 1.0f32;
121 let mut weighted_sum = 0.0f32;
122 let mut weight_total = 0.0f32;
123
124 for score in sorted {
125 weighted_sum += weight * score;
126 weight_total += weight;
127 weight *= decay;
128 }
129
130 if weight_total > 0.0 {
131 weighted_sum / weight_total
132 } else {
133 0.0
134 }
135 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_combiner_sum() {
146 let scores = vec![(0, 1.0), (1, 2.0), (2, 3.0)];
147 let combiner = MultiValueCombiner::Sum;
148 assert!((combiner.combine(&scores) - 6.0).abs() < 1e-6);
149 }
150
151 #[test]
152 fn test_combiner_max() {
153 let scores = vec![(0, 1.0), (1, 3.0), (2, 2.0)];
154 let combiner = MultiValueCombiner::Max;
155 assert!((combiner.combine(&scores) - 3.0).abs() < 1e-6);
156 }
157
158 #[test]
159 fn test_combiner_avg() {
160 let scores = vec![(0, 1.0), (1, 2.0), (2, 3.0)];
161 let combiner = MultiValueCombiner::Avg;
162 assert!((combiner.combine(&scores) - 2.0).abs() < 1e-6);
163 }
164
165 #[test]
166 fn test_combiner_log_sum_exp() {
167 let scores = vec![(0, 1.0), (1, 2.0), (2, 3.0)];
168 let combiner = MultiValueCombiner::log_sum_exp();
169 let result = combiner.combine(&scores);
170 assert!(result >= 3.0);
172 assert!(result <= 3.0 + (3.0_f32).ln() / 1.5);
173 }
174
175 #[test]
176 fn test_combiner_log_sum_exp_approaches_max_with_high_temp() {
177 let scores = vec![(0, 1.0), (1, 5.0), (2, 2.0)];
178 let combiner = MultiValueCombiner::log_sum_exp_with_temperature(10.0);
180 let result = combiner.combine(&scores);
181 assert!((result - 5.0).abs() < 0.5);
183 }
184
185 #[test]
186 fn test_combiner_weighted_top_k() {
187 let scores = vec![(0, 5.0), (1, 3.0), (2, 1.0), (3, 0.5)];
188 let combiner = MultiValueCombiner::weighted_top_k_with_params(3, 0.5);
189 let result = combiner.combine(&scores);
190 assert!((result - 3.857).abs() < 0.01);
195 }
196
197 #[test]
198 fn test_combiner_weighted_top_k_less_than_k() {
199 let scores = vec![(0, 2.0), (1, 1.0)];
200 let combiner = MultiValueCombiner::weighted_top_k_with_params(5, 0.7);
201 let result = combiner.combine(&scores);
202 assert!((result - 1.588).abs() < 0.01);
207 }
208
209 #[test]
210 fn test_combiner_empty_scores() {
211 let scores: Vec<(u32, f32)> = vec![];
212 assert_eq!(MultiValueCombiner::Sum.combine(&scores), 0.0);
213 assert_eq!(MultiValueCombiner::Max.combine(&scores), 0.0);
214 assert_eq!(MultiValueCombiner::Avg.combine(&scores), 0.0);
215 assert_eq!(MultiValueCombiner::log_sum_exp().combine(&scores), 0.0);
216 assert_eq!(MultiValueCombiner::weighted_top_k().combine(&scores), 0.0);
217 }
218
219 #[test]
220 fn test_combiner_single_score() {
221 let scores = vec![(0, 5.0)];
222 assert!((MultiValueCombiner::Sum.combine(&scores) - 5.0).abs() < 1e-6);
224 assert!((MultiValueCombiner::Max.combine(&scores) - 5.0).abs() < 1e-6);
225 assert!((MultiValueCombiner::Avg.combine(&scores) - 5.0).abs() < 1e-6);
226 assert!((MultiValueCombiner::log_sum_exp().combine(&scores) - 5.0).abs() < 1e-6);
227 assert!((MultiValueCombiner::weighted_top_k().combine(&scores) - 5.0).abs() < 1e-6);
228 }
229
230 #[test]
231 fn test_default_combiner_is_log_sum_exp() {
232 let combiner = MultiValueCombiner::default();
233 match combiner {
234 MultiValueCombiner::LogSumExp { temperature } => {
235 assert!((temperature - 1.5).abs() < 1e-6);
236 }
237 _ => panic!("Default combiner should be LogSumExp"),
238 }
239 }
240}