use crate::prelude::*;
use kalosm_language_model::*;
#[derive(Debug, Clone)]
struct SemanticChunk<S: VectorSpace> {
range: std::ops::Range<usize>,
sentences: usize,
embedding: Embedding<S>,
distance_to_next: Option<f32>,
}
pub struct SemanticChunker {
target_score: f32,
small_chunk_merge_bonus: f32,
small_chunk_exponent: f32,
large_chunk_penalty: f32,
large_chunk_exponent: f32,
}
impl Default for SemanticChunker {
fn default() -> Self {
Self::new()
}
}
impl SemanticChunker {
pub const fn new() -> Self {
Self {
target_score: 0.65,
small_chunk_merge_bonus: 10.0,
small_chunk_exponent: -2.0,
large_chunk_penalty: 200.0,
large_chunk_exponent: 1.5,
}
}
pub fn with_target_score(mut self, target_score: f32) -> Self {
self.target_score = target_score;
self
}
pub fn with_small_chunk_merge_bonus(mut self, small_chunk_merge_bonus: f32) -> Self {
self.small_chunk_merge_bonus = small_chunk_merge_bonus;
self
}
pub fn with_small_chunk_exponent(mut self, small_chunk_exponent: f32) -> Self {
self.small_chunk_exponent = small_chunk_exponent;
self
}
pub fn with_large_chunk_penalty(mut self, large_chunk_penalty: f32) -> Self {
self.large_chunk_penalty = large_chunk_penalty;
self
}
pub fn with_large_chunk_exponent(mut self, large_chunk_exponent: f32) -> Self {
self.large_chunk_exponent = large_chunk_exponent;
self
}
fn score_merge<S: VectorSpace>(
&self,
first_chunk: &SemanticChunk<S>,
second_chunk: &SemanticChunk<S>,
) -> f32 {
let short_chunk_merge_bonus = (self.small_chunk_merge_bonus
/ first_chunk.range.len().min(second_chunk.range.len()) as f32)
.powf(self.small_chunk_exponent);
let large_chunk_penalty = (first_chunk.sentences.max(second_chunk.sentences) as f32)
.powf(self.large_chunk_exponent)
/ -self.large_chunk_penalty;
let similarity = first_chunk.distance_to_next.unwrap();
similarity + short_chunk_merge_bonus + large_chunk_penalty
}
}
impl Chunker for SemanticChunker {
async fn chunk<E: Embedder + Send>(
&self,
document: &Document,
embedder: &E,
) -> anyhow::Result<Vec<Chunk<E::VectorSpace>>> {
let text = document.body();
let mut current_chunks = Vec::new();
let chunker = ChunkStrategy::Sentence {
sentence_count: 1,
overlap: 0,
};
let mut initial_chunks = Vec::new();
for chunk in chunker.chunk_str(text) {
let trimmed = text[chunk.clone()].trim();
if !trimmed.is_empty() {
current_chunks.push(chunk);
initial_chunks.push(trimmed.to_string());
}
}
let embeddings = embedder.embed_vec(initial_chunks).await?;
let mut chunks = Vec::new();
for (i, chunk) in current_chunks.iter().enumerate() {
if i == current_chunks.len() - 1 {
chunks.push(SemanticChunk {
range: chunk.clone(),
sentences: 1,
embedding: embeddings[i].clone(),
distance_to_next: None,
});
break;
}
let first = &embeddings[i];
let second = &embeddings[i + 1];
let distance_to_next = first.cosine_similarity(second);
let chunk = SemanticChunk {
range: chunk.clone(),
sentences: 1,
embedding: first.clone(),
distance_to_next: Some(distance_to_next),
};
chunks.push(chunk);
}
while let Some((index, first_chunk)) = chunks
.iter()
.enumerate()
.filter(|(_, c)| c.distance_to_next.is_some())
.max_by(|(index, c1), (index2, c2)| {
let c1_score = self.score_merge(c1, &chunks[index + 1]);
let c2_score = self.score_merge(c2, &chunks[index2 + 1]);
c1_score.partial_cmp(&c2_score).unwrap()
})
{
let second_chunk = &chunks[index + 1];
let highest_similarity = self.score_merge(first_chunk, second_chunk);
if highest_similarity < self.target_score {
break;
}
let range = first_chunk.range.start..second_chunk.range.end;
let sentences = first_chunk.sentences + second_chunk.sentences;
let new_text = text[range.clone()].trim();
let embedding = embedder.embed(new_text).await?;
let distance_to_next = chunks
.get(index + 2)
.map(|chunk_after_merge| embedding.cosine_similarity(&chunk_after_merge.embedding));
if let Some(prev_chunk) = index.checked_sub(1).and_then(|index| chunks.get_mut(index)) {
let distance_to_prev = prev_chunk.embedding.cosine_similarity(&embedding);
prev_chunk.distance_to_next = Some(distance_to_prev);
}
let new_chunk = SemanticChunk {
range,
sentences,
embedding,
distance_to_next,
};
chunks.remove(index + 1);
chunks[index] = new_chunk;
}
let mut final_chunks = Vec::new();
for chunk in chunks {
let SemanticChunk {
range, embedding, ..
} = chunk;
final_chunks.push(Chunk {
byte_range: range,
embeddings: vec![embedding],
});
}
Ok(final_chunks)
}
}