lc_shared/
splitter_types.rs1use crate::document_types::Document;
9use serde_json::Value;
10
11pub trait TextSplitter: Send + Sync {
13 fn split_text(&self, text: &str) -> Vec<String>;
15
16 fn split_document(&self, document: &Document) -> Vec<Document> {
18 let chunks = self.split_text(&document.content);
19 chunks
20 .into_iter()
21 .enumerate()
22 .map(|(i, chunk)| {
23 let mut metadata = document.metadata.clone();
24 metadata
27 .entry("chunk".to_string())
28 .or_insert(Value::String(i.to_string()));
29
30 Document {
31 content: chunk,
32 metadata,
33 id: None,
34 }
35 })
36 .collect()
37 }
38}
39
40pub struct RecursiveCharacterSplitter {
44 chunk_size: usize,
46
47 chunk_overlap: usize,
49
50 separators: Vec<String>,
52}
53
54impl RecursiveCharacterSplitter {
55 pub fn new(chunk_size: usize, chunk_overlap: usize) -> Self {
62 Self {
63 chunk_size: chunk_size.max(1),
64 chunk_overlap,
65 separators: vec![
66 "\n\n".to_string(), "\n".to_string(), "。".to_string(), ".".to_string(), " ".to_string(), "".to_string(), ],
73 }
74 }
75
76 pub fn with_defaults() -> Self {
78 Self::new(1000, 200)
79 }
80
81 pub fn with_separators(mut self, separators: Vec<String>) -> Self {
83 self.separators = separators;
84 self
85 }
86
87 fn split_text_recursive(&self, text: &str, separators: &[String]) -> Vec<String> {
89 let mut chunks = Vec::new();
90
91 if text.is_empty() {
92 return chunks;
93 }
94
95 if text.chars().count() <= self.chunk_size {
97 chunks.push(text.to_string());
98 return chunks;
99 }
100
101 let separator = separators
103 .iter()
104 .find(|s| text.contains(s.as_str()))
105 .cloned()
106 .unwrap_or_default();
107
108 let splits: Vec<String> = if separator.is_empty() {
110 text.chars().map(|c| c.to_string()).collect()
111 } else {
112 text.split(&separator).map(|s| s.to_string()).collect()
113 };
114
115 let mut current_chunk = String::new();
117
118 for split in splits {
119 let split_with_sep = if separator.is_empty() {
120 split.clone()
121 } else if current_chunk.is_empty() {
122 split
123 } else {
124 format!("{}{}", separator, split)
125 };
126
127 if split_with_sep.chars().count() > self.chunk_size {
129 if !current_chunk.is_empty() {
131 chunks.push(current_chunk.clone());
132 current_chunk.clear();
133 }
134
135 let next_separators = if separators.len() > 1 {
137 &separators[1..]
138 } else {
139 &[]
140 };
141
142 if next_separators.is_empty() {
148 for c in split_with_sep.chars() {
149 chunks.push(c.to_string());
150 }
151 } else {
152 let sub_chunks = self.split_text_recursive(&split_with_sep, next_separators);
153 chunks.extend(sub_chunks);
154 }
155 } else if current_chunk.chars().count() + split_with_sep.chars().count()
156 > self.chunk_size
157 {
158 chunks.push(current_chunk.clone());
160 current_chunk = split_with_sep;
161 } else {
162 current_chunk.push_str(&split_with_sep);
163 }
164 }
165
166 if !current_chunk.is_empty() {
167 chunks.push(current_chunk);
168 }
169
170 chunks
171 }
172}
173
174impl TextSplitter for RecursiveCharacterSplitter {
175 fn split_text(&self, text: &str) -> Vec<String> {
176 let mut chunks = self.split_text_recursive(text, &self.separators);
177
178 if self.chunk_overlap > 0 && chunks.len() > 1 {
180 let mut overlapped = Vec::new();
181
182 for (i, chunk) in chunks.into_iter().enumerate() {
183 if i == 0 {
184 overlapped.push(chunk);
185 } else {
186 let prev = &overlapped[i - 1];
188 let chars: Vec<char> = prev.chars().collect();
189 let overlap_chars = chars.len().saturating_sub(self.chunk_overlap);
190 let mut overlap: String = chars[overlap_chars..].iter().collect();
191
192 let budget = self.chunk_size.saturating_sub(chunk.chars().count());
197 if overlap.chars().count() > budget {
198 let ov_chars: Vec<char> = overlap.chars().collect();
199 overlap = ov_chars[ov_chars.len().saturating_sub(budget)..]
200 .iter()
201 .collect();
202 }
203
204 overlapped.push(format!("{}{}", overlap, chunk));
205 }
206 }
207
208 chunks = overlapped;
209 }
210
211 chunks
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_recursive_splitter() {
221 let splitter = RecursiveCharacterSplitter::new(50, 10);
222
223 let text = "This is a sentence. This is another sentence. And a third one.";
224 let chunks = splitter.split_text(text);
225
226 assert!(!chunks.is_empty());
227 for chunk in &chunks {
229 assert!(chunk.chars().count() <= 50);
230 }
231 }
232
233 #[test]
234 fn test_split_document() {
235 let splitter = RecursiveCharacterSplitter::new(100, 20);
236
237 let doc = Document::new("First paragraph.\n\nSecond paragraph.\n\nThird paragraph.")
238 .with_metadata("source", "test");
239
240 let chunks = splitter.split_document(&doc);
241
242 assert!(!chunks.is_empty());
243 for (i, chunk) in chunks.iter().enumerate() {
244 assert!(chunk.metadata.contains_key("chunk"));
245 assert_eq!(
246 chunk.metadata.get("chunk"),
247 Some(&Value::String(i.to_string()))
248 );
249 assert_eq!(
250 chunk.metadata.get("source"),
251 Some(&Value::String("test".to_string()))
252 );
253 }
254 }
255
256 #[test]
257 fn test_split_document_preserves_user_chunk_key() {
258 let splitter = RecursiveCharacterSplitter::new(20, 5);
259
260 let doc = Document::new("This is a longer paragraph that gets split into several chunks.")
262 .with_metadata("chunk", "user-supplied");
263
264 let chunks = splitter.split_document(&doc);
265
266 assert!(!chunks.is_empty());
267 for chunk in &chunks {
268 assert_eq!(
269 chunk.metadata.get("chunk"),
270 Some(&Value::String("user-supplied".to_string()))
271 );
272 }
273 }
274
275 #[test]
276 fn test_empty_text() {
277 let splitter = RecursiveCharacterSplitter::new(100, 20);
278 let chunks = splitter.split_text("");
279 assert!(chunks.is_empty());
280 }
281
282 #[test]
283 fn test_small_text() {
284 let splitter = RecursiveCharacterSplitter::new(1000, 200);
285 let text = "Short text";
286 let chunks = splitter.split_text(text);
287 assert_eq!(chunks.len(), 1);
288 assert_eq!(chunks[0], "Short text");
289 }
290
291 #[test]
292 fn test_chunk_size_zero_does_not_stack_overflow() {
293 let splitter = RecursiveCharacterSplitter::new(0, 0);
296 let chunks = splitter.split_text("This text is long enough to need splitting");
297
298 assert!(!chunks.is_empty());
299 for chunk in &chunks {
301 assert!(chunk.chars().count() <= 1);
302 }
303 }
304}