1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
16pub struct RerankFeatures {
17 pub relevance: f64,
19 pub salience: f64,
21 pub temporal: f64,
23 pub text_match: bool,
25 pub vector_match: bool,
27}
28
29pub fn weighted_rerank(features: &RerankFeatures, weights: &HashMap<String, f64>) -> f64 {
57 let mut numerator = 0.0_f64;
58 let mut weight_sum = 0.0_f64;
59 for (name, &weight) in weights {
60 if weight == 0.0 {
61 continue;
62 }
63 let feature_value = match name.as_str() {
64 "relevance" => features.relevance,
65 "salience" => features.salience,
66 "temporal" => features.temporal,
67 "text_match" => f64::from(features.text_match),
68 "vector_match" => f64::from(features.vector_match),
69 _ => continue,
71 };
72 numerator += weight * feature_value;
73 if weight > 0.0 {
74 weight_sum += weight;
75 }
76 }
77 if weight_sum == 0.0 {
78 return 0.0;
79 }
80 numerator / weight_sum
81}
82
83#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn features() -> RerankFeatures {
90 RerankFeatures {
91 relevance: 0.8,
92 salience: 0.6,
93 temporal: 0.4,
94 text_match: true,
95 vector_match: false,
96 }
97 }
98
99 #[test]
100 fn empty_weights_returns_zero() {
101 let score = weighted_rerank(&features(), &HashMap::new());
102 assert_eq!(score, 0.0, "empty weights must return 0.0");
103 }
104
105 #[test]
106 fn single_relevance_weight_produces_expected_score() {
107 let weights: HashMap<String, f64> = [("relevance".to_string(), 1.0)].into_iter().collect();
108 let score = weighted_rerank(&features(), &weights);
109 let diff = (score - 0.8).abs();
110 assert!(
111 diff < 1e-12,
112 "relevance weight=1.0 on relevance=0.8 should give 0.8, got {score}"
113 );
114 }
115
116 #[test]
117 fn single_salience_weight_produces_expected_score() {
118 let weights: HashMap<String, f64> = [("salience".to_string(), 2.0)].into_iter().collect();
121 let score = weighted_rerank(&features(), &weights);
122 let diff = (score - 0.6).abs();
123 assert!(
124 diff < 1e-12,
125 "salience weight=2.0 on salience=0.6 should normalize to 0.6, got {score}"
126 );
127 }
128
129 #[test]
130 fn multi_feature_weight_produces_expected_combination() {
131 let weights: HashMap<String, f64> = [
135 ("relevance".to_string(), 0.5),
136 ("salience".to_string(), 0.3),
137 ("temporal".to_string(), 0.2),
138 ]
139 .into_iter()
140 .collect();
141 let score = weighted_rerank(&features(), &weights);
142 let diff = (score - 0.66).abs();
143 assert!(
144 diff < 1e-12,
145 "multi-feature combination should give 0.66, got {score}"
146 );
147 }
148
149 #[test]
150 fn boolean_text_match_feature() {
151 let weights: HashMap<String, f64> = [
154 ("text_match".to_string(), 0.1),
155 ("vector_match".to_string(), 0.5),
156 ]
157 .into_iter()
158 .collect();
159 let score = weighted_rerank(&features(), &weights);
160 let expected = 0.1_f64 / 0.6_f64;
161 let diff = (score - expected).abs();
162 assert!(
163 diff < 1e-12,
164 "boolean features: (text_match*0.1 + vector_match*0.5) / 0.6 ≈ 0.16667, got {score}"
165 );
166 }
167
168 #[test]
169 fn unknown_feature_key_is_silently_ignored() {
170 let weights: HashMap<String, f64> = [
171 ("relevance".to_string(), 1.0),
172 ("future_feature_xyz".to_string(), 999.0),
173 ]
174 .into_iter()
175 .collect();
176 let score = weighted_rerank(&features(), &weights);
177 let diff = (score - 0.8).abs();
179 assert!(
180 diff < 1e-12,
181 "unknown key should be ignored, expected 0.8, got {score}"
182 );
183 }
184
185 #[test]
186 fn zero_weight_entry_is_skipped() {
187 let weights: HashMap<String, f64> = [
188 ("relevance".to_string(), 0.0),
189 ("salience".to_string(), 1.0),
190 ]
191 .into_iter()
192 .collect();
193 let score = weighted_rerank(&features(), &weights);
194 let diff = (score - 0.6).abs();
196 assert!(
197 diff < 1e-12,
198 "zero-weight key should not contribute, expected 0.6, got {score}"
199 );
200 }
201
202 #[test]
206 fn doubling_all_weights_does_not_change_score() {
207 let weights_1x: HashMap<String, f64> = [
208 ("relevance".to_string(), 1.0),
209 ("salience".to_string(), 0.3),
210 ]
211 .into_iter()
212 .collect();
213 let weights_2x: HashMap<String, f64> = [
214 ("relevance".to_string(), 2.0),
215 ("salience".to_string(), 0.6),
216 ]
217 .into_iter()
218 .collect();
219 let score_1x = weighted_rerank(&features(), &weights_1x);
220 let score_2x = weighted_rerank(&features(), &weights_2x);
221 let diff = (score_1x - score_2x).abs();
222 assert!(
223 diff < 1e-12,
224 "doubling all weights must produce identical score: 1x={score_1x} 2x={score_2x}"
225 );
226 }
227
228 #[test]
231 fn single_weight_of_any_magnitude_returns_feature_value() {
232 let f = features(); for &mag in &[0.5_f64, 1.0, 2.0, 100.0] {
234 let weights: HashMap<String, f64> =
235 [("relevance".to_string(), mag)].into_iter().collect();
236 let score = weighted_rerank(&f, &weights);
237 let diff = (score - f.relevance).abs();
238 assert!(
239 diff < 1e-12,
240 "single weight={mag}: expected feature value {}, got {score}",
241 f.relevance
242 );
243 }
244 }
245}