lc_shared/
splitter_types.rs1use crate::document_types::Document;
9
10pub trait TextSplitter: Send + Sync {
12 fn split_text(&self, text: &str) -> Vec<String>;
14
15 fn split_document(&self, document: &Document) -> Vec<Document> {
17 let chunks = self.split_text(&document.content);
18 chunks
19 .into_iter()
20 .enumerate()
21 .map(|(i, chunk)| {
22 let mut metadata = document.metadata.clone();
23 metadata.entry("chunk".to_string()).or_insert(i.to_string());
26
27 Document {
28 content: chunk,
29 metadata,
30 id: None,
31 }
32 })
33 .collect()
34 }
35}
36
37pub struct RecursiveCharacterSplitter {
41 chunk_size: usize,
43
44 chunk_overlap: usize,
46
47 separators: Vec<String>,
49}
50
51impl RecursiveCharacterSplitter {
52 pub fn new(chunk_size: usize, chunk_overlap: usize) -> Self {
54 Self {
55 chunk_size,
56 chunk_overlap,
57 separators: vec![
58 "\n\n".to_string(), "\n".to_string(), "。".to_string(), ".".to_string(), " ".to_string(), "".to_string(), ],
65 }
66 }
67
68 pub fn with_defaults() -> Self {
70 Self::new(1000, 200)
71 }
72
73 pub fn with_separators(mut self, separators: Vec<String>) -> Self {
75 self.separators = separators;
76 self
77 }
78
79 fn split_text_recursive(&self, text: &str, separators: &[String]) -> Vec<String> {
81 let mut chunks = Vec::new();
82
83 if text.is_empty() {
84 return chunks;
85 }
86
87 if text.chars().count() <= self.chunk_size {
89 chunks.push(text.to_string());
90 return chunks;
91 }
92
93 let separator = separators
95 .iter()
96 .find(|s| text.contains(s.as_str()))
97 .cloned()
98 .unwrap_or_default();
99
100 let splits: Vec<String> = if separator.is_empty() {
102 text.chars().map(|c| c.to_string()).collect()
103 } else {
104 text.split(&separator).map(|s| s.to_string()).collect()
105 };
106
107 let mut current_chunk = String::new();
109
110 for split in splits {
111 let split_with_sep = if separator.is_empty() {
112 split.clone()
113 } else if current_chunk.is_empty() {
114 split
115 } else {
116 format!("{}{}", separator, split)
117 };
118
119 if split_with_sep.chars().count() > self.chunk_size {
121 if !current_chunk.is_empty() {
123 chunks.push(current_chunk.clone());
124 current_chunk.clear();
125 }
126
127 let next_separators = if separators.len() > 1 {
129 &separators[1..]
130 } else {
131 &[]
132 };
133
134 let sub_chunks = self.split_text_recursive(&split_with_sep, next_separators);
135 chunks.extend(sub_chunks);
136 } else if current_chunk.chars().count() + split_with_sep.chars().count()
137 > self.chunk_size
138 {
139 chunks.push(current_chunk.clone());
141 current_chunk = split_with_sep;
142 } else {
143 current_chunk.push_str(&split_with_sep);
144 }
145 }
146
147 if !current_chunk.is_empty() {
148 chunks.push(current_chunk);
149 }
150
151 chunks
152 }
153}
154
155impl TextSplitter for RecursiveCharacterSplitter {
156 fn split_text(&self, text: &str) -> Vec<String> {
157 let mut chunks = self.split_text_recursive(text, &self.separators);
158
159 if self.chunk_overlap > 0 && chunks.len() > 1 {
161 let mut overlapped = Vec::new();
162
163 for (i, chunk) in chunks.into_iter().enumerate() {
164 if i == 0 {
165 overlapped.push(chunk);
166 } else {
167 let prev = &overlapped[i - 1];
169 let chars: Vec<char> = prev.chars().collect();
170 let overlap_chars = chars.len().saturating_sub(self.chunk_overlap);
171 let mut overlap: String = chars[overlap_chars..].iter().collect();
172
173 let budget = self.chunk_size.saturating_sub(chunk.chars().count());
178 if overlap.chars().count() > budget {
179 let ov_chars: Vec<char> = overlap.chars().collect();
180 overlap = ov_chars[ov_chars.len().saturating_sub(budget)..]
181 .iter()
182 .collect();
183 }
184
185 overlapped.push(format!("{}{}", overlap, chunk));
186 }
187 }
188
189 chunks = overlapped;
190 }
191
192 chunks
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_recursive_splitter() {
202 let splitter = RecursiveCharacterSplitter::new(50, 10);
203
204 let text = "This is a sentence. This is another sentence. And a third one.";
205 let chunks = splitter.split_text(text);
206
207 assert!(!chunks.is_empty());
208 for chunk in &chunks {
210 assert!(chunk.chars().count() <= 50);
211 }
212 }
213
214 #[test]
215 fn test_split_document() {
216 let splitter = RecursiveCharacterSplitter::new(100, 20);
217
218 let doc = Document::new("First paragraph.\n\nSecond paragraph.\n\nThird paragraph.")
219 .with_metadata("source", "test");
220
221 let chunks = splitter.split_document(&doc);
222
223 assert!(!chunks.is_empty());
224 for (i, chunk) in chunks.iter().enumerate() {
225 assert!(chunk.metadata.contains_key("chunk"));
226 assert_eq!(chunk.metadata.get("chunk"), Some(&i.to_string()));
227 assert_eq!(chunk.metadata.get("source"), Some(&"test".to_string()));
228 }
229 }
230
231 #[test]
232 fn test_split_document_preserves_user_chunk_key() {
233 let splitter = RecursiveCharacterSplitter::new(20, 5);
234
235 let doc = Document::new("This is a longer paragraph that gets split into several chunks.")
237 .with_metadata("chunk", "user-supplied");
238
239 let chunks = splitter.split_document(&doc);
240
241 assert!(!chunks.is_empty());
242 for chunk in &chunks {
243 assert_eq!(
244 chunk.metadata.get("chunk"),
245 Some(&"user-supplied".to_string())
246 );
247 }
248 }
249
250 #[test]
251 fn test_empty_text() {
252 let splitter = RecursiveCharacterSplitter::new(100, 20);
253 let chunks = splitter.split_text("");
254 assert!(chunks.is_empty());
255 }
256
257 #[test]
258 fn test_small_text() {
259 let splitter = RecursiveCharacterSplitter::new(1000, 200);
260 let text = "Short text";
261 let chunks = splitter.split_text(text);
262 assert_eq!(chunks.len(), 1);
263 assert_eq!(chunks[0], "Short text");
264 }
265}