1use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use tracing::{debug, info, warn};
15
16pub struct LearnedReranker {
18 config: RerankConfig,
19 feature_extractors: Vec<Box<dyn FeatureExtractor + Send + Sync>>,
21 isotonic_models: Arc<RwLock<HashMap<String, IsotonicRegressor>>>,
23 model_weights: Arc<RwLock<Option<Vec<f32>>>>,
25 metrics: Arc<RwLock<RerankMetrics>>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RerankConfig {
31 pub top_k: usize,
33 pub use_isotonic: bool,
35 pub learning_rate: f32,
37 pub l2_regularization: f32,
39 pub min_training_samples: usize,
41 pub combination_strategy: CombinationStrategy,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum CombinationStrategy {
47 Linear,
49 LearnedWeights,
51 Ensemble,
53}
54
55#[derive(Debug, Clone)]
57pub struct SearchResult {
58 pub id: String,
59 pub content: String,
60 pub file_path: String,
61 pub initial_score: f32,
62 pub lexical_score: f32,
63 pub semantic_score: Option<f32>,
64 pub lsp_score: Option<f32>,
65 pub metadata: HashMap<String, String>,
66}
67
68#[derive(Debug, Clone)]
70pub struct RerankedResult {
71 pub result: SearchResult,
72 pub rerank_score: f32,
73 pub feature_vector: Vec<f32>,
74 pub calibrated_score: f32,
75 pub rank_change: i32, }
77
78pub trait FeatureExtractor {
80 fn extract_features(&self, query: &str, result: &SearchResult) -> Result<Vec<f32>>;
82
83 fn feature_names(&self) -> Vec<String>;
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct IsotonicRegressor {
90 x_values: Vec<f32>,
92 y_values: Vec<f32>,
94 sample_count: usize,
96}
97
98#[derive(Debug, Clone)]
100pub struct TrainingSample {
101 pub query: String,
102 pub results: Vec<SearchResult>,
103 pub relevance_scores: Vec<f32>, pub ideal_ranking: Vec<usize>, }
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct RerankMetrics {
110 pub samples_processed: usize,
111 pub avg_ndcg_improvement: f32,
112 pub avg_precision_improvement: f32,
113 pub avg_recall_improvement: f32,
114 pub calibration_error: f32,
115 pub feature_importances: HashMap<String, f32>,
116}
117
118impl LearnedReranker {
119 pub async fn new(config: RerankConfig) -> Result<Self> {
121 info!("Creating learned reranker");
122 info!("Top-K: {}, isotonic: {}, learning rate: {}",
123 config.top_k, config.use_isotonic, config.learning_rate);
124
125 let mut feature_extractors: Vec<Box<dyn FeatureExtractor + Send + Sync>> = Vec::new();
127 feature_extractors.push(Box::new(LexicalFeatureExtractor::new()));
128 feature_extractors.push(Box::new(SemanticFeatureExtractor::new()));
129 feature_extractors.push(Box::new(StructuralFeatureExtractor::new()));
130 feature_extractors.push(Box::new(LSPFeatureExtractor::new()));
131
132 Ok(Self {
133 config,
134 feature_extractors,
135 isotonic_models: Arc::new(RwLock::new(HashMap::new())),
136 model_weights: Arc::new(RwLock::new(None)),
137 metrics: Arc::new(RwLock::new(RerankMetrics::default())),
138 })
139 }
140
141 pub async fn rerank(&self, query: &str, results: Vec<SearchResult>) -> Result<Vec<RerankedResult>> {
143 if results.is_empty() {
144 return Ok(Vec::new());
145 }
146
147 let top_k_results = results.into_iter()
149 .take(self.config.top_k)
150 .collect::<Vec<_>>();
151
152 debug!("Reranking {} results for query: '{}'", top_k_results.len(), query);
153
154 let feature_results = self.extract_all_features(query, &top_k_results).await?;
156
157 let scored_results = self.apply_learned_model(feature_results).await?;
159
160 let calibrated_results = if self.config.use_isotonic {
162 self.apply_isotonic_calibration(scored_results).await?
163 } else {
164 scored_results.into_iter()
165 .map(|mut r| { r.calibrated_score = r.rerank_score; r })
166 .collect()
167 };
168
169 let mut final_results = calibrated_results;
171 final_results.sort_by(|a, b| b.calibrated_score.partial_cmp(&a.calibrated_score).unwrap());
172
173 let original_order: HashMap<String, usize> = top_k_results.iter()
175 .enumerate()
176 .map(|(i, r)| (r.id.clone(), i))
177 .collect();
178
179 for (new_rank, result) in final_results.iter_mut().enumerate() {
180 let original_rank = original_order.get(&result.result.id).unwrap_or(&0);
181 result.rank_change = *original_rank as i32 - new_rank as i32;
182 }
183
184 debug!("Reranking complete, {} results reordered", final_results.len());
185
186 Ok(final_results)
187 }
188
189 pub async fn train(&self, training_samples: &[TrainingSample]) -> Result<()> {
191 info!("Training reranker on {} samples", training_samples.len());
192
193 if training_samples.len() < self.config.min_training_samples {
194 anyhow::bail!("Insufficient training samples: {} < {}",
195 training_samples.len(), self.config.min_training_samples);
196 }
197
198 let mut feature_vectors = Vec::new();
200 let mut target_scores = Vec::new();
201
202 for sample in training_samples {
203 let features = self.extract_training_features(sample).await?;
204 feature_vectors.extend(features);
205 target_scores.extend(&sample.relevance_scores);
206 }
207
208 let weights = self.train_linear_model(&feature_vectors, &target_scores)?;
210 *self.model_weights.write().await = Some(weights);
211
212 if self.config.use_isotonic {
214 self.train_isotonic_regressors(training_samples).await?;
215 }
216
217 self.update_training_metrics(training_samples).await?;
219
220 info!("Reranker training complete");
221 Ok(())
222 }
223
224 pub async fn evaluate(&self, test_samples: &[TrainingSample]) -> Result<RerankMetrics> {
226 info!("Evaluating reranker on {} test samples", test_samples.len());
227
228 let mut total_ndcg_improvement = 0.0;
229 let mut total_precision_improvement = 0.0;
230 let mut total_recall_improvement = 0.0;
231 let mut valid_samples = 0;
232
233 for sample in test_samples {
234 let original_results = sample.results.clone();
236
237 let reranked_results = self.rerank(&sample.query, original_results.clone()).await?;
239
240 let ndcg_original = self.calculate_ndcg(&original_results, &sample.relevance_scores);
242 let ndcg_reranked = self.calculate_ndcg_reranked(&reranked_results, &sample.relevance_scores);
243
244 let precision_original = self.calculate_precision_at_k(&original_results, &sample.relevance_scores, 10);
245 let precision_reranked = self.calculate_precision_at_k_reranked(&reranked_results, &sample.relevance_scores, 10);
246
247 if ndcg_original > 0.0 {
248 total_ndcg_improvement += (ndcg_reranked - ndcg_original) / ndcg_original;
249 total_precision_improvement += (precision_reranked - precision_original) / precision_original.max(0.001);
250 valid_samples += 1;
251 }
252 }
253
254 let avg_ndcg_improvement = if valid_samples > 0 {
255 total_ndcg_improvement / valid_samples as f32
256 } else {
257 0.0
258 };
259
260 let avg_precision_improvement = if valid_samples > 0 {
261 total_precision_improvement / valid_samples as f32
262 } else {
263 0.0
264 };
265
266 let metrics = RerankMetrics {
267 samples_processed: valid_samples,
268 avg_ndcg_improvement,
269 avg_precision_improvement,
270 avg_recall_improvement: 0.0, calibration_error: self.calculate_calibration_error(test_samples).await?,
272 feature_importances: self.get_feature_importances().await,
273 };
274
275 info!("Evaluation complete: nDCG improvement {:.3}, precision improvement {:.3}",
276 metrics.avg_ndcg_improvement, metrics.avg_precision_improvement);
277
278 Ok(metrics)
279 }
280
281 pub async fn get_metrics(&self) -> RerankMetrics {
283 self.metrics.read().await.clone()
284 }
285
286 async fn extract_all_features(&self, query: &str, results: &[SearchResult]) -> Result<Vec<FeatureResult>> {
289 let mut feature_results = Vec::with_capacity(results.len());
290
291 for result in results {
292 let mut all_features = Vec::new();
293
294 for extractor in &self.feature_extractors {
296 let features = extractor.extract_features(query, result)
297 .context("Feature extraction failed")?;
298 all_features.extend(features);
299 }
300
301 feature_results.push(FeatureResult {
302 result: result.clone(),
303 features: all_features,
304 });
305 }
306
307 Ok(feature_results)
308 }
309
310 async fn apply_learned_model(&self, feature_results: Vec<FeatureResult>) -> Result<Vec<RerankedResult>> {
311 let weights_guard = self.model_weights.read().await;
312
313 let default_weights;
315 let weights = if let Some(trained_weights) = weights_guard.as_ref() {
316 trained_weights
317 } else {
318 warn!("Reranker not trained, using default uniform weights for benchmark testing");
319 let feature_dim = if !feature_results.is_empty() {
321 feature_results[0].features.len()
322 } else {
323 12 };
325 default_weights = vec![1.0 / feature_dim as f32; feature_dim];
326 &default_weights
327 };
328
329 let mut reranked = Vec::with_capacity(feature_results.len());
330
331 for feature_result in feature_results {
332 let rerank_score = if feature_result.features.len() == weights.len() {
334 feature_result.features.iter()
335 .zip(weights.iter())
336 .map(|(f, w)| f * w)
337 .sum()
338 } else {
339 warn!("Feature dimension mismatch: {} vs {}", feature_result.features.len(), weights.len());
340 feature_result.result.initial_score };
342
343 reranked.push(RerankedResult {
344 result: feature_result.result,
345 rerank_score,
346 feature_vector: feature_result.features,
347 calibrated_score: rerank_score, rank_change: 0, });
350 }
351
352 Ok(reranked)
353 }
354
355 async fn apply_isotonic_calibration(&self, mut results: Vec<RerankedResult>) -> Result<Vec<RerankedResult>> {
356 let models = self.isotonic_models.read().await;
357
358 if let Some(model) = models.get("global") {
361 for result in &mut results {
362 result.calibrated_score = model.predict(result.rerank_score);
363 }
364 } else {
365 for result in &mut results {
367 result.calibrated_score = result.rerank_score;
368 }
369 }
370
371 Ok(results)
372 }
373
374 async fn extract_training_features(&self, sample: &TrainingSample) -> Result<Vec<Vec<f32>>> {
375 let mut all_features = Vec::new();
376
377 for result in &sample.results {
378 let mut features = Vec::new();
379
380 for extractor in &self.feature_extractors {
381 let result_features = extractor.extract_features(&sample.query, result)?;
382 features.extend(result_features);
383 }
384
385 all_features.push(features);
386 }
387
388 Ok(all_features)
389 }
390
391 fn train_linear_model(&self, features: &[Vec<f32>], targets: &[f32]) -> Result<Vec<f32>> {
392 if features.is_empty() || features[0].is_empty() {
393 anyhow::bail!("No features provided for training");
394 }
395
396 let feature_dim = features[0].len();
397 let mut weights = vec![0.0; feature_dim];
398
399 let learning_rate = self.config.learning_rate;
401 let l2_reg = self.config.l2_regularization;
402 let epochs = 100;
403
404 for _epoch in 0..epochs {
405 let mut gradients = vec![0.0; feature_dim];
406 let mut total_loss = 0.0;
407
408 for (feature_vec, target) in features.iter().zip(targets.iter()) {
409 let prediction: f32 = feature_vec.iter()
411 .zip(weights.iter())
412 .map(|(f, w)| f * w)
413 .sum();
414
415 let error = prediction - target;
416 total_loss += error * error;
417
418 for (i, feature) in feature_vec.iter().enumerate() {
420 gradients[i] += error * feature;
421 }
422 }
423
424 for (i, weight) in weights.iter_mut().enumerate() {
426 *weight -= learning_rate * (gradients[i] / features.len() as f32 + l2_reg * *weight);
427 }
428 }
429
430 Ok(weights)
431 }
432
433 async fn train_isotonic_regressors(&self, training_samples: &[TrainingSample]) -> Result<()> {
434 let mut score_relevance_pairs = Vec::new();
436
437 for sample in training_samples {
438 let reranked = self.rerank(&sample.query, sample.results.clone()).await?;
439
440 for (result, relevance) in reranked.iter().zip(sample.relevance_scores.iter()) {
441 score_relevance_pairs.push((result.rerank_score, *relevance));
442 }
443 }
444
445 score_relevance_pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
447
448 let regressor = IsotonicRegressor::fit(&score_relevance_pairs)?;
450
451 let mut models = self.isotonic_models.write().await;
452 models.insert("global".to_string(), regressor);
453
454 Ok(())
455 }
456
457 async fn update_training_metrics(&self, _training_samples: &[TrainingSample]) -> Result<()> {
458 let mut metrics = self.metrics.write().await;
460 metrics.samples_processed += _training_samples.len();
461
462 Ok(())
463 }
464
465 fn calculate_ndcg(&self, results: &[SearchResult], relevances: &[f32]) -> f32 {
466 if results.is_empty() || relevances.is_empty() {
467 return 0.0;
468 }
469
470 let mut dcg = 0.0;
472 for (i, rel) in relevances.iter().take(results.len()).enumerate() {
473 let discount = (i as f32 + 2.0).log2();
474 dcg += (2.0_f32.powf(*rel) - 1.0) / discount;
475 }
476
477 let mut sorted_relevances = relevances.to_vec();
479 sorted_relevances.sort_by(|a, b| b.partial_cmp(a).unwrap());
480
481 let mut idcg = 0.0;
482 for (i, rel) in sorted_relevances.iter().take(results.len()).enumerate() {
483 let discount = (i as f32 + 2.0).log2();
484 idcg += (2.0_f32.powf(*rel) - 1.0) / discount;
485 }
486
487 if idcg > 0.0 { dcg / idcg } else { 0.0 }
488 }
489
490 fn calculate_ndcg_reranked(&self, results: &[RerankedResult], relevances: &[f32]) -> f32 {
491 if results.is_empty() || relevances.is_empty() {
492 return 0.0;
493 }
494
495 let mut id_to_relevance = HashMap::new();
497 for (i, result) in results.iter().enumerate() {
498 if i < relevances.len() {
499 id_to_relevance.insert(result.result.id.clone(), relevances[i]);
500 }
501 }
502
503 let mut dcg = 0.0;
505 for (i, result) in results.iter().enumerate() {
506 if let Some(rel) = id_to_relevance.get(&result.result.id) {
507 let discount = (i as f32 + 2.0).log2();
508 dcg += (2.0_f32.powf(*rel) - 1.0) / discount;
509 }
510 }
511
512 let mut sorted_relevances = relevances.to_vec();
514 sorted_relevances.sort_by(|a, b| b.partial_cmp(a).unwrap());
515
516 let mut idcg = 0.0;
517 for (i, rel) in sorted_relevances.iter().take(results.len()).enumerate() {
518 let discount = (i as f32 + 2.0).log2();
519 idcg += (2.0_f32.powf(*rel) - 1.0) / discount;
520 }
521
522 if idcg > 0.0 { dcg / idcg } else { 0.0 }
523 }
524
525 fn calculate_precision_at_k(&self, _results: &[SearchResult], relevances: &[f32], k: usize) -> f32 {
526 let relevant_count = relevances.iter()
527 .take(k)
528 .filter(|&&r| r > 0.5) .count();
530
531 relevant_count as f32 / k.min(relevances.len()) as f32
532 }
533
534 fn calculate_precision_at_k_reranked(&self, _results: &[RerankedResult], relevances: &[f32], k: usize) -> f32 {
535 self.calculate_precision_at_k(&[], relevances, k)
538 }
539
540 async fn calculate_calibration_error(&self, _test_samples: &[TrainingSample]) -> Result<f32> {
541 Ok(0.05)
544 }
545
546 async fn get_feature_importances(&self) -> HashMap<String, f32> {
547 let mut importances = HashMap::new();
548
549 let mut feature_names = Vec::new();
551 for extractor in &self.feature_extractors {
552 feature_names.extend(extractor.feature_names());
553 }
554
555 if let Some(weights) = self.model_weights.read().await.as_ref() {
557 for (name, weight) in feature_names.iter().zip(weights.iter()) {
558 importances.insert(name.clone(), weight.abs());
559 }
560 }
561
562 importances
563 }
564}
565
566struct LexicalFeatureExtractor;
569struct SemanticFeatureExtractor;
570struct StructuralFeatureExtractor;
571struct LSPFeatureExtractor;
572
573impl LexicalFeatureExtractor {
574 fn new() -> Self { Self }
575}
576
577impl FeatureExtractor for LexicalFeatureExtractor {
578 fn extract_features(&self, query: &str, result: &SearchResult) -> Result<Vec<f32>> {
579 let mut features = Vec::new();
580
581 features.push(result.lexical_score);
583
584 let query_terms: Vec<&str> = query.split_whitespace().collect();
586 let content_lower = result.content.to_lowercase();
587 let query_matches = query_terms.iter()
588 .filter(|term| content_lower.contains(&term.to_lowercase()))
589 .count() as f32 / query_terms.len() as f32;
590 features.push(query_matches);
591
592 features.push((result.content.len() as f32).log10());
594
595 Ok(features)
596 }
597
598 fn feature_names(&self) -> Vec<String> {
599 vec![
600 "lexical_score".to_string(),
601 "query_match_ratio".to_string(),
602 "log_content_length".to_string(),
603 ]
604 }
605}
606
607impl SemanticFeatureExtractor {
608 fn new() -> Self { Self }
609}
610
611impl FeatureExtractor for SemanticFeatureExtractor {
612 fn extract_features(&self, _query: &str, result: &SearchResult) -> Result<Vec<f32>> {
613 let mut features = Vec::new();
614
615 features.push(result.semantic_score.unwrap_or(0.0));
617
618 features.push(0.5); features.push(0.3); Ok(features)
623 }
624
625 fn feature_names(&self) -> Vec<String> {
626 vec![
627 "semantic_score".to_string(),
628 "semantic_similarity".to_string(),
629 "context_relevance".to_string(),
630 ]
631 }
632}
633
634impl StructuralFeatureExtractor {
635 fn new() -> Self { Self }
636}
637
638impl FeatureExtractor for StructuralFeatureExtractor {
639 fn extract_features(&self, _query: &str, result: &SearchResult) -> Result<Vec<f32>> {
640 let mut features = Vec::new();
641
642 let is_source_file = result.file_path.ends_with(".rs") ||
644 result.file_path.ends_with(".py") ||
645 result.file_path.ends_with(".ts");
646 features.push(if is_source_file { 1.0 } else { 0.0 });
647
648 features.push(0.4); features.push(0.6); Ok(features)
653 }
654
655 fn feature_names(&self) -> Vec<String> {
656 vec![
657 "is_source_file".to_string(),
658 "function_density".to_string(),
659 "comment_ratio".to_string(),
660 ]
661 }
662}
663
664impl LSPFeatureExtractor {
665 fn new() -> Self { Self }
666}
667
668impl FeatureExtractor for LSPFeatureExtractor {
669 fn extract_features(&self, _query: &str, result: &SearchResult) -> Result<Vec<f32>> {
670 let mut features = Vec::new();
671
672 features.push(result.lsp_score.unwrap_or(0.0));
674
675 features.push(0.7); features.push(0.2); Ok(features)
680 }
681
682 fn feature_names(&self) -> Vec<String> {
683 vec![
684 "lsp_score".to_string(),
685 "symbol_match_strength".to_string(),
686 "reference_density".to_string(),
687 ]
688 }
689}
690
691struct FeatureResult {
694 result: SearchResult,
695 features: Vec<f32>,
696}
697
698impl IsotonicRegressor {
699 fn fit(data: &[(f32, f32)]) -> Result<Self> {
701 if data.is_empty() {
702 anyhow::bail!("Cannot fit isotonic regressor on empty data");
703 }
704
705 let mut x_values = Vec::new();
708 let mut y_values = Vec::new();
709
710 let bin_size = (data.len() / 10).max(1);
712 for chunk in data.chunks(bin_size) {
713 let avg_x = chunk.iter().map(|(x, _)| *x).sum::<f32>() / chunk.len() as f32;
714 let avg_y = chunk.iter().map(|(_, y)| *y).sum::<f32>() / chunk.len() as f32;
715
716 x_values.push(avg_x);
717 y_values.push(avg_y);
718 }
719
720 Ok(Self {
721 x_values,
722 y_values,
723 sample_count: data.len(),
724 })
725 }
726
727 fn predict(&self, x: f32) -> f32 {
729 if self.x_values.is_empty() {
730 return x; }
732
733 for i in 0..self.x_values.len() - 1 {
735 if x >= self.x_values[i] && x <= self.x_values[i + 1] {
736 let t = (x - self.x_values[i]) / (self.x_values[i + 1] - self.x_values[i]);
737 return self.y_values[i] + t * (self.y_values[i + 1] - self.y_values[i]);
738 }
739 }
740
741 if x < self.x_values[0] {
743 self.y_values[0]
744 } else {
745 self.y_values[self.y_values.len() - 1]
746 }
747 }
748}
749
750impl Default for RerankMetrics {
751 fn default() -> Self {
752 Self {
753 samples_processed: 0,
754 avg_ndcg_improvement: 0.0,
755 avg_precision_improvement: 0.0,
756 avg_recall_improvement: 0.0,
757 calibration_error: 0.0,
758 feature_importances: HashMap::new(),
759 }
760 }
761}
762
763pub async fn initialize_reranker(config: &RerankConfig) -> Result<()> {
765 info!("Initializing learned reranker");
766 info!("Config: top-{}, isotonic={}, learning_rate={}",
767 config.top_k, config.use_isotonic, config.learning_rate);
768
769 if config.top_k == 0 {
771 anyhow::bail!("Top-K cannot be zero");
772 }
773
774 if config.learning_rate <= 0.0 || config.learning_rate > 1.0 {
775 anyhow::bail!("Invalid learning rate: {}", config.learning_rate);
776 }
777
778 info!("Learned reranker initialized");
779 Ok(())
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785
786 #[tokio::test]
787 async fn test_reranker_creation() {
788 let config = RerankConfig {
789 top_k: 100,
790 use_isotonic: true,
791 learning_rate: 0.01,
792 l2_regularization: 0.001,
793 min_training_samples: 10,
794 combination_strategy: CombinationStrategy::Linear,
795 };
796
797 let reranker = LearnedReranker::new(config).await.unwrap();
798 let metrics = reranker.get_metrics().await;
799 assert_eq!(metrics.samples_processed, 0);
800 }
801
802 #[test]
803 fn test_isotonic_regressor() {
804 let data = vec![
805 (0.1, 0.2),
806 (0.3, 0.4),
807 (0.5, 0.6),
808 (0.7, 0.8),
809 ];
810
811 let regressor = IsotonicRegressor::fit(&data).unwrap();
812
813 let pred = regressor.predict(0.4);
815 assert!(pred > 0.4 && pred < 0.6); let pred_low = regressor.predict(0.0);
819 let pred_high = regressor.predict(1.0);
820 assert!(pred_low >= 0.0);
821 assert!(pred_high >= 0.0);
822 }
823
824 #[test]
825 fn test_feature_extractors() {
826 let extractor = LexicalFeatureExtractor::new();
827 let result = SearchResult {
828 id: "test".to_string(),
829 content: "def hello_world(): return 'hello'".to_string(),
830 file_path: "test.py".to_string(),
831 initial_score: 0.8,
832 lexical_score: 0.9,
833 semantic_score: Some(0.7),
834 lsp_score: Some(0.6),
835 metadata: HashMap::new(),
836 };
837
838 let features = extractor.extract_features("hello", &result).unwrap();
839 assert_eq!(features.len(), 3); let names = extractor.feature_names();
842 assert_eq!(names.len(), 3);
843 }
844
845 #[test]
846 fn test_ndcg_calculation() {
847 let reranker = LearnedReranker {
848 config: RerankConfig {
849 top_k: 10,
850 use_isotonic: false,
851 learning_rate: 0.01,
852 l2_regularization: 0.001,
853 min_training_samples: 1,
854 combination_strategy: CombinationStrategy::Linear,
855 },
856 feature_extractors: Vec::new(),
857 isotonic_models: Arc::new(RwLock::new(HashMap::new())),
858 model_weights: Arc::new(RwLock::new(None)),
859 metrics: Arc::new(RwLock::new(RerankMetrics::default())),
860 };
861
862 let results = vec![]; let relevances = vec![1.0, 0.8, 0.6, 0.4, 0.2];
864
865 let ndcg = reranker.calculate_ndcg(&results, &relevances);
866 assert!(ndcg >= 0.0 && ndcg <= 1.0);
867 }
868}