知识库-向量化功能-文本文件向量化
一、核心逻辑
基于句子结束符的智能分片策略
- 按句子结束符(
。!?;\n\.!?;)分割文本,保证分片语义完整性; - 对每个文本分片独立向量化
- 分片后批量存储至Elasticsearch,同时保留原文档元数据与分片标识,便于后续检索溯源。
二、关键实现代码
2.1 文本向量化主方法
java
/**
* 处理文本分片与向量化,并批量存储至ES
* @param largeText 待处理的文本内容
* @param docMetadata 原文档元数据(如文件名称、上传时间、文件ID等)
* @return 分片ID列表(用于后续关联查询)
*/
public List<String> processLargeText(String largeText, Map<String,Object> docMetadata) {
// 1. 智能分片:按句子结束符分割,避免大文本一次性处理
List<String> chunks = textSplitter.split(largeText);
// 2. 构建分片Document,关联原文档元数据+分片信息+向量数据
List<Document> chunkDocs = chunks.stream()
.map(chunk -> {
// 生成分片唯一ID
String chunkId = UUID.randomUUID().toString().replace("-", "");
// 文本分片向量化(调用lrs33/bce-embedding-base_v1模型)
float[] embs = embeddingModel.embed(chunk);
// 分片元数据:继承原文档元数据 + 分片专属信息
Map<String, Object> chunkMetadata = new HashMap<>(docMetadata);
chunkMetadata.put("chunkId", chunkId); // 分片唯一标识
chunkMetadata.put("chunkIndex", chunks.indexOf(chunk)); // 分片序号
chunkMetadata.put("totalChunks", chunks.size()); // 总分片数
chunkMetadata.put("overlapLength", TextSplitter.CHUNK_OVERLAP); // 分片重叠长度
chunkMetadata.put("vector", embs); // 向量数据
return new Document(chunkId, chunk, chunkMetadata);
}).collect(Collectors.toList());
// 3. 批量存储分片至ES向量库(自动适配配置的bulk-size)
compatibleVectorStoreService.batchStore(chunkDocs);
// 返回所有分片ID,便于后续溯源/删除操作
return chunkDocs.stream().map(Document::getId).collect(Collectors.toList());
}
2.2 智能文本分片工具类
java
/**
* 文本分片工具类:按句子结束符分割,保证语义完整性,避免大文本卡死
*/
public class TextSplitter {
// 分片最大长度(可根据业务调整,建议500-1000字符)
public static final int MAX_CHUNK_LENGTH = 1000;
// 分片重叠长度(避免句子被截断导致语义丢失,建议50-100字符)
public static final int CHUNK_OVERLAP = 50;
/**
* 智能分割大文本
* @param text 待分割的大文本
* @return 语义完整的文本分片列表
*/
public List<String> split(String text) {
List<String> chunks = new ArrayList<>();
int textLength = text.length();
int start = 0;
// 循环分片,直到处理完所有文本
while (start < textLength) {
// 1. 计算分片初始结束位置(不超过最大长度)
int end = Math.min(start + MAX_CHUNK_LENGTH, textLength);
// 2. 若未到文本末尾,回溯至最近的句子结束符,保证分片语义完整
if (end < textLength) {
String subText = text.substring(start, end);
int lastEndPos = findLastSentenceEnd(subText);
// 找到句子结束符:调整结束位置至结束符后
if (lastEndPos != -1) {
end = start + lastEndPos + 1;
} else {
// 未找到结束符:兜底按最大长度分割(避免无限循环)
end = start + MAX_CHUNK_LENGTH;
}
}
// 3. 截取分片并去除首尾空白,非空则加入列表
String chunk = text.substring(start, end).trim();
if (!chunk.isEmpty()) {
chunks.add(chunk);
}
// 4. 计算下一个分片起始位置(重叠处理,避免语义断裂)
start = end - CHUNK_OVERLAP;
// 防止重叠后起始位置为负数(仅首次分片可能触发)
if (start < 0) {
start = 0;
}
}
return chunks;
}
/**
* 查找子文本中最后一个句子结束符的位置
* 结束符:。!?;\n\.!?;
* @param subText 子文本
* @return 最后一个结束符的索引(无则返回-1)
*/
private int findLastSentenceEnd(String subText) {
// 定义句子结束符正则
String endChars = "。!?;\n\\.!?;";
for (int i = subText.length() - 1; i >= 0; i--) {
if (endChars.indexOf(subText.charAt(i)) != -1) {
return i;
}
}
return -1;
}
}
三、核心设计说明
3.1 分片策略优势
| 设计点 | 解决的问题 |
|---|---|
| 按句子结束符分割 | 避免生硬截断文本导致的语义破碎,保证分片语义完整性 |
| 分片重叠处理(CHUNK_OVERLAP) | 解决跨分片句子语义断裂问题(如分片1末尾与分片2开头重叠50字符) |
| 兜底按最大长度分割 | 避免无结束符的超长文本(如纯数字/英文)导致循环无法终止 |
| 非空分片过滤 | 避免空字符串分片占用ES存储空间 |
3.2 关键参数说明
| 参数名 | 默认值 | 调整建议 |
|---|---|---|
| MAX_CHUNK_LENGTH | 1000 | 可根据模型上下文窗口(num_ctx=1024)调整,建议不超过1000字符,避免模型处理超时 |
| CHUNK_OVERLAP | 50 | 建议为MAX_CHUNK_LENGTH的5%-10%,过小易丢失跨分片语义,过大则增加冗余存储 |
3.3 元数据设计
分片元数据同时包含原文档属性 和分片专属属性,便于:
- 检索时溯源至原文件(如通过文件ID关联);
- 查看分片在原文本中的位置(chunkIndex);
- 统计原文本的总分片数(totalChunks);
- 快速定位向量数据(vector字段)。
四、性能优化说明
- 避免大文本一次性处理:通过循环分片+逐片向量化,降低单次内存占用,解决split方法卡死问题;
- 批量写入ES :结合配置的
bulk-size,减少ES网络请求次数,提升写入效率; - 分片去空:过滤空白分片,减少无效数据存储和向量计算开销;
- 索引刷新间隔 :配合ES配置的
refresh-interval,避免高频刷盘导致的性能损耗。
五、注意事项
- 句子结束符正则需覆盖中英文场景(
。!?;\n\.!?;),避免遗漏分割点; - 向量化时需保证
embeddingModel与配置的lrs33/bce-embedding-base_v1模型一致,否则向量维度不匹配; - 分片ID采用UUID生成,避免重复;若需关联原文件,可在元数据中增加
fileId等标识; - 处理长篇文本时,建议增加异步处理逻辑,避免阻塞主线程。
- 一次性传入超长文本容易使split方法卡死,建议提前将文本分割后再使用