Skip to main content

khive_fusion/
strategy.rs

1//! Fusion strategy types.
2
3use serde::{Deserialize, Serialize};
4
5/// Default RRF constant k=60, standard in literature (Craswell et al., 2009).
6pub const DEFAULT_RRF_K: usize = 60;
7
8/// Fusion strategy for combining ranked result lists.
9///
10/// See module-level docs for algorithm details.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum FusionStrategy {
14    /// Reciprocal Rank Fusion (default, recommended).
15    ///
16    /// Uses only ranks, making it robust to different score distributions.
17    /// Formula: score(d) = Σ 1/(k + rank_i(d))
18    #[serde(alias = "Rrf")]
19    Rrf {
20        /// Smoothing constant. Higher values reduce impact of rank differences.
21        /// Default: 60 (standard in literature).
22        k: usize,
23    },
24
25    /// Weighted linear combination of scores.
26    ///
27    /// Requires score normalization for different score scales (e.g., vector
28    /// similarity 0-1 vs BM25 0-∞).
29    ///
30    /// Weights are normalized to sum to 1.0 internally.
31    #[serde(alias = "Weighted")]
32    Weighted {
33        /// Weights for each source (will be normalized).
34        weights: Vec<f64>,
35    },
36
37    /// Take union with max score per ID.
38    ///
39    /// Useful when you want the best score from any source.
40    #[serde(alias = "Union")]
41    Union,
42
43    /// Skip BM25 entirely — return only vector (HNSW) results.
44    ///
45    /// Use when keyword search degrades quality (short queries, code search).
46    /// The result list is the raw HNSW output with no fusion step.
47    #[serde(alias = "VectorOnly")]
48    VectorOnly,
49
50    /// Skip HNSW entirely — return only BM25 keyword results.
51    ///
52    /// Use for exact-match retrieval (medication names, identifiers, slugs).
53    /// The result list is the raw BM25 output with no fusion step.
54    #[serde(alias = "KeywordOnly")]
55    KeywordOnly,
56}
57
58impl Default for FusionStrategy {
59    fn default() -> Self {
60        Self::Rrf { k: DEFAULT_RRF_K }
61    }
62}
63
64impl FusionStrategy {
65    /// Create an RRF strategy with default k=60.
66    #[inline]
67    pub fn rrf() -> Self {
68        Self::Rrf { k: DEFAULT_RRF_K }
69    }
70
71    /// Create an RRF strategy with custom k value.
72    #[inline]
73    pub fn rrf_with_k(k: usize) -> Self {
74        Self::Rrf { k: k.max(1) } // Ensure k >= 1
75    }
76
77    /// Create a weighted strategy with given weights.
78    #[inline]
79    pub fn weighted(weights: Vec<f64>) -> Self {
80        Self::Weighted { weights }
81    }
82
83    /// Create a union strategy.
84    #[inline]
85    pub fn union() -> Self {
86        Self::Union
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_fusion_strategy_default() {
96        let default = FusionStrategy::default();
97        assert_eq!(default, FusionStrategy::Rrf { k: 60 });
98    }
99
100    #[test]
101    fn test_fusion_strategy_builders() {
102        assert_eq!(FusionStrategy::rrf(), FusionStrategy::Rrf { k: 60 });
103        assert_eq!(
104            FusionStrategy::rrf_with_k(20),
105            FusionStrategy::Rrf { k: 20 }
106        );
107        assert_eq!(FusionStrategy::rrf_with_k(0), FusionStrategy::Rrf { k: 1 }); // min enforced
108        assert_eq!(
109            FusionStrategy::weighted(vec![0.5, 0.5]),
110            FusionStrategy::Weighted {
111                weights: vec![0.5, 0.5]
112            }
113        );
114        assert_eq!(FusionStrategy::union(), FusionStrategy::Union);
115    }
116}