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) char_end: usize,
pub(crate) token_start: usize,
pub(crate) token_count: usize,
}
fn digest_hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut out, byte| {
let _ = write!(&mut out, "{byte:02x}");
out
})
}
pub(crate) fn chunk_content_hash(text: &str) -> String {
use sha2::{Digest, Sha256};
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
digest_hex(&Sha256::digest(normalized.as_bytes()))
}
pub(crate) fn deterministic_work_item_id(
tenant_id: &str,
source_name: &str,
parent_pk: &str,
chunk_seq: u32,
doc_version: &str,
chunk_hash: &str,
) -> String {
use sha2::{Digest, Sha256};
let identity = format!(
"{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
tenant_id.trim(),
source_name.trim(),
parent_pk.trim(),
chunk_seq,
doc_version.trim(),
chunk_hash
);
digest_hex(&Sha256::digest(identity.as_bytes()))
}
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();
}
let leading = text
.chars()
.take_while(|value| value.is_whitespace())
.count();
return vec![Chunk {
seq: 0,
text: trimmed.to_string(),
char_start: leading,
char_end: leading + trimmed.chars().count(),
token_start: 0,
token_count: trimmed.split_whitespace().count(),
}];
}
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 raw_slice = &text[boundaries[start]..boundaries[end]];
let slice = raw_slice.trim();
if !slice.is_empty() {
let leading = raw_slice
.chars()
.take_while(|value| value.is_whitespace())
.count();
let char_start = start + leading;
chunks.push(Chunk {
seq,
text: slice.to_string(),
char_start,
char_end: char_start + slice.chars().count(),
token_start: text[..boundaries[char_start]].split_whitespace().count(),
token_count: slice.split_whitespace().count(),
});
seq += 1;
}
if end >= total {
break;
}
let next = end.saturating_sub(overlap).max(start + 1);
start = next;
}
chunks
}
pub(crate) fn chunk_text_tokens(
text: &str,
requested_tokens: usize,
overlap_tokens: usize,
max_input_tokens: usize,
max_chunks: usize,
) -> Vec<Chunk> {
let provider_safe_max = max_input_tokens
.saturating_mul(85)
.checked_div(100)
.unwrap_or(0);
let window = requested_tokens.max(1).min(provider_safe_max.max(1));
let overlap = overlap_tokens.min(window.saturating_sub(1));
let max_chunks = max_chunks.max(1);
let tokens = text
.split_whitespace()
.filter_map(|token| {
let start = token.as_ptr() as usize - text.as_ptr() as usize;
let end = start + token.len();
(end <= text.len()).then_some((start, end))
})
.collect::<Vec<_>>();
if tokens.is_empty() {
return Vec::new();
}
let mut chunks = Vec::new();
let mut start_token = 0usize;
while start_token < tokens.len() && chunks.len() < max_chunks {
let hard_end = (start_token + window).min(tokens.len());
let mut end_token = hard_end;
if hard_end < tokens.len() {
let lower = start_token + (window.saturating_mul(4) / 5).max(1);
if let Some(boundary) = (lower..hard_end).rev().find(|index| {
let gap_start = tokens[*index - 1].1;
let gap_end = tokens[*index].0;
let gap_is_block = text
.get(gap_start..gap_end)
.is_some_and(|gap| gap.contains("\n\n") || gap.contains('\n'));
let previous = text
.get(tokens[*index - 1].0..tokens[*index - 1].1)
.unwrap_or_default()
.trim_end_matches(|value: char| {
matches!(value, '\"' | '\'' | ')' | ']' | '*' | '_')
});
let next = text
.get(tokens[*index].0..tokens[*index].1)
.unwrap_or_default();
gap_is_block
|| previous
.chars()
.next_back()
.is_some_and(|value| matches!(value, '.' | '?' | '!' | ';' | '}'))
|| next.starts_with('#')
|| next.starts_with("```")
}) {
end_token = boundary;
}
}
let byte_start = tokens[start_token].0;
let byte_end = tokens[end_token - 1].1;
let value = text[byte_start..byte_end].trim();
if !value.is_empty() {
chunks.push(Chunk {
seq: chunks.len() as u32,
text: value.to_string(),
char_start: text[..byte_start].chars().count(),
char_end: text[..byte_end].chars().count(),
token_start: start_token,
token_count: end_token - start_token,
});
}
if end_token >= tokens.len() {
break;
}
start_token = end_token.saturating_sub(overlap).max(start_token + 1);
}
chunks
}
pub(crate) fn chunk_source_text_for_model(
text: &str,
model: &super::model::StoredModel,
) -> Vec<Chunk> {
if model.chunking_strategy.eq_ignore_ascii_case("CHARACTER") {
return chunk_source_text(text);
}
chunk_text_tokens(
text,
model.chunk_tokens.max(1) as usize,
model.chunk_overlap_tokens.max(0) as usize,
model.max_input_tokens.max(1) as usize,
super::config::embedding_max_chunks_per_row(),
)
}
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(),
)
}