llm_kernel/search/mod.rs
1//! Hybrid search: BM25 + vector similarity with Reciprocal Rank Fusion.
2//!
3//! Provides a unified search interface that combines keyword (BM25) and
4//! semantic (vector) search results using RRF for optimal relevance.
5//!
6//! ```
7//! use llm_kernel::search::{SearchResult, rrf_fuse};
8//!
9//! let bm25 = vec![
10//! SearchResult { id: "doc1".into(), score: 0.9, text: "Rust programming".into() },
11//! SearchResult { id: "doc2".into(), score: 0.7, text: "Python basics".into() },
12//! ];
13//! let vector = vec![
14//! SearchResult { id: "doc2".into(), score: 0.95, text: "Python basics".into() },
15//! SearchResult { id: "doc3".into(), score: 0.6, text: "Go concurrency".into() },
16//! ];
17//!
18//! let fused = rrf_fuse(&[bm25, vector], 60);
19//! assert!(!fused.is_empty());
20//! ```
21
22pub mod federation;
23pub mod fusion;
24pub mod provider;
25pub mod rrf;
26pub mod types;
27
28pub use federation::{FusionStrategy, federate_results};
29pub use fusion::{combmnz_fuse, normalize_minmax, weighted_sum_fuse};
30pub use provider::{KeywordIndex, SearchProvider};
31pub use rrf::rrf_fuse;
32pub use types::SearchResult;
33
34#[cfg(feature = "federation")]
35pub use federation::FederatedSearch;
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 /// Two distinct providers compose via the existing RRF fusion with the
42 /// expected merged top result (AC1).
43 #[test]
44 fn two_providers_compose_via_rrf() {
45 let keywords = KeywordIndex::new(vec![
46 ("shared".to_string(), "rust async runtime tokio".to_string()),
47 (
48 "kw_only".to_string(),
49 "rust ownership borrowing".to_string(),
50 ),
51 ]);
52 let semantic = KeywordIndex::new(vec![
53 ("shared".to_string(), "rust async runtime tokio".to_string()),
54 ("sem_only".to_string(), "rust green threads mio".to_string()),
55 ]);
56
57 let kw_results = keywords.search("rust async", 10).unwrap();
58 let sem_results = semantic.search("rust async", 10).unwrap();
59
60 let fused = rrf_fuse(&[kw_results, sem_results], 60);
61 assert!(!fused.is_empty());
62 // "shared" appears at rank 0 in both providers -> highest RRF score.
63 assert_eq!(fused[0].id, "shared");
64 }
65}