1use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
7pub struct RerankFeatures {
8 pub relevance: f64,
10 pub salience: f64,
12 pub temporal: f64,
14 pub text_match: bool,
16 pub vector_match: bool,
18}
19
20pub fn weighted_rerank(features: &RerankFeatures, weights: &HashMap<String, f64>) -> f64 {
23 let mut numerator = 0.0_f64;
24 let mut weight_sum = 0.0_f64;
25 for (name, &weight) in weights {
26 if weight == 0.0 {
27 continue;
28 }
29 let feature_value = match name.as_str() {
30 "relevance" => features.relevance,
31 "salience" => features.salience,
32 "temporal" => features.temporal,
33 "text_match" => f64::from(features.text_match),
34 "vector_match" => f64::from(features.vector_match),
35 _ => continue,
37 };
38 numerator += weight * feature_value;
39 if weight > 0.0 {
40 weight_sum += weight;
41 }
42 }
43 if weight_sum == 0.0 {
44 return 0.0;
45 }
46 numerator / weight_sum
47}
48
49#[cfg(test)]
52mod tests {
53 use super::*;
54
55 fn features() -> RerankFeatures {
56 RerankFeatures {
57 relevance: 0.8,
58 salience: 0.6,
59 temporal: 0.4,
60 text_match: true,
61 vector_match: false,
62 }
63 }
64
65 #[test]
66 fn empty_weights_returns_zero() {
67 let score = weighted_rerank(&features(), &HashMap::new());
68 assert_eq!(score, 0.0, "empty weights must return 0.0");
69 }
70
71 #[test]
72 fn single_relevance_weight_produces_expected_score() {
73 let weights: HashMap<String, f64> = [("relevance".to_string(), 1.0)].into_iter().collect();
74 let score = weighted_rerank(&features(), &weights);
75 let diff = (score - 0.8).abs();
76 assert!(
77 diff < 1e-12,
78 "relevance weight=1.0 on relevance=0.8 should give 0.8, got {score}"
79 );
80 }
81
82 #[test]
83 fn single_salience_weight_produces_expected_score() {
84 let weights: HashMap<String, f64> = [("salience".to_string(), 2.0)].into_iter().collect();
87 let score = weighted_rerank(&features(), &weights);
88 let diff = (score - 0.6).abs();
89 assert!(
90 diff < 1e-12,
91 "salience weight=2.0 on salience=0.6 should normalize to 0.6, got {score}"
92 );
93 }
94
95 #[test]
96 fn multi_feature_weight_produces_expected_combination() {
97 let weights: HashMap<String, f64> = [
101 ("relevance".to_string(), 0.5),
102 ("salience".to_string(), 0.3),
103 ("temporal".to_string(), 0.2),
104 ]
105 .into_iter()
106 .collect();
107 let score = weighted_rerank(&features(), &weights);
108 let diff = (score - 0.66).abs();
109 assert!(
110 diff < 1e-12,
111 "multi-feature combination should give 0.66, got {score}"
112 );
113 }
114
115 #[test]
116 fn boolean_text_match_feature() {
117 let weights: HashMap<String, f64> = [
120 ("text_match".to_string(), 0.1),
121 ("vector_match".to_string(), 0.5),
122 ]
123 .into_iter()
124 .collect();
125 let score = weighted_rerank(&features(), &weights);
126 let expected = 0.1_f64 / 0.6_f64;
127 let diff = (score - expected).abs();
128 assert!(
129 diff < 1e-12,
130 "boolean features: (text_match*0.1 + vector_match*0.5) / 0.6 ≈ 0.16667, got {score}"
131 );
132 }
133
134 #[test]
135 fn unknown_feature_key_is_silently_ignored() {
136 let weights: HashMap<String, f64> = [
137 ("relevance".to_string(), 1.0),
138 ("future_feature_xyz".to_string(), 999.0),
139 ]
140 .into_iter()
141 .collect();
142 let score = weighted_rerank(&features(), &weights);
143 let diff = (score - 0.8).abs();
145 assert!(
146 diff < 1e-12,
147 "unknown key should be ignored, expected 0.8, got {score}"
148 );
149 }
150
151 #[test]
152 fn zero_weight_entry_is_skipped() {
153 let weights: HashMap<String, f64> = [
154 ("relevance".to_string(), 0.0),
155 ("salience".to_string(), 1.0),
156 ]
157 .into_iter()
158 .collect();
159 let score = weighted_rerank(&features(), &weights);
160 let diff = (score - 0.6).abs();
162 assert!(
163 diff < 1e-12,
164 "zero-weight key should not contribute, expected 0.6, got {score}"
165 );
166 }
167
168 #[test]
172 fn doubling_all_weights_does_not_change_score() {
173 let weights_1x: HashMap<String, f64> = [
174 ("relevance".to_string(), 1.0),
175 ("salience".to_string(), 0.3),
176 ]
177 .into_iter()
178 .collect();
179 let weights_2x: HashMap<String, f64> = [
180 ("relevance".to_string(), 2.0),
181 ("salience".to_string(), 0.6),
182 ]
183 .into_iter()
184 .collect();
185 let score_1x = weighted_rerank(&features(), &weights_1x);
186 let score_2x = weighted_rerank(&features(), &weights_2x);
187 let diff = (score_1x - score_2x).abs();
188 assert!(
189 diff < 1e-12,
190 "doubling all weights must produce identical score: 1x={score_1x} 2x={score_2x}"
191 );
192 }
193
194 #[test]
197 fn single_weight_of_any_magnitude_returns_feature_value() {
198 let f = features(); for &mag in &[0.5_f64, 1.0, 2.0, 100.0] {
200 let weights: HashMap<String, f64> =
201 [("relevance".to_string(), mag)].into_iter().collect();
202 let score = weighted_rerank(&f, &weights);
203 let diff = (score - f.relevance).abs();
204 assert!(
205 diff < 1e-12,
206 "single weight={mag}: expected feature value {}, got {score}",
207 f.relevance
208 );
209 }
210 }
211}