1use lc_vector_stores::Document;
7use std::collections::HashMap;
8
9pub const RRF_K: usize = 60;
10
11fn doc_content_hash(doc: &Document) -> String {
13 use std::hash::{Hash, Hasher};
14 let mut hasher = std::collections::hash_map::DefaultHasher::new();
15 doc.content.hash(&mut hasher);
16 format!("{:016x}", hasher.finish())
17}
18
19#[derive(Debug, Clone)]
21pub struct RetrievedDocument {
22 pub document: Document,
23 pub score: f64,
24 pub source: RetrievalSource,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum RetrievalSource {
30 BM25,
31 Vector,
32 Hybrid,
33}
34
35pub fn reciprocal_rank_fusion(
47 bm25_results: Vec<Document>,
48 vector_results: Vec<Document>,
49 k: usize,
50) -> Vec<RetrievedDocument> {
51 let mut rrf_scores: HashMap<String, (f64, Document)> = HashMap::new();
52
53 for (rank, doc) in bm25_results.iter().enumerate() {
55 let doc_id = doc.id.clone().unwrap_or_else(|| doc_content_hash(doc));
56 let rrf_contribution = 1.0 / (k as f64 + (rank + 1) as f64);
57
58 rrf_scores
59 .entry(doc_id.clone())
60 .and_modify(|(score, _existing_doc)| {
61 *score += rrf_contribution;
62 })
63 .or_insert((rrf_contribution, doc.clone()));
64 }
65
66 for (rank, doc) in vector_results.iter().enumerate() {
68 let doc_id = doc.id.clone().unwrap_or_else(|| doc_content_hash(doc));
69 let rrf_contribution = 1.0 / (k as f64 + (rank + 1) as f64);
70
71 rrf_scores
72 .entry(doc_id.clone())
73 .and_modify(|(score, _)| {
74 *score += rrf_contribution;
75 })
76 .or_insert((rrf_contribution, doc.clone()));
77 }
78
79 let mut results: Vec<RetrievedDocument> = rrf_scores
81 .into_iter()
82 .map(|(_, (score, doc))| RetrievedDocument {
83 document: doc,
84 score,
85 source: RetrievalSource::Hybrid,
86 })
87 .collect();
88
89 results.sort_by(|a, b| {
90 b.score
91 .partial_cmp(&a.score)
92 .unwrap_or(std::cmp::Ordering::Equal)
93 });
94
95 results
96}
97
98pub fn reciprocal_rank_fusion_with_scores(
102 bm25_results: Vec<(Document, f64)>,
103 vector_results: Vec<(Document, f64)>,
104 k: usize,
105) -> Vec<RetrievedDocument> {
106 let mut rrf_scores: HashMap<String, (f64, Document, Option<f64>, Option<f64>)> = HashMap::new();
107
108 for (rank, (doc, bm25_score)) in bm25_results.iter().enumerate() {
110 let doc_id = doc.id.clone().unwrap_or_else(|| doc_content_hash(doc));
111 let rrf_contribution = 1.0 / (k as f64 + (rank + 1) as f64);
112
113 rrf_scores
114 .entry(doc_id.clone())
115 .and_modify(|(score, _, bm25, _vector)| {
116 *score += rrf_contribution;
117 *bm25 = Some(*bm25_score);
118 })
119 .or_insert((rrf_contribution, doc.clone(), Some(*bm25_score), None));
120 }
121
122 for (rank, (doc, vector_score)) in vector_results.iter().enumerate() {
124 let doc_id = doc.id.clone().unwrap_or_else(|| doc_content_hash(doc));
125 let rrf_contribution = 1.0 / (k as f64 + (rank + 1) as f64);
126
127 rrf_scores
128 .entry(doc_id.clone())
129 .and_modify(|(score, _, _bm25, vector)| {
130 *score += rrf_contribution;
131 *vector = Some(*vector_score);
132 })
133 .or_insert((rrf_contribution, doc.clone(), None, Some(*vector_score)));
134 }
135
136 let mut results: Vec<RetrievedDocument> = rrf_scores
138 .into_iter()
139 .map(|(_, (score, doc, _, _))| RetrievedDocument {
140 document: doc,
141 score,
142 source: RetrievalSource::Hybrid,
143 })
144 .collect();
145
146 results.sort_by(|a, b| {
147 b.score
148 .partial_cmp(&a.score)
149 .unwrap_or(std::cmp::Ordering::Equal)
150 });
151
152 results
153}
154
155#[allow(dead_code)]
157pub struct HybridRetriever {
158 bm25_k: usize,
159 vector_k: usize,
160 rrf_k: usize,
161}
162
163impl HybridRetriever {
164 pub fn new() -> Self {
165 Self {
166 bm25_k: 10,
167 vector_k: 10,
168 rrf_k: RRF_K,
169 }
170 }
171
172 pub fn with_top_k(bm25_k: usize, vector_k: usize) -> Self {
173 Self {
174 bm25_k,
175 vector_k,
176 rrf_k: RRF_K,
177 }
178 }
179
180 pub fn with_rrf_k(mut self, k: usize) -> Self {
181 self.rrf_k = k;
182 self
183 }
184
185 pub fn retrieve(
195 &self,
196 bm25_results: Vec<Document>,
197 vector_results: Vec<Document>,
198 ) -> Vec<RetrievedDocument> {
199 reciprocal_rank_fusion(bm25_results, vector_results, self.rrf_k)
200 }
201
202 pub fn retrieve_with_scores(
204 &self,
205 bm25_results: Vec<(Document, f64)>,
206 vector_results: Vec<(Document, f64)>,
207 ) -> Vec<RetrievedDocument> {
208 reciprocal_rank_fusion_with_scores(bm25_results, vector_results, self.rrf_k)
209 }
210}
211
212impl Default for HybridRetriever {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_rrf_basic() {
224 let bm25_docs = vec![
225 Document::new("Rust系统编程").with_id("doc1"),
226 Document::new("Python数据科学").with_id("doc2"),
227 Document::new("Go并发编程").with_id("doc3"),
228 ];
229
230 let vector_docs = vec![
231 Document::new("Rust系统编程").with_id("doc1"),
232 Document::new("JavaScript前端").with_id("doc4"),
233 Document::new("Python数据科学").with_id("doc2"),
234 ];
235
236 let results = reciprocal_rank_fusion(bm25_docs, vector_docs, 60);
237
238 println!("RRF 融合结果:");
239 for (i, r) in results.iter().enumerate() {
240 println!(
241 " [{}] doc_id={}, score={:.4}",
242 i,
243 r.document.id.clone().unwrap_or_default(),
244 r.score
245 );
246 }
247
248 let first_doc_id = results[0].document.id.clone().unwrap_or_default();
250 println!("最高分文档: {}", first_doc_id);
251 }
252
253 #[test]
254 fn test_rrf_with_scores() {
255 let bm25_docs = vec![
256 (Document::new("Rust").with_id("doc1"), 3.5),
257 (Document::new("Python").with_id("doc2"), 2.1),
258 ];
259
260 let vector_docs = vec![
261 (Document::new("Rust").with_id("doc1"), 0.92),
262 (Document::new("Go").with_id("doc3"), 0.88),
263 ];
264
265 let results = reciprocal_rank_fusion_with_scores(bm25_docs, vector_docs, 60);
266
267 println!("带分数的 RRF 融合:");
268 for r in &results {
269 println!(
270 " doc_id={}, rrf_score={:.4}",
271 r.document.id.clone().unwrap_or_default(),
272 r.score
273 );
274 }
275 }
276
277 #[test]
278 fn test_hybrid_retriever() {
279 let retriever = HybridRetriever::new();
280
281 let bm25_docs = vec![
282 Document::new("机器学习").with_id("doc1"),
283 Document::new("深度学习").with_id("doc2"),
284 ];
285
286 let vector_docs = vec![
287 Document::new("机器学习").with_id("doc1"),
288 Document::new("自然语言处理").with_id("doc3"),
289 ];
290
291 let results = retriever.retrieve(bm25_docs, vector_docs);
292
293 println!("HybridRetriever 结果数: {}", results.len());
294 for r in &results {
295 println!(
296 " id={}, score={:.4}",
297 r.document.id.clone().unwrap_or_default(),
298 r.score
299 );
300 }
301 }
302}