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 {
57 Self {
58 chunk_size,
59 chunk_overlap,
60 separators: vec![
61 "\n\n".to_string(), "\n".to_string(), "。".to_string(), ".".to_string(), " ".to_string(), "".to_string(), ],
68 }
69 }
70
71 pub fn with_defaults() -> Self {
73 Self::new(1000, 200)
74 }
75
76 pub fn with_separators(mut self, separators: Vec<String>) -> Self {
78 self.separators = separators;
79 self
80 }
81
82 fn split_text_recursive(&self, text: &str, separators: &[String]) -> Vec<String> {
84 let mut chunks = Vec::new();
85
86 if text.is_empty() {
87 return chunks;
88 }
89
90 if text.chars().count() <= self.chunk_size {
92 chunks.push(text.to_string());
93 return chunks;
94 }
95
96 let separator = separators
98 .iter()
99 .find(|s| text.contains(s.as_str()))
100 .cloned()
101 .unwrap_or_default();
102
103 let splits: Vec<String> = if separator.is_empty() {
105 text.chars().map(|c| c.to_string()).collect()
106 } else {
107 text.split(&separator).map(|s| s.to_string()).collect()
108 };
109
110 let mut current_chunk = String::new();
112
113 for split in splits {
114 let split_with_sep = if separator.is_empty() {
115 split.clone()
116 } else if current_chunk.is_empty() {
117 split
118 } else {
119 format!("{}{}", separator, split)
120 };
121
122 if split_with_sep.chars().count() > self.chunk_size {
124 if !current_chunk.is_empty() {
126 chunks.push(current_chunk.clone());
127 current_chunk.clear();
128 }
129
130 let next_separators = if separators.len() > 1 {
132 &separators[1..]
133 } else {
134 &[]
135 };
136
137 let sub_chunks = self.split_text_recursive(&split_with_sep, next_separators);
138 chunks.extend(sub_chunks);
139 } else if current_chunk.chars().count() + split_with_sep.chars().count()
140 > self.chunk_size
141 {
142 chunks.push(current_chunk.clone());
144 current_chunk = split_with_sep;
145 } else {
146 current_chunk.push_str(&split_with_sep);
147 }
148 }
149
150 if !current_chunk.is_empty() {
151 chunks.push(current_chunk);
152 }
153
154 chunks
155 }
156}
157
158impl TextSplitter for RecursiveCharacterSplitter {
159 fn split_text(&self, text: &str) -> Vec<String> {
160 let mut chunks = self.split_text_recursive(text, &self.separators);
161
162 if self.chunk_overlap > 0 && chunks.len() > 1 {
164 let mut overlapped = Vec::new();
165
166 for (i, chunk) in chunks.into_iter().enumerate() {
167 if i == 0 {
168 overlapped.push(chunk);
169 } else {
170 let prev = &overlapped[i - 1];
172 let chars: Vec<char> = prev.chars().collect();
173 let overlap_chars = chars.len().saturating_sub(self.chunk_overlap);
174 let mut overlap: String = chars[overlap_chars..].iter().collect();
175
176 let budget = self.chunk_size.saturating_sub(chunk.chars().count());
181 if overlap.chars().count() > budget {
182 let ov_chars: Vec<char> = overlap.chars().collect();
183 overlap = ov_chars[ov_chars.len().saturating_sub(budget)..]
184 .iter()
185 .collect();
186 }
187
188 overlapped.push(format!("{}{}", overlap, chunk));
189 }
190 }
191
192 chunks = overlapped;
193 }
194
195 chunks
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_recursive_splitter() {
205 let splitter = RecursiveCharacterSplitter::new(50, 10);
206
207 let text = "This is a sentence. This is another sentence. And a third one.";
208 let chunks = splitter.split_text(text);
209
210 assert!(!chunks.is_empty());
211 for chunk in &chunks {
213 assert!(chunk.chars().count() <= 50);
214 }
215 }
216
217 #[test]
218 fn test_split_document() {
219 let splitter = RecursiveCharacterSplitter::new(100, 20);
220
221 let doc = Document::new("First paragraph.\n\nSecond paragraph.\n\nThird paragraph.")
222 .with_metadata("source", "test");
223
224 let chunks = splitter.split_document(&doc);
225
226 assert!(!chunks.is_empty());
227 for (i, chunk) in chunks.iter().enumerate() {
228 assert!(chunk.metadata.contains_key("chunk"));
229 assert_eq!(
230 chunk.metadata.get("chunk"),
231 Some(&Value::String(i.to_string()))
232 );
233 assert_eq!(
234 chunk.metadata.get("source"),
235 Some(&Value::String("test".to_string()))
236 );
237 }
238 }
239
240 #[test]
241 fn test_split_document_preserves_user_chunk_key() {
242 let splitter = RecursiveCharacterSplitter::new(20, 5);
243
244 let doc = Document::new("This is a longer paragraph that gets split into several chunks.")
246 .with_metadata("chunk", "user-supplied");
247
248 let chunks = splitter.split_document(&doc);
249
250 assert!(!chunks.is_empty());
251 for chunk in &chunks {
252 assert_eq!(
253 chunk.metadata.get("chunk"),
254 Some(&Value::String("user-supplied".to_string()))
255 );
256 }
257 }
258
259 #[test]
260 fn test_empty_text() {
261 let splitter = RecursiveCharacterSplitter::new(100, 20);
262 let chunks = splitter.split_text("");
263 assert!(chunks.is_empty());
264 }
265
266 #[test]
267 fn test_small_text() {
268 let splitter = RecursiveCharacterSplitter::new(1000, 200);
269 let text = "Short text";
270 let chunks = splitter.split_text(text);
271 assert_eq!(chunks.len(), 1);
272 assert_eq!(chunks[0], "Short text");
273 }
274}