1use lc_vector_stores::{Document, SearchResult};
7use std::collections::HashMap;
8
9#[derive(Debug)]
11#[non_exhaustive]
12pub enum RerankingError {
13 ScoringError(String),
15 InvalidInput(String),
17}
18
19impl std::fmt::Display for RerankingError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 RerankingError::ScoringError(msg) => write!(f, "scoring error: {}", msg),
23 RerankingError::InvalidInput(msg) => write!(f, "invalid input: {}", msg),
24 }
25 }
26}
27
28impl std::error::Error for RerankingError {}
29
30pub struct RerankingConfig {
32 pub top_n: usize,
34
35 pub min_score: Option<f32>,
37
38 pub preserve_original_score: bool,
40}
41
42impl Default for RerankingConfig {
43 fn default() -> Self {
44 Self {
45 top_n: 5,
46 min_score: None,
47 preserve_original_score: true,
48 }
49 }
50}
51
52impl RerankingConfig {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn with_top_n(mut self, n: usize) -> Self {
60 self.top_n = n;
61 self
62 }
63
64 pub fn with_min_score(mut self, score: f32) -> Self {
66 self.min_score = Some(score);
67 self
68 }
69
70 pub fn with_preserve_original_score(mut self, preserve: bool) -> Self {
72 self.preserve_original_score = preserve;
73 self
74 }
75}
76
77pub trait Reranker: Send + Sync {
79 fn score(&self, query: &str, documents: &[Document]) -> Result<Vec<f32>, RerankingError>;
81}
82
83pub struct KeywordReranker {
85 keyword_weights: HashMap<String, f32>,
87}
88
89impl KeywordReranker {
90 pub fn new() -> Self {
92 Self {
93 keyword_weights: HashMap::new(),
94 }
95 }
96
97 pub fn with_keyword_weights(mut self, weights: HashMap<String, f32>) -> Self {
99 self.keyword_weights = weights;
100 self
101 }
102
103 fn extract_keywords(&self, query: &str) -> Vec<String> {
104 query
105 .split_whitespace()
106 .filter(|w| w.len() > 1)
107 .map(|w| w.to_lowercase())
108 .collect()
109 }
110
111 fn count_keyword_matches(&self, keywords: &[String], document: &Document) -> f32 {
112 let doc_lower = document.content.to_lowercase();
113 let mut score = 0.0;
114
115 for keyword in keywords {
116 let count = doc_lower.matches(keyword).count() as f32;
117 let weight = self.keyword_weights.get(keyword).unwrap_or(&1.0);
118 score += count * weight;
119 }
120
121 score
122 }
123}
124
125impl Default for KeywordReranker {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131impl Reranker for KeywordReranker {
132 fn score(&self, query: &str, documents: &[Document]) -> Result<Vec<f32>, RerankingError> {
133 if documents.is_empty() {
134 return Ok(Vec::new());
135 }
136
137 let keywords = self.extract_keywords(query);
138
139 if keywords.is_empty() {
140 return Ok(documents.iter().map(|_| 0.0).collect());
141 }
142
143 let scores: Vec<f32> = documents
144 .iter()
145 .map(|doc| self.count_keyword_matches(&keywords, doc))
146 .collect();
147
148 Ok(scores)
149 }
150}
151
152pub struct RerankingExecutor {
154 reranker: Box<dyn Reranker>,
155 config: RerankingConfig,
156}
157
158impl RerankingExecutor {
159 pub fn new(reranker: Box<dyn Reranker>) -> Self {
161 Self {
162 reranker,
163 config: RerankingConfig::default(),
164 }
165 }
166
167 pub fn with_config(mut self, config: RerankingConfig) -> Self {
169 self.config = config;
170 self
171 }
172
173 pub fn with_top_n(mut self, n: usize) -> Self {
175 self.config.top_n = n;
176 self
177 }
178
179 pub fn with_min_score(mut self, score: f32) -> Self {
181 self.config.min_score = Some(score);
182 self
183 }
184
185 pub fn with_preserve_original_score(mut self, preserve: bool) -> Self {
187 self.config.preserve_original_score = preserve;
188 self
189 }
190
191 pub fn rerank(
193 &self,
194 query: &str,
195 results: Vec<SearchResult>,
196 ) -> Result<Vec<SearchResult>, RerankingError> {
197 if results.is_empty() {
198 return Ok(Vec::new());
199 }
200
201 let documents: Vec<Document> = results.iter().map(|r| r.document.clone()).collect();
202 let scores = self.reranker.score(query, &documents)?;
203
204 let max_original = results
206 .iter()
207 .map(|r| r.score.abs())
208 .fold(0.0_f32, f32::max);
209 let max_rerank = scores.iter().map(|s| s.abs()).fold(0.0_f32, f32::max);
210
211 let mut reranked: Vec<SearchResult> = results
212 .iter()
213 .enumerate()
214 .map(|(idx, r)| {
215 let new_score = if self.config.preserve_original_score {
216 let norm_original = if max_original > 0.0 {
217 r.score / max_original
218 } else {
219 0.0
220 };
221 let norm_rerank = if max_rerank > 0.0 {
222 scores[idx] / max_rerank
223 } else {
224 0.0
225 };
226 norm_original + norm_rerank
227 } else {
228 scores[idx]
229 };
230
231 SearchResult {
232 document: r.document.clone(),
233 score: new_score,
234 }
235 })
236 .collect();
237
238 if let Some(min_score) = self.config.min_score {
239 reranked.retain(|r| r.score >= min_score);
240 }
241
242 reranked.sort_by(|a, b| {
243 b.score
244 .partial_cmp(&a.score)
245 .unwrap_or(std::cmp::Ordering::Equal)
246 });
247
248 reranked.truncate(self.config.top_n);
249
250 Ok(reranked)
251 }
252
253 pub fn rerank_documents(
255 &self,
256 query: &str,
257 documents: Vec<Document>,
258 ) -> Result<Vec<SearchResult>, RerankingError> {
259 if documents.is_empty() {
260 return Ok(Vec::new());
261 }
262
263 let scores = self.reranker.score(query, &documents)?;
264
265 let mut results: Vec<SearchResult> = documents
266 .iter()
267 .enumerate()
268 .map(|(idx, doc)| SearchResult {
269 document: doc.clone(),
270 score: scores[idx],
271 })
272 .collect();
273
274 if let Some(min_score) = self.config.min_score {
275 results.retain(|r| r.score >= min_score);
276 }
277
278 results.sort_by(|a, b| {
279 b.score
280 .partial_cmp(&a.score)
281 .unwrap_or(std::cmp::Ordering::Equal)
282 });
283
284 results.truncate(self.config.top_n);
285
286 Ok(results)
287 }
288}
289
290pub struct BM25Reranker {
292 k1: f32,
293 b: f32,
294}
295
296impl BM25Reranker {
297 pub fn new() -> Self {
299 Self { k1: 1.5, b: 0.75 }
300 }
301
302 pub fn with_params(mut self, k1: f32, b: f32) -> Self {
304 self.k1 = k1;
305 self.b = b;
306 self
307 }
308
309 fn tokenize(&self, text: &str) -> Vec<String> {
310 text.split_whitespace()
311 .filter(|w| w.len() > 1)
312 .map(|w| w.to_lowercase())
313 .collect()
314 }
315}
316
317impl Default for BM25Reranker {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323impl Reranker for BM25Reranker {
324 fn score(&self, query: &str, documents: &[Document]) -> Result<Vec<f32>, RerankingError> {
325 if documents.is_empty() {
326 return Ok(Vec::new());
327 }
328
329 let query_terms = self.tokenize(query);
330
331 if query_terms.is_empty() {
332 return Ok(documents.iter().map(|_| 0.0).collect());
333 }
334
335 let avgdl = documents
336 .iter()
337 .map(|d| d.content.split_whitespace().count() as f32)
338 .sum::<f32>()
339 / documents.len() as f32;
340
341 let scores: Vec<f32> = documents
342 .iter()
343 .map(|doc| {
344 let doc_len = doc.content.split_whitespace().count() as f32;
345 let doc_lower = doc.content.to_lowercase();
346 query_terms
347 .iter()
348 .map(|term| {
349 let freq = doc_lower.matches(term.as_str()).count() as f32;
350 let tf =
351 freq / (freq + self.k1 * (1.0 - self.b + self.b * doc_len / avgdl));
352 tf * (1.0 + self.k1)
353 / (tf + self.k1 * (1.0 - self.b + self.b * doc_len / avgdl))
354 })
355 .sum()
356 })
357 .collect();
358
359 Ok(scores)
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_reranking_config_default() {
369 let config = RerankingConfig::default();
370
371 assert_eq!(config.top_n, 5);
372 assert!(config.min_score.is_none());
373 assert!(config.preserve_original_score);
374 }
375
376 #[test]
377 fn test_reranking_config_custom() {
378 let config = RerankingConfig::new()
379 .with_top_n(10)
380 .with_min_score(0.5)
381 .with_preserve_original_score(false);
382
383 assert_eq!(config.top_n, 10);
384 assert_eq!(config.min_score, Some(0.5));
385 assert!(!config.preserve_original_score);
386 }
387
388 #[test]
389 fn test_keyword_reranker_basic() {
390 let reranker = KeywordReranker::new();
391
392 let query = "Rust programming";
393 let documents = vec![
394 Document::new("Rust is a programming language"),
395 Document::new("Python is also a programming language"),
396 Document::new("JavaScript for web"),
397 ];
398
399 let scores = reranker.score(query, &documents).unwrap();
400
401 assert_eq!(scores.len(), 3);
402 assert!(scores[0] > 0.0);
403 assert!(scores[1] > 0.0);
404 }
405
406 #[test]
407 fn test_keyword_reranker_empty_query() {
408 let reranker = KeywordReranker::new();
409
410 let documents = vec![Document::new("Some content")];
411
412 let scores = reranker.score("", &documents).unwrap();
413
414 assert_eq!(scores[0], 0.0);
415 }
416
417 #[test]
418 fn test_reranking_executor_basic() {
419 let reranker = Box::new(KeywordReranker::new());
420 let executor = RerankingExecutor::new(reranker).with_top_n(2);
421
422 let results = vec![
423 SearchResult {
424 document: Document::new("Rust programming language"),
425 score: 0.5,
426 },
427 SearchResult {
428 document: Document::new("Python scripting"),
429 score: 0.4,
430 },
431 SearchResult {
432 document: Document::new("JavaScript web"),
433 score: 0.3,
434 },
435 ];
436
437 let reranked = executor.rerank("Rust programming", results).unwrap();
438
439 assert_eq!(reranked.len(), 2);
440 }
441
442 #[test]
443 fn test_reranking_executor_min_score() {
444 let reranker = Box::new(KeywordReranker::new());
445 let executor = RerankingExecutor::new(reranker)
446 .with_top_n(5)
447 .with_min_score(1.0);
448
449 let results = vec![
450 SearchResult {
451 document: Document::new("Rust Rust Rust"),
452 score: 0.0,
453 },
454 SearchResult {
455 document: Document::new("No match"),
456 score: 0.0,
457 },
458 ];
459
460 let reranked = executor.rerank("Rust", results).unwrap();
461
462 assert!(reranked.len() <= 1);
463 }
464
465 #[test]
466 fn test_bm25_reranker_basic() {
467 let reranker = BM25Reranker::new();
468
469 let query = "programming language";
470 let documents = vec![
471 Document::new("Rust is a programming language"),
472 Document::new("Python is a programming language too"),
473 Document::new("Web development"),
474 ];
475
476 let scores = reranker.score(query, &documents).unwrap();
477
478 assert_eq!(scores.len(), 3);
479 assert!(scores[0] > scores[2]);
480 }
481
482 #[test]
483 fn test_bm25_reranker_params() {
484 let reranker = BM25Reranker::new().with_params(2.0, 0.5);
485
486 let documents = vec![Document::new("test content")];
487
488 let scores = reranker.score("test", &documents).unwrap();
489
490 assert!(scores[0] > 0.0);
491 }
492
493 #[test]
494 fn test_rerank_documents() {
495 let reranker = Box::new(KeywordReranker::new());
496 let executor = RerankingExecutor::new(reranker).with_top_n(2);
497
498 let documents = vec![
499 Document::new("Rust programming"),
500 Document::new("Python scripting"),
501 Document::new("JavaScript web"),
502 ];
503
504 let results = executor.rerank_documents("Rust", documents).unwrap();
505
506 assert_eq!(results.len(), 2);
507 }
508}