1use std::collections::HashSet;
14use std::path::PathBuf;
15use std::sync::Arc;
16
17use serde::Deserialize;
18
19use crate::util::serde::env_var;
20
21#[derive(Deserialize)]
22pub struct Config {
23 pub normalization: ConfigNormalization,
24
25 pub tokenization: ConfigTokenization,
26
27 pub stopwords: ConfigStopwords,
28
29 pub search: ConfigSearch,
30
31 pub store: ConfigStore,
32}
33
34impl Config {
35 pub fn validate(&self) {
36 if self.store.kv.database.write_buffer_size == Some(0) {
38 panic!("write_buffer for kv must not be zero");
39 }
40
41 if self.store.kv.database.flush_after >= self.store.kv.pool.inactive_after {
43 panic!("flush_after for kv must be strictly lower than inactive_after");
44 }
45
46 if self.store.kv.database.max_flushes.is_some()
48 && self.store.kv.database.max_background_jobs.is_some()
49 {
50 panic!("max_background_jobs makes max_flushes unneeded, don’t configure both");
51 }
52
53 if self.store.fst.graph.consolidate_after >= self.store.fst.pool.inactive_after {
55 panic!("consolidate_after for fst must be strictly lower than inactive_after");
56 }
57 }
58}
59
60#[derive(Deserialize, Clone, Copy)]
63pub struct ConfigNormalization {
64 #[serde(with = "crate::util::serde::none_string_as_none")]
65 pub unicode_normalization: Option<UnicodeNormalization>,
66
67 pub diacritic_folding_enabled: bool,
68
69 #[cfg(feature = "stemming")]
70 pub stemming_enabled: bool,
71}
72
73#[derive(Deserialize, Debug, Clone, Copy)]
74pub enum UnicodeNormalization {
75 Nfc,
77 Nfkc,
79}
80
81#[derive(Deserialize, Clone, Copy)]
83pub struct ConfigTokenization {
84 pub detect_special_patterns: bool,
85
86 #[serde(alias = "split_special_patterns")]
87 pub compat_split_special_patterns: bool,
88}
89
90#[derive(Deserialize, Clone, Default)]
91pub struct ConfigStopwords {
92 #[serde(deserialize_with = "to_stopwords")]
93 pub allow: HashSet<String>,
94
95 #[serde(deserialize_with = "to_stopwords")]
96 pub deny: HashSet<String>,
97}
98
99fn to_stopwords<'de, D>(deserializer: D) -> Result<HashSet<String>, D::Error>
100where
101 D: serde::de::Deserializer<'de>,
102{
103 use unicode_normalization::UnicodeNormalization as _;
104
105 let vec: Vec<Box<str>> = Deserialize::deserialize(deserializer)?;
106 let stopwords_iter = vec.into_iter().map(|s| s.nfkd().to_string());
107 Ok(HashSet::from_iter(stopwords_iter))
108}
109
110#[derive(Deserialize)]
111pub struct ConfigSearch {
112 pub query_limit_default: u16,
113
114 pub query_limit_maximum: u16,
115
116 pub query_alternates_try: usize,
117
118 pub query_minimum_term_idf_default: f32,
119
120 pub query_minimum_term_idf_minimum_object_count: u64,
121
122 pub suggest_limit_default: u16,
123
124 pub suggest_limit_maximum: u16,
125
126 pub list_limit_default: u16,
127
128 pub list_limit_maximum: u16,
129}
130
131#[derive(Deserialize)]
132pub struct ConfigStore {
133 pub kv: Arc<ConfigStoreKV>,
134
135 pub fst: Arc<ConfigStoreFST>,
136}
137
138#[derive(Deserialize)]
139pub struct ConfigStoreKV {
140 #[serde(deserialize_with = "env_var::path_buf")]
141 pub path: PathBuf,
142
143 pub retain_word_objects: usize,
144
145 pub pool: ConfigStoreKVPool,
146
147 pub database: ConfigStoreKVDatabase,
148}
149
150#[derive(Deserialize)]
151pub struct ConfigStoreKVPool {
152 pub inactive_after: u64,
153}
154
155#[derive(Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct ConfigStoreKVDatabase {
158 pub flush_after: u64,
159
160 pub write_ahead_log: bool,
161
162 #[serde(default)]
167 pub compress: Option<bool>,
168
169 #[serde(default)]
170 pub parallelism: Option<i32>,
171
172 #[serde(default)]
173 #[serde(alias = "max_files")]
174 pub max_open_files: Option<i32>,
175
176 #[serde(default)]
179 #[serde(alias = "write_buffer")]
180 pub write_buffer_size: Option<usize>,
181
182 #[serde(default)]
183 pub max_write_buffer_number: Option<i32>,
184
185 #[serde(default)]
186 pub min_write_buffer_number: Option<i32>,
187
188 #[serde(default)]
189 pub min_write_buffer_number_to_merge: Option<i32>,
190
191 #[serde(default)]
192 pub block_cache_size: Option<u32>,
193
194 #[serde(default)]
195 pub cache_index_and_filter_blocks: Option<bool>,
196
197 #[serde(default)]
198 #[serde(alias = "compression")]
199 #[serde(deserialize_with = "to_rocksdb_compression_type_opt")]
200 pub compression_type: Option<rocksdb::DBCompressionType>,
201
202 #[serde(default)]
203 #[serde(alias = "wal_compression")]
204 #[serde(deserialize_with = "to_rocksdb_compression_type_opt")]
205 pub wal_compression_type: Option<rocksdb::DBCompressionType>,
206
207 #[serde(default)]
208 pub wal_ttl_seconds: Option<u64>,
209
210 #[serde(default)]
211 pub wal_size_limit_mb: Option<u64>,
212
213 #[serde(default)]
214 pub wal_bytes_per_sync: Option<u64>,
215
216 #[serde(default)]
217 #[serde(deserialize_with = "to_rocksdb_recovery_mode_opt")]
218 pub wal_recovery_mode: Option<rocksdb::DBRecoveryMode>,
219
220 #[serde(default)]
221 pub compression_level: Option<i32>,
222
223 #[serde(default)]
224 #[serde(alias = "compression_start_level")]
225 pub min_level_to_compress: Option<std::ffi::c_int>,
226
227 #[serde(default)]
228 #[serde(alias = "level0_file_num_compaction_trigger")]
229 pub level_zero_file_num_compaction_trigger: Option<i32>,
230
231 #[serde(default)]
232 #[serde(alias = "level0_slowdown_writes_trigger")]
233 pub level_zero_slowdown_writes_trigger: Option<i32>,
234
235 #[serde(default)]
236 #[serde(alias = "level0_stop_writes_trigger")]
237 pub level_zero_stop_writes_trigger: Option<i32>,
238
239 #[serde(default)]
240 pub max_bytes_for_level_base: Option<u64>,
241
242 #[serde(default)]
243 pub max_bytes_for_level_multiplier: Option<f64>,
244
245 #[serde(default)]
246 pub target_file_size_base: Option<u64>,
247
248 #[serde(default)]
249 pub max_background_jobs: Option<i32>,
250
251 #[serde(default)]
252 #[serde(alias = "max_compactions")]
253 pub max_subcompactions: Option<u32>,
254
255 #[serde(default)]
256 pub max_flushes: Option<u32>,
257
258 #[serde(default)]
259 pub stats_dump_period_sec: Option<u32>,
260}
261
262fn parse_rocksdb_compression_type<E: serde::de::Error>(
263 str: &str,
264) -> Result<rocksdb::DBCompressionType, E> {
265 match str.to_ascii_lowercase().as_str() {
268 "none" => Ok(rocksdb::DBCompressionType::None),
269 "zstd" => Ok(rocksdb::DBCompressionType::Zstd),
275 _ => Err(serde::de::Error::unknown_variant(
276 str,
277 &[
278 "none",
279 "zstd",
285 ],
286 )),
287 }
288}
289
290fn to_rocksdb_compression_type_opt<'de, D>(
291 deserializer: D,
292) -> Result<Option<rocksdb::DBCompressionType>, D::Error>
293where
294 D: serde::de::Deserializer<'de>,
295{
296 let str: Option<String> = Deserialize::deserialize(deserializer)?;
297 str.map(|s| parse_rocksdb_compression_type(&s)).transpose()
298}
299
300fn parse_rocksdb_recovery_mode<E: serde::de::Error>(
301 str: &str,
302) -> Result<rocksdb::DBRecoveryMode, E> {
303 match str.to_ascii_lowercase().as_str() {
304 "tolerate_corrupted_tail_records" | "TolerateCorruptedTailRecords" => {
305 Ok(rocksdb::DBRecoveryMode::TolerateCorruptedTailRecords)
306 }
307 "absolute_consistency" | "AbsoluteConsistency" => {
308 Ok(rocksdb::DBRecoveryMode::AbsoluteConsistency)
309 }
310 "point_in_time" | "PointInTime" => Ok(rocksdb::DBRecoveryMode::PointInTime),
311 "skip_any_corrupted_record" | "SkipAnyCorruptedRecord" => {
312 Ok(rocksdb::DBRecoveryMode::SkipAnyCorruptedRecord)
313 }
314 _ => Err(serde::de::Error::unknown_variant(
315 str,
316 &[
317 "tolerate_corrupted_tail_records",
318 "absolute_consistency",
319 "point_in_time",
320 "skip_any_corrupted_record",
321 ],
322 )),
323 }
324}
325
326fn to_rocksdb_recovery_mode_opt<'de, D>(
327 deserializer: D,
328) -> Result<Option<rocksdb::DBRecoveryMode>, D::Error>
329where
330 D: serde::de::Deserializer<'de>,
331{
332 let str: Option<String> = Deserialize::deserialize(deserializer)?;
333 str.map(|s| parse_rocksdb_recovery_mode(&s)).transpose()
334}
335
336#[derive(Deserialize)]
337pub struct ConfigStoreFST {
338 #[serde(deserialize_with = "env_var::path_buf")]
339 pub path: PathBuf,
340
341 pub pool: ConfigStoreFSTPool,
342
343 pub graph: ConfigStoreFSTGraph,
344}
345
346#[derive(Deserialize)]
347pub struct ConfigStoreFSTPool {
348 pub inactive_after: u64,
349}
350
351#[derive(Deserialize)]
352pub struct ConfigStoreFSTGraph {
353 pub consolidate_after: u64,
354
355 pub max_size: usize,
356
357 pub max_words: usize,
358}
359
360#[cfg(test)]
361pub(crate) mod tests {
362 pub fn defaults_toml() -> &'static str {
363 r#"
364 [channel]
365 inet = "[::1]:1491"
366 tcp_timeout = 300
367
368 [normalization]
369 unicode_normalization = "none"
370 diacritic_folding_enabled = true
371 stemming_enabled = false
372
373 [tokenization]
374 detect_special_patterns = true
375 compat_split_special_patterns = false
376
377 [stopwords]
378 allow = []
379 deny = []
380
381 [search]
382 query_limit_default = 10
383 query_limit_maximum = 100
384 query_alternates_try = 4
385 query_minimum_term_idf_default = 0.1
386 query_minimum_term_idf_minimum_object_count = 100
387 suggest_limit_default = 5
388 suggest_limit_maximum = 20
389 list_limit_default = 100
390 list_limit_maximum = 500
391
392 [store.kv]
393 path = "./data/store/kv/"
394 retain_word_objects = 1000
395 pool.inactive_after = 1800
396 database.flush_after = 900
397 database.compression_type = "zstd"
398 database.parallelism = 2
399 database.max_subcompactions = 1
400 database.max_flushes = 1
401 database.write_buffer_size = 16384
402 database.write_ahead_log = true
403
404 [store.fst]
405 path = "./data/store/fst/"
406 pool.inactive_after = 300
407 graph.consolidate_after = 180
408 graph.max_size = 2048
409 graph.max_words = 250000
410 "#
411 }
412}