pub(crate) const CHUNK_ID_SEP: &str = "#chunk:";
pub(crate) struct Chunk {
pub(crate) seq: u32,
pub(crate) text: String,
pub(crate) char_start: usize,
}
pub(crate) fn chunk_point_id(parent_pk: &str, seq: u32, chunk_count: usize) -> String {
if chunk_count <= 1 {
parent_pk.to_string()
} else {
format!("{parent_pk}{CHUNK_ID_SEP}{seq}")
}
}
pub(crate) fn parse_chunk_point_id(point_id: &str) -> (&str, u32) {
if let Some(idx) = point_id.rfind(CHUNK_ID_SEP) {
let seq_str = &point_id[idx + CHUNK_ID_SEP.len()..];
if !seq_str.is_empty() && seq_str.bytes().all(|b| b.is_ascii_digit()) {
if let Ok(seq) = seq_str.parse::<u32>() {
return (&point_id[..idx], seq);
}
}
}
(point_id, 0)
}
pub(crate) fn chunk_text(text: &str, size: usize, overlap: usize, max_chunks: usize) -> Vec<Chunk> {
let size = size.max(1);
let overlap = overlap.min(size - 1);
let max_chunks = max_chunks.max(1);
let mut boundaries: Vec<usize> = text.char_indices().map(|(b, _)| b).collect();
boundaries.push(text.len());
let whitespace: Vec<bool> = text.chars().map(char::is_whitespace).collect();
let total = whitespace.len();
if total == 0 {
return Vec::new();
}
if total <= size {
let trimmed = text.trim();
if trimmed.is_empty() {
return Vec::new();
}
return vec![Chunk {
seq: 0,
text: trimmed.to_string(),
char_start: 0,
}];
}
let mut chunks = Vec::new();
let mut start = 0usize;
let mut seq = 0u32;
while start < total && (chunks.len() as usize) < max_chunks {
let hard_end = (start + size).min(total);
let mut end = hard_end;
if hard_end < total {
if let Some(ws) = (start + 1..hard_end).rev().find(|&i| whitespace[i]) {
end = ws;
}
}
let slice = text[boundaries[start]..boundaries[end]].trim();
if !slice.is_empty() {
chunks.push(Chunk {
seq,
text: slice.to_string(),
char_start: start,
});
seq += 1;
}
if end >= total {
break;
}
let next = end.saturating_sub(overlap).max(start + 1);
start = next;
}
chunks
}
pub(crate) fn chunk_source_text(text: &str) -> Vec<Chunk> {
chunk_text(
text,
super::config::embedding_chunk_size(),
super::config::embedding_chunk_overlap(),
super::config::embedding_max_chunks_per_row(),
)
}