Skip to main content

lc_rag/
hybrid.rs

1// src/retrieval/hybrid.rs
2//! 混合检索模块
3//!
4//! 结合 BM25 关键词检索 + 向量语义检索
5
6use lc_vector_stores::Document;
7use std::collections::HashMap;
8
9pub const RRF_K: usize = 60;
10
11/// Generate a stable document ID from content hash to avoid collisions (H46).
12fn 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/// 检索结果(带分数)
20#[derive(Debug, Clone)]
21pub struct RetrievedDocument {
22    pub document: Document,
23    pub score: f64,
24    pub source: RetrievalSource,
25}
26
27/// 检索来源
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum RetrievalSource {
30    BM25,
31    Vector,
32    Hybrid,
33}
34
35/// RRF 融合算法
36///
37/// 公式: RRF_score(d) = Σ 1/(k + rank(d))
38///
39/// 参数:
40/// - bm25_results: BM25 检索结果,按分数降序排列
41/// - vector_results: 向量检索结果,按相似度降序排列
42/// - k: RRF 参数,通常为 60
43///
44/// 返回:
45/// - 融合后的文档列表,按 RRF 分数降序排列
46pub 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    // BM25 结果处理
54    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    // 向量结果处理
67    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    // 按 RRF 分数排序
80    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
98/// 带原始分数的 RRF 融合
99///
100/// 保留 BM25 和 Vector 的原始分数信息
101pub 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    // BM25 结果处理
109    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    // 向量结果处理
123    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    // 按 RRF 分数排序
137    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/// 混合检索器
156#[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    /// 执行混合检索
186    ///
187    /// 参数:
188    /// - query: 查询文本
189    /// - bm25_results: BM25 检索结果
190    /// - vector_results: 向量检索结果
191    ///
192    /// 返回:
193    /// - 融合后的 top-k 结果
194    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    /// 执行混合检索(带原始分数)
203    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        // doc1 在两个列表都出现,分数应该最高
249        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}