知识库-向量化功能-流式分片
一、设计背景
针对超大文本(如100MB以上纯文本文件),传统“一次性加载全部文本到内存再分片”的方式易导致内存溢出、方法卡死等问题。因此采用流式分片策略:逐批次读取文本到缓冲区,按需生成分片,全程不加载完整文本到内存,大幅降低内存占用。
二、核心逻辑
- 流式读取:通过
Reader逐缓冲区读取文本,避免一次性加载全量内容;
- 批次分片:对每批次读取的文本按句子结束符分割,保证分片语义完整性;
- 兜底机制:设置最小递增步长、最大循环次数,避免死循环;
- 重叠处理:分片间保留重叠字符,防止跨批次句子语义断裂;
- 剩余文本处理:合并未处理完的文本到下一批次,保证分片完整性。
三、核心实现代码
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
public class StreamTextSplitter {
private static final Pattern SENTENCE_END_PATTERN = Pattern.compile("[。!?;\\n\\.!?;]");
private static final int MAX_CHUNK_LENGTH = 1000;
public static final int CHUNK_OVERLAP = 100;
private static final int MIN_STEP = MAX_CHUNK_LENGTH / 2;
private static final int MAX_LOOP_COUNT = 100;
public Iterator<String> streamSplitText(Reader textReader, int bufferSize) throws IOException {
List<String> results = new ArrayList<>();
List<String> currentBatchChunks = new ArrayList<>();
char[] buffer = new char[bufferSize];
int readLen;
String remainingText = "";
while ((readLen = textReader.read(buffer)) != -1) {
String batchText = remainingText + new String(buffer, 0, readLen);
List<String> batchChunks = splitBatchText(batchText);
if (!batchChunks.isEmpty()) {
String lastChunk = batchChunks.get(batchChunks.size() - 1);
if (lastChunk.length() < MAX_CHUNK_LENGTH / 2) {
remainingText = lastChunk;
currentBatchChunks.addAll(batchChunks.subList(0, batchChunks.size() - 1));
} else {
remainingText = "";
currentBatchChunks.addAll(batchChunks);
}
}
results.addAll(currentBatchChunks);
currentBatchChunks.clear();
}
if (!remainingText.isEmpty()) {
String lastChunk = remainingText.trim();
if (!lastChunk.isEmpty()) {
results.add(lastChunk);
}
}
return results.iterator();
}
private List<String> splitBatchText(String text) {
List<String> chunks = new ArrayList<>();
int textLength = text.length();
if (textLength == 0) {
return chunks;
}
int start = 0;
int loopCount = 0;
while (start < textLength && loopCount < MAX_LOOP_COUNT) {
loopCount++;
int end = Math.min(start + MAX_CHUNK_LENGTH, textLength);
end = adjustToSentenceEnd(text, start, end);
String chunk = text.substring(start, end).trim();
if (!chunk.isEmpty()) {
chunks.add(chunk);
}
int nextStart = end - CHUNK_OVERLAP;
nextStart = Math.max(nextStart, start + MIN_STEP);
nextStart = Math.min(nextStart, textLength);
if (nextStart <= start) {
break;
}
start = nextStart;
}
if (start < textLength) {
String lastChunk = text.substring(start).trim();
if (!lastChunk.isEmpty()) {
chunks.add(lastChunk);
}
}
if (loopCount >= MAX_LOOP_COUNT) {
System.err.printf("批次文本分片循环次数达上限(%d次),可能存在异常文本格式!文本长度:%d%n", MAX_LOOP_COUNT, textLength);
}
return chunks;
}
private int adjustToSentenceEnd(String text, int start, int end) {
if (end >= text.length()) {
return end;
}
String subText = text.substring(start, end);
int lastEndPos = findLastSentenceEnd(subText);
int minValidLength = 50;
if (lastEndPos != -1) {
int adjustedEnd = start + lastEndPos + 1;
if (adjustedEnd - start >= minValidLength) {
return adjustedEnd;
}
}
return end;
}
private int findLastSentenceEnd(String text) {
for (int i = text.length() - 1; i >= 0; i--) {
if (SENTENCE_END_PATTERN.matcher(String.valueOf(text.charAt(i))).matches()) {
return i;
}
}
return -1;
}
}
四、使用方法
4.1 基础使用(读取本地超大文本文件)
import java.io.FileReader;
import java.io.Reader;
import java.util.Iterator;
public class StreamSplitterDemo {
public static void main(String[] args) {
StreamTextSplitter splitter = new StreamTextSplitter();
String filePath = "D:/large_text.txt";
int bufferSize = 8192;
try (Reader textReader = new FileReader(filePath)) {
Iterator<String> chunkIterator = splitter.streamSplitText(textReader, bufferSize);
int chunkIndex = 0;
while (chunkIterator.hasNext()) {
String chunk = chunkIterator.next();
chunkIndex++;
System.out.printf("分片%d:长度=%d,内容预览:%s%n",
chunkIndex,
chunk.length(),
chunk.substring(0, Math.min(chunk.length(), 50)));
}
System.out.printf("分片完成,共生成%d个分片%n", chunkIndex);
} catch (Exception e) {
System.err.println("流式分片失败:" + e.getMessage());
e.printStackTrace();
}
}
}
4.2 结合文件上传场景(InputStreamReader)
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public void processUploadedLargeFile(MultipartFile file) throws Exception {
StreamTextSplitter splitter = new StreamTextSplitter();
int bufferSize = 16384;
try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) {
Iterator<String> chunkIterator = splitter.streamSplitText(reader, bufferSize);
while (chunkIterator.hasNext()) {
String chunk = chunkIterator.next();
}
}
}
4.3 结合PDF/Word解析(流式处理解析结果)
public void streamProcessPdf(String pdfFilePath) throws Exception {
String pdfText = PdfTextExtractor.getContent(pdfFilePath);
try (Reader reader = new java.io.StringReader(pdfText)) {
StreamTextSplitter splitter = new StreamTextSplitter();
Iterator<String> chunkIterator = splitter.streamSplitText(reader, 8192);
while (chunkIterator.hasNext()) {
String chunk = chunkIterator.next();
}
}
}
五、核心设计说明
5.1 关键参数说明
| 参数名 | 取值 | 作用 |
|---|
| MAX_CHUNK_LENGTH | 1000 | 单分片最大字符数,需匹配嵌入模型上下文窗口(如num_ctx=1024) |
| CHUNK_OVERLAP | 100 | 分片间重叠字符数,避免跨分片句子语义断裂(如“我是一个测试文本”被截断为“我是一个”和“测试文本”) |
| MIN_STEP | 500 | 最小递增步长,防止因重叠导致start位置不递增,触发死循环 |
| MAX_LOOP_COUNT | 100 | 最大循环次数,兜底处理异常文本(如无句子结束符的超长文本) |
| bufferSize | 8192/16384 | 缓冲区大小,建议为2的幂次,平衡IO次数与内存占用 |
5.2 核心优势
| 优势 | 解决的问题 |
|---|
| 流式读取 | 避免一次性加载超大文本到内存,防止OOM/方法卡死 |
| 句子结束符分割 | 保证分片语义完整性,避免生硬截断导致的向量化误差 |
| 剩余文本合并 | 防止跨批次文本丢失,保证分片完整性 |
| 多重兜底机制 | 最小步长+最大循环次数,彻底避免死循环 |
| 迭代器返回 | 支持按需遍历,分片处理完成后立即释放内存 |
六、注意事项
- 编码一致性:创建
Reader时需指定编码(如UTF-8),避免不同编码导致的字符解析错误;
- 缓冲区大小调整:小文件(<1MB)建议用8192,超大文件(>100MB)建议用16384/32768,减少IO次数;
- 异常文本处理:若文本无任何句子结束符(如纯数字/乱码),会触发兜底按最大长度分割,需在日志中监控此类情况;
- 资源释放:必须通过
try-with-resources关闭Reader,避免文件句柄泄漏;
- 性能优化:分片处理(向量化/存储)建议结合线程池异步执行,提升整体效率;
- 内存监控:处理GB级文本时,建议监控JVM堆内存,避免缓冲区+分片结果集占用过多内存;
- 特殊字符处理:若文本包含大量特殊符号(如\t/\r),需在分片前先格式化清理。
七、扩展建议
- 自定义结束符:开放
SENTENCE_END_PATTERN配置,支持业务自定义句子结束符;
- 分片过滤:增加空分片/短分片过滤逻辑(如过滤长度<50的分片);
- 进度回调:增加分片进度回调函数,便于前端展示处理进度;
- 批量写入:结合ES的bulk写入,积累一定数量分片后批量存储,减少网络请求;
- 超时控制:为分片处理增加超时机制,避免单个分片处理耗时过长;
- 监控指标:统计分片总数、平均分片长度、处理耗时,便于性能调优;
- 自适应缓冲区:根据文本读取速度动态调整缓冲区大小,平衡性能与内存。