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