Skip to main content

linkrs_config/
fulltext.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// Local fulltext engine selector.
5///
6/// Only the local Tantivy BM25 engine is supported. The enum is kept so
7/// stored configuration stays explicit about which engine produced an index.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
9pub enum FulltextEngineType {
10    #[default]
11    Bm25,
12}
13
14impl std::fmt::Display for FulltextEngineType {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        match self {
17            FulltextEngineType::Bm25 => write!(f, "bm25"),
18        }
19    }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum TokenizerKind {
25    Jieba,
26    Raw,
27    #[default]
28    Default,
29    Whitespace,
30}
31
32impl TokenizerKind {
33    pub fn name(&self) -> &'static str {
34        match self {
35            TokenizerKind::Jieba => "jieba",
36            TokenizerKind::Raw => "raw",
37            TokenizerKind::Default => "default",
38            TokenizerKind::Whitespace => "whitespace",
39        }
40    }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct Bm25Params {
46    pub k1: f32,
47    pub b: f32,
48}
49
50impl Default for Bm25Params {
51    fn default() -> Self {
52        Self { k1: 1.2, b: 0.75 }
53    }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct TantivyConfig {
59    pub writer_memory_budget: usize,
60    #[serde(default)]
61    pub tokenizer: TokenizerKind,
62    #[serde(default = "default_doc_store_cache_num_blocks")]
63    pub doc_store_cache_num_blocks: usize,
64    #[serde(default)]
65    pub bm25_params: Bm25Params,
66}
67
68fn default_doc_store_cache_num_blocks() -> usize {
69    100
70}
71
72impl Default for TantivyConfig {
73    fn default() -> Self {
74        Self {
75            writer_memory_budget: 50_000_000,
76            tokenizer: TokenizerKind::default(),
77            doc_store_cache_num_blocks: 100,
78            bm25_params: Bm25Params::default(),
79        }
80    }
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
84#[serde(rename_all = "snake_case")]
85pub enum SyncFailurePolicy {
86    #[default]
87    FailOpen,
88    FailClosed,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct SyncConfig {
94    #[serde(default = "default_queue_size")]
95    pub queue_size: usize,
96    #[serde(default = "default_commit_interval_ms")]
97    pub commit_interval_ms: u64,
98    #[serde(default = "default_batch_size")]
99    pub batch_size: usize,
100    #[serde(default)]
101    pub failure_policy: SyncFailurePolicy,
102}
103
104impl Default for SyncConfig {
105    fn default() -> Self {
106        Self {
107            queue_size: default_queue_size(),
108            commit_interval_ms: default_commit_interval_ms(),
109            batch_size: default_batch_size(),
110            failure_policy: SyncFailurePolicy::default(),
111        }
112    }
113}
114
115fn default_queue_size() -> usize {
116    10000
117}
118
119fn default_commit_interval_ms() -> u64 {
120    1000
121}
122
123fn default_batch_size() -> usize {
124    100
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(deny_unknown_fields)]
129pub struct FulltextConfig {
130    pub enabled: bool,
131    /// Engine that produced the indexes. Only the local BM25 engine exists;
132    /// the field documents index provenance and must stay `Bm25`.
133    pub default_engine: FulltextEngineType,
134    pub index_path: PathBuf,
135    pub sync: SyncConfig,
136    pub tantivy: TantivyConfig,
137    pub cache_size: usize,
138    pub max_result_cache: usize,
139    pub result_cache_ttl_secs: u64,
140}
141
142impl Default for FulltextConfig {
143    fn default() -> Self {
144        Self {
145            enabled: false,
146            default_engine: FulltextEngineType::default(),
147            index_path: PathBuf::from("data/fulltext"),
148            sync: SyncConfig::default(),
149            tantivy: TantivyConfig::default(),
150            cache_size: 100,
151            max_result_cache: 1000,
152            result_cache_ttl_secs: 60,
153        }
154    }
155}
156
157impl FulltextConfig {
158    /// Validate the configuration.
159    pub fn validate(&self) -> Result<(), String> {
160        if self.cache_size == 0 {
161            return Err("fulltext cache_size must be greater than 0".to_string());
162        }
163        if self.max_result_cache == 0 {
164            return Err("fulltext max_result_cache must be greater than 0".to_string());
165        }
166        if self.result_cache_ttl_secs == 0 {
167            return Err("fulltext result_cache_ttl_secs must be greater than 0".to_string());
168        }
169        if self.tantivy.writer_memory_budget == 0 {
170            return Err("fulltext writer_memory_budget must be greater than 0".to_string());
171        }
172        if self.tantivy.doc_store_cache_num_blocks == 0 {
173            return Err("fulltext doc_store_cache_num_blocks must be greater than 0".to_string());
174        }
175        if !self.tantivy.bm25_params.k1.is_finite() || self.tantivy.bm25_params.k1 < 0.0 {
176            return Err("fulltext bm25_params.k1 must be a non-negative number".to_string());
177        }
178        if !self.tantivy.bm25_params.b.is_finite()
179            || !(0.0..=1.0).contains(&self.tantivy.bm25_params.b)
180        {
181            return Err("fulltext bm25_params.b must be within 0.0..=1.0".to_string());
182        }
183        Ok(())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_fulltext_config_validate() {
193        assert!(FulltextConfig::default().validate().is_ok());
194
195        let zero_cache = FulltextConfig {
196            cache_size: 0,
197            ..Default::default()
198        };
199        assert!(zero_cache.validate().is_err());
200
201        let bm25 = FulltextConfig {
202            tantivy: TantivyConfig {
203                bm25_params: Bm25Params { k1: 1.2, b: 1.5 },
204                ..Default::default()
205            },
206            ..Default::default()
207        };
208        assert!(bm25.validate().is_err());
209    }
210}