use crate::pipeline::token_counter::{TokenCounter, WordProxyCounter};
use crate::pipeline::Element;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct SemanticChunkConfig {
pub max_tokens: usize,
pub overlap_tokens: usize,
pub respect_element_boundaries: bool,
}
impl Default for SemanticChunkConfig {
fn default() -> Self {
Self {
max_tokens: 512,
overlap_tokens: 50,
respect_element_boundaries: true,
}
}
}
impl SemanticChunkConfig {
pub fn new(max_tokens: usize) -> Self {
Self {
max_tokens,
..Default::default()
}
}
pub fn with_overlap(mut self, overlap: usize) -> Self {
self.overlap_tokens = overlap;
self
}
}
#[derive(Debug, Clone)]
pub struct SemanticChunk {
elements: Vec<Element>,
oversized: bool,
token_estimate: usize,
}
impl SemanticChunk {
pub fn elements(&self) -> &[Element] {
&self.elements
}
pub fn text(&self) -> String {
self.elements
.iter()
.map(|e| e.display_text())
.collect::<Vec<_>>()
.join("\n")
}
pub fn token_estimate(&self) -> usize {
self.token_estimate
}
pub fn page_numbers(&self) -> Vec<u32> {
let mut pages: Vec<u32> = self.elements.iter().map(|e| e.page()).collect();
pages.sort_unstable();
pages.dedup();
pages
}
pub fn is_oversized(&self) -> bool {
self.oversized
}
}
pub struct SemanticChunker {
config: SemanticChunkConfig,
counter: Arc<dyn TokenCounter>,
}
impl Default for SemanticChunker {
fn default() -> Self {
Self {
config: SemanticChunkConfig::default(),
counter: Arc::new(WordProxyCounter),
}
}
}
impl SemanticChunker {
pub fn new(config: SemanticChunkConfig) -> Self {
Self {
config,
counter: Arc::new(WordProxyCounter),
}
}
pub fn with_token_counter(mut self, counter: Arc<dyn TokenCounter>) -> Self {
self.counter = counter;
self
}
fn element_tokens(&self, element: &Element) -> usize {
self.counter.count(&element.display_text())
}
fn appended_tokens(&self, elements: &[Element], tokens: usize, next: &Element) -> usize {
if elements.is_empty() {
return self.element_tokens(next);
}
if self.counter.is_additive_over_whitespace_join() {
return tokens.saturating_add(self.element_tokens(next));
}
let text = elements
.iter()
.chain(std::iter::once(next))
.map(Element::display_text)
.collect::<Vec<_>>()
.join("\n");
self.counter.count(&text)
}
fn make_chunk(&self, elements: Vec<Element>) -> SemanticChunk {
let text = elements
.iter()
.map(|e| e.display_text())
.collect::<Vec<_>>()
.join("\n");
let token_estimate = self.counter.count(&text);
SemanticChunk {
elements,
oversized: token_estimate > self.config.max_tokens,
token_estimate,
}
}
fn make_paragraph_chunk(
&self,
text: &str,
meta: &crate::pipeline::ElementMetadata,
) -> SemanticChunk {
self.make_chunk(vec![Element::Paragraph(crate::pipeline::ElementData {
text: text.to_string(),
metadata: meta.clone(),
})])
}
pub fn chunk(&self, elements: &[Element]) -> Vec<SemanticChunk> {
if elements.is_empty() {
return Vec::new();
}
let mut chunks = Vec::new();
let mut current_elements: Vec<Element> = Vec::new();
let mut current_tokens = 0usize;
for element in elements {
let elem_tokens = self.element_tokens(element);
let mut joined_tokens =
self.appended_tokens(¤t_elements, current_tokens, element);
if !is_splittable(element) {
if !current_elements.is_empty()
&& joined_tokens > self.config.max_tokens
&& self.config.respect_element_boundaries
{
self.flush_chunk(
&mut chunks,
&mut current_elements,
&mut current_tokens,
element,
);
joined_tokens =
self.appended_tokens(¤t_elements, current_tokens, element);
}
if elem_tokens > self.config.max_tokens && current_elements.is_empty() {
chunks.push(self.make_chunk(vec![element.clone()]));
continue;
}
current_tokens = joined_tokens;
current_elements.push(element.clone());
continue;
}
if joined_tokens <= self.config.max_tokens {
current_tokens = joined_tokens;
current_elements.push(element.clone());
} else if elem_tokens <= self.config.max_tokens {
if !current_elements.is_empty() {
self.flush_chunk(
&mut chunks,
&mut current_elements,
&mut current_tokens,
element,
);
}
current_tokens = self.appended_tokens(¤t_elements, current_tokens, element);
current_elements.push(element.clone());
} else {
if !current_elements.is_empty() {
chunks.push(self.make_chunk(std::mem::take(&mut current_elements)));
}
current_tokens = 0;
let sentences = split_sentences(element.text());
let meta = element.metadata().clone();
let mut sentence_buf = String::new();
let mut sentence_tokens = 0usize;
for sentence in &sentences {
let single_tokens = self.counter.count(sentence);
let joined_tokens = if sentence_buf.is_empty() {
single_tokens
} else if self.counter.is_additive_over_whitespace_join() {
sentence_tokens.saturating_add(single_tokens)
} else {
self.counter.count(&format!("{sentence_buf} {sentence}"))
};
if joined_tokens > self.config.max_tokens && !sentence_buf.is_empty() {
chunks.push(self.make_paragraph_chunk(&sentence_buf, &meta));
sentence_buf.clear();
sentence_tokens = single_tokens;
} else {
sentence_tokens = joined_tokens;
}
if !sentence_buf.is_empty() {
sentence_buf.push(' ');
}
sentence_buf.push_str(sentence);
}
if !sentence_buf.is_empty() {
current_tokens = self.counter.count(&sentence_buf);
current_elements.push(Element::Paragraph(crate::pipeline::ElementData {
text: sentence_buf,
metadata: meta,
}));
}
}
}
if !current_elements.is_empty() {
chunks.push(self.make_chunk(current_elements));
}
chunks
}
fn flush_chunk(
&self,
chunks: &mut Vec<SemanticChunk>,
current_elements: &mut Vec<Element>,
current_tokens: &mut usize,
next: &Element,
) {
let flushed = std::mem::take(current_elements);
*current_tokens = 0;
if self.config.overlap_tokens > 0 {
let mut retained_start = flushed.len();
let mut tokens = 0usize;
for start in (0..flushed.len()).rev() {
let suffix = &flushed[start..];
tokens = if self.counter.is_additive_over_whitespace_join() {
tokens.saturating_add(self.element_tokens(&flushed[start]))
} else {
let text = suffix
.iter()
.map(Element::display_text)
.collect::<Vec<_>>()
.join("\n");
self.counter.count(&text)
};
if tokens > self.config.overlap_tokens
|| self.appended_tokens(suffix, tokens, next) > self.config.max_tokens
{
break;
}
retained_start = start;
*current_tokens = tokens;
}
current_elements.extend_from_slice(&flushed[retained_start..]);
}
chunks.push(self.make_chunk(flushed));
}
}
fn is_splittable(element: &Element) -> bool {
matches!(
element,
Element::Paragraph(_) | Element::ListItem(_) | Element::CodeBlock(_) | Element::KeyValue(_)
)
}
fn split_sentences(text: &str) -> Vec<String> {
let mut sentences = Vec::new();
let mut current = String::new();
for ch in text.chars() {
current.push(ch);
if (ch == '.' || ch == '!' || ch == '?') && !current.trim().is_empty() {
sentences.push(current.trim().to_string());
current.clear();
}
}
if !current.trim().is_empty() {
if let Some(last) = sentences.last_mut() {
last.push(' ');
last.push_str(current.trim());
} else {
sentences.push(current.trim().to_string());
}
}
sentences
}