use ahash::AHashSet;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TableChunkingMode {
#[default]
Split,
RepeatHeader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ChunkerType {
#[default]
Text,
Markdown,
Yaml,
Semantic,
}
#[derive(Debug, Clone, Serialize, Default)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChunkSizing {
#[default]
Characters,
#[cfg(feature = "chunking-tokenizers")]
Tokenizer {
model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cache_dir: Option<std::path::PathBuf>,
},
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
enum ChunkSizingWire {
Characters {},
#[cfg(feature = "chunking-tokenizers")]
Tokenizer {
model: String,
#[serde(default)]
cache_dir: Option<std::path::PathBuf>,
},
}
impl<'de> Deserialize<'de> for ChunkSizing {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match ChunkSizingWire::deserialize(deserializer)? {
ChunkSizingWire::Characters {} => Self::Characters,
#[cfg(feature = "chunking-tokenizers")]
ChunkSizingWire::Tokenizer { model, cache_dir } => Self::Tokenizer { model, cache_dir },
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PostProcessorConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub enabled_processors: Option<Vec<String>>,
#[serde(default)]
pub disabled_processors: Option<Vec<String>>,
#[serde(skip)]
pub enabled_set: Option<AHashSet<String>>,
#[serde(skip)]
pub disabled_set: Option<AHashSet<String>>,
}
impl PostProcessorConfig {
#[cfg(test)]
pub(crate) fn build_lookup_sets(&mut self) {
if let Some(ref enabled) = self.enabled_processors {
self.enabled_set = Some(enabled.iter().cloned().collect());
}
if let Some(ref disabled) = self.disabled_processors {
self.disabled_set = Some(disabled.iter().cloned().collect());
}
}
}
impl Default for PostProcessorConfig {
fn default() -> Self {
Self {
enabled: true,
enabled_processors: None,
disabled_processors: None,
enabled_set: None,
disabled_set: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ChunkingConfig {
#[serde(default = "default_chunk_size", rename = "max_chars", alias = "max_characters")]
pub max_characters: usize,
#[serde(default = "default_chunk_overlap", rename = "max_overlap", alias = "overlap")]
pub overlap: usize,
#[serde(default = "default_trim")]
pub trim: bool,
#[serde(default = "default_chunker_type")]
pub chunker_type: ChunkerType,
#[serde(skip_serializing_if = "Option::is_none")]
pub embedding: Option<EmbeddingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "alef-meta", alef(since = "1.1.0"))]
pub sparse_embedding: Option<super::sparse_embedding::SparseEmbeddingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "alef-meta", alef(since = "1.1.0"))]
pub late_interaction: Option<super::late_interaction::LateInteractionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preset: Option<String>,
#[serde(default, deserialize_with = "deserialize_null_default")]
pub sizing: ChunkSizing,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub topic_threshold: Option<f32>,
#[serde(default)]
pub table_chunking: TableChunkingMode,
}
impl ChunkingConfig {
#[cfg(test)]
pub(crate) fn with_topic_threshold(mut self, threshold: f32) -> Self {
assert!(
(0.0..=1.0).contains(&threshold),
"topic_threshold must be in [0.0, 1.0], got {threshold}"
);
self.topic_threshold = Some(threshold);
self
}
#[cfg(feature = "embeddings")]
pub(crate) fn resolve_preset(&self) -> Self {
let preset_name = match &self.preset {
Some(name) => name,
None => return self.clone(),
};
let preset = match crate::embeddings::get_preset(preset_name) {
Some(p) => p,
None => {
tracing::warn!(
"Unknown chunking preset '{}', using manual config. Available: {:?}",
preset_name,
crate::embeddings::list_presets()
);
return self.clone();
}
};
let mut resolved = self.clone();
resolved.max_characters = preset.chunk_size;
resolved.overlap = preset.overlap;
resolved
}
#[cfg(all(feature = "chunking", not(feature = "embeddings")))]
pub(crate) fn resolve_preset(&self) -> Self {
if self.preset.is_some() {
tracing::warn!("Chunking presets require the 'embeddings' feature");
}
self.clone()
}
}
impl Default for ChunkingConfig {
fn default() -> Self {
Self {
max_characters: 1000,
overlap: 200,
trim: true,
chunker_type: ChunkerType::Text,
embedding: None,
sparse_embedding: None,
late_interaction: None,
preset: None,
sizing: ChunkSizing::default(),
topic_threshold: None,
table_chunking: TableChunkingMode::Split,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EmbeddingConfig {
#[serde(default = "default_model", deserialize_with = "deserialize_null_model")]
pub model: EmbeddingModelType,
#[serde(default = "default_normalize")]
pub normalize: bool,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default)]
pub show_download_progress: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acceleration: Option<super::acceleration::AccelerationConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_embed_duration_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_sequence_length: Option<usize>,
}
impl Default for EmbeddingConfig {
fn default() -> Self {
Self {
model: default_balanced_embedding_model(),
normalize: true,
batch_size: 32,
show_download_progress: false,
cache_dir: None,
acceleration: None,
max_embed_duration_secs: Some(60),
max_sequence_length: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum EmbeddingModelType {
Preset {
name: String,
},
Custom {
model_id: String,
dimensions: usize,
},
Llm {
llm: Box<super::llm::LlmConfig>,
},
Plugin {
name: String,
},
}
impl Default for EmbeddingModelType {
fn default() -> Self {
Self::Preset {
name: "gte-modernbert-base".to_string(),
}
}
}
fn default_true() -> bool {
true
}
fn default_chunk_size() -> usize {
1000
}
pub(crate) fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: Default + serde::Deserialize<'de>,
{
let opt = Option::<T>::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
fn default_chunk_overlap() -> usize {
200
}
fn default_trim() -> bool {
true
}
fn default_chunker_type() -> ChunkerType {
ChunkerType::Text
}
fn default_normalize() -> bool {
true
}
fn default_batch_size() -> usize {
32
}
fn default_model() -> EmbeddingModelType {
EmbeddingModelType::Preset {
name: "gte-modernbert-base".to_string(),
}
}
fn default_balanced_embedding_model() -> EmbeddingModelType {
EmbeddingModelType::Preset {
name: "balanced".to_string(),
}
}
fn deserialize_null_model<'de, D>(deserializer: D) -> Result<EmbeddingModelType, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<EmbeddingModelType>::deserialize(deserializer)?;
Ok(opt.unwrap_or_else(default_model))
}
#[cfg(test)]
mod tests;