lc_rag/
semantic_splitter.rs1use lc_embeddings::{cosine_similarity, EmbeddingError, Embeddings};
13use lc_vector_stores::Document;
14
15pub struct SemanticSplitter<E> {
21 embeddings: E,
22 breakpoint_threshold: f32,
24 max_chunk_size: usize,
26}
27
28impl<E: Embeddings> SemanticSplitter<E> {
29 pub fn new(embeddings: E, breakpoint_threshold: f32, max_chunk_size: usize) -> Self {
37 Self {
38 embeddings,
39 breakpoint_threshold,
40 max_chunk_size,
41 }
42 }
43
44 pub fn with_defaults(embeddings: E) -> Self {
46 Self::new(embeddings, 0.5, 1000)
47 }
48
49 fn split_sentences(text: &str) -> Vec<String> {
51 let mut sentences = Vec::new();
52 let mut current = String::new();
53 for ch in text.chars() {
54 current.push(ch);
55 if matches!(ch, '。' | '!' | '?' | ';' | '\n' | '.' | '!' | '?') {
56 let trimmed = current.trim().to_string();
57 if !trimmed.is_empty() {
58 sentences.push(trimmed);
59 }
60 current.clear();
61 }
62 }
63 let trimmed = current.trim().to_string();
64 if !trimmed.is_empty() {
65 sentences.push(trimmed);
66 }
67 sentences
68 }
69
70 pub async fn split_text(&self, text: &str) -> Result<Vec<String>, EmbeddingError> {
72 let sentences = Self::split_sentences(text);
73 if sentences.is_empty() {
74 return Ok(Vec::new());
75 }
76 if sentences.len() == 1 {
77 return Ok(vec![sentences.into_iter().next().unwrap_or_default()]);
78 }
79
80 let refs: Vec<&str> = sentences.iter().map(|s| s.as_str()).collect();
82 let embeddings = self.embeddings.embed_documents(&refs).await?;
83
84 let mut chunks = Vec::new();
85 let mut current = sentences[0].clone();
86
87 for i in 1..sentences.len() {
88 let sim = cosine_similarity(&embeddings[i - 1], &embeddings[i]).unwrap_or(0.0);
89 let would_exceed = current.len() + sentences[i].len() + 1 > self.max_chunk_size;
90
91 if sim < self.breakpoint_threshold || would_exceed {
92 chunks.push(std::mem::take(&mut current));
93 current = sentences[i].clone();
94 } else {
95 current.push('\n');
96 current.push_str(&sentences[i]);
97 }
98 }
99 if !current.is_empty() {
100 chunks.push(current);
101 }
102 Ok(chunks)
103 }
104
105 pub async fn split_document(
107 &self,
108 document: &Document,
109 ) -> Result<Vec<Document>, EmbeddingError> {
110 let chunks = self.split_text(&document.content).await?;
111 Ok(chunks
112 .into_iter()
113 .enumerate()
114 .map(|(i, chunk)| {
115 let mut metadata = document.metadata.clone();
116 metadata.insert("chunk".to_string(), i.to_string().into());
117 Document {
118 content: chunk,
119 metadata,
120 id: None,
121 }
122 })
123 .collect())
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use async_trait::async_trait;
131 use lc_embeddings::{EmbeddingError, Embeddings, MockEmbeddings};
132
133 struct FailingEmbeddings;
135 #[async_trait]
136 impl Embeddings for FailingEmbeddings {
137 async fn embed_query(&self, _text: &str) -> Result<Vec<f32>, EmbeddingError> {
138 Err(EmbeddingError::ApiError("intentional failure".to_string()))
139 }
140 fn dimension(&self) -> usize {
141 32
142 }
143 fn model_name(&self) -> &str {
144 "failing"
145 }
146 }
147
148 fn splitter(threshold: f32, max_chunk: usize) -> SemanticSplitter<MockEmbeddings> {
149 SemanticSplitter::new(MockEmbeddings::new(32), threshold, max_chunk)
150 }
151
152 #[tokio::test]
153 async fn test_empty_text() {
154 let s = splitter(0.5, 1000);
155 assert!(s.split_text("").await.unwrap().is_empty());
156 }
157
158 #[tokio::test]
159 async fn test_single_sentence() {
160 let s = splitter(0.5, 1000);
161 let chunks = s.split_text("只有一句没有标点").await.unwrap();
162 assert_eq!(chunks, vec!["只有一句没有标点".to_string()]);
163 }
164
165 #[tokio::test]
166 async fn test_chunks_contain_all_sentences() {
167 let s = splitter(0.5, 1000);
168 let text = "苹果是一种水果。香蕉是黄色的。樱桃很小。";
169 let chunks = s.split_text(text).await.unwrap();
170 assert!(!chunks.is_empty());
171 let joined = chunks.join("");
173 assert!(joined.contains("苹果是一种水果"));
174 assert!(joined.contains("香蕉是黄色的"));
175 assert!(joined.contains("樱桃很小"));
176 }
177
178 #[tokio::test]
179 async fn test_max_chunk_size_enforces_break() {
180 let s = splitter(0.0, 5);
182 let text = "AAAA。BBBB。CCCC。";
183 let chunks = s.split_text(text).await.unwrap();
184 assert!(
185 chunks.len() >= 2,
186 "max_chunk=5 应强制断块, 实际 {} 块",
187 chunks.len()
188 );
189 }
190
191 #[tokio::test]
192 async fn test_split_document_metadata() {
193 let s = splitter(0.0, 5);
194 let doc = Document::new("AAAA。BBBB。CCCC。").with_metadata("source", "test");
195 let chunks = s.split_document(&doc).await.unwrap();
196 assert!(chunks.len() >= 2);
197 for (i, c) in chunks.iter().enumerate() {
198 assert_eq!(
199 c.metadata.get("chunk"),
200 Some(&serde_json::Value::String(i.to_string()))
201 );
202 assert_eq!(
203 c.metadata.get("source"),
204 Some(&serde_json::Value::String("test".to_string()))
205 );
206 assert!(c.id.is_none());
207 }
208 }
209
210 #[tokio::test]
211 async fn test_embedding_failure_returns_error() {
212 let s = SemanticSplitter::new(FailingEmbeddings, 0.5, 1000);
214 let text = "句子一。句子二。句子三。";
215 let result = s.split_text(text).await;
216 assert!(result.is_err());
217 }
218}