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