1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[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 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 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}