use cp_core::{CPError, Chunk, Result};
use text_splitter::{ChunkConfig as TSChunkConfig, MarkdownSplitter};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct ChunkConfig {
pub chunk_size: usize,
pub overlap: usize,
}
impl Default for ChunkConfig {
fn default() -> Self {
Self {
chunk_size: 1000, overlap: 200, }
}
}
pub struct Chunker {
config: ChunkConfig,
}
impl Default for Chunker {
fn default() -> Self {
Self::new(ChunkConfig::default())
}
}
impl Chunker {
pub fn new(config: ChunkConfig) -> Self {
Self { config }
}
pub fn chunk(&self, doc_id: Uuid, text: &str) -> Result<Vec<Chunk>> {
if text.is_empty() {
return Ok(Vec::new());
}
let ts_config = TSChunkConfig::new(self.config.chunk_size)
.with_overlap(self.config.overlap)
.map_err(|e| CPError::Parse(format!("Invalid chunk config: {e}")))?
.with_trim(true);
let splitter = MarkdownSplitter::new(ts_config);
let chunks: Vec<Chunk> = splitter
.chunk_indices(text)
.enumerate()
.map(|(seq, (byte_offset, chunk_text))| {
Chunk::new(doc_id, chunk_text, byte_offset as u64, seq as u32)
})
.collect();
Ok(chunks)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_text() {
let chunker = Chunker::default();
let chunks = chunker.chunk(Uuid::new_v4(), "").unwrap();
assert!(chunks.is_empty());
}
#[test]
fn test_short_text() {
let chunker = Chunker::default();
let chunks = chunker.chunk(Uuid::new_v4(), "Short text.").unwrap();
assert_eq!(chunks.len(), 1);
}
#[test]
fn test_long_text_chunking() {
let chunker = Chunker::new(ChunkConfig {
chunk_size: 100,
overlap: 20,
});
let text = "A".repeat(250);
let chunks = chunker.chunk(Uuid::new_v4(), &text).unwrap();
assert!(chunks.len() > 1);
for (i, chunk) in chunks.iter().enumerate() {
assert_eq!(chunk.sequence, i as u32);
}
}
#[test]
fn test_sentence_boundary() {
let chunker = Chunker::new(ChunkConfig {
chunk_size: 20,
overlap: 5,
});
let text = "First sentence. Second sentence. Third sentence.";
let chunks = chunker.chunk(Uuid::new_v4(), text).unwrap();
assert!(chunks.len() > 1);
}
#[test]
fn test_byte_offsets_valid() {
let chunker = Chunker::new(ChunkConfig {
chunk_size: 50,
overlap: 10,
});
let text = "# Heading\n\nFirst paragraph with some text.\n\n## Subheading\n\nSecond paragraph with more text here.";
let chunks = chunker.chunk(Uuid::new_v4(), text).unwrap();
for chunk in &chunks {
let offset = chunk.byte_offset as usize;
assert!(
offset <= text.len(),
"byte_offset {} exceeds text len {}",
offset,
text.len()
);
}
}
#[test]
fn test_overlap_shared_text() {
let chunker = Chunker::new(ChunkConfig {
chunk_size: 30,
overlap: 10,
});
let text = "Word one. Word two. Word three. Word four. Word five. Word six.";
let chunks = chunker.chunk(Uuid::new_v4(), text).unwrap();
if chunks.len() >= 2 {
for (i, chunk) in chunks.iter().enumerate() {
assert_eq!(chunk.sequence, i as u32);
}
}
}
}