知识库-向量化功能-EXCEL文件向量化
一、功能概述
基于Alibaba EasyExcel 实现Excel文件全量解析,适配 .xls/.xlsx 双格式,核心流程为:解析Excel单元格数据 → 按行合并生成结构化文本块 → 文本向量化 → 批量存储至Elasticsearch向量库。 ✅ 核心特性:
- 自动跳过空单元格,过滤无效数据;
- 保留完整元数据(工作表名、行号、列号),支持检索后溯源;
- 按行聚合文本,保证单行列数据的语义关联性;
- 批量向量化+批量写入ES,兼顾效率与内存安全。
二、技术依赖(Maven)
xml
<!-- Excel文件解析核心依赖(适配.xls/.xlsx,无POI版本冲突) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
✅ 说明:EasyExcel 3.3.2 已内置适配的POI依赖,无需额外引入POI包,彻底解决传统POI解析Excel的内存溢出、版本冲突问题。
三、核心数据模型定义
3.1 单元格数据实体(解析层)
封装Excel单元格原始数据+完整元数据,用于解析结果暂存与数据流转
java
import lombok.Data;
/**
* Excel单元格数据实体
* 存储单个单元格的内容+全量元数据,支持检索溯源
*/
@Data
public class ExcelCellData {
/** 工作表名称(如:Sheet1、用户信息表) */
private String sheetName;
/** 行号(从1开始,与Excel视觉行号一致) */
private Integer rowNum;
/** 列号(从1开始,与Excel视觉列号一致) */
private Integer colNum;
/** 单元格有效值(已去空、去首尾空格) */
private String cellValue;
}
3.2 文本块实体(分片层)
按工作表+行聚合后的结构化文本块,作为向量化的入参载体
java
import lombok.Data;
import java.util.Map;
/**
* Excel行级文本块实体
* 一行所有单元格数据聚合后的结构化文本,用于向量化
*/
@Data
public class TextChunk {
/** 行级聚合文本内容(结构化格式,提升向量化准确性) */
private String content;
/** 元数据(工作表名、行号、文件类型,用于ES存储/检索溯源) */
private Map<String, Object> metadata;
}
3.3 向量块实体(向量化层)
封装「文本+元数据+向量」的完整数据,作为写入ES的前置载体
java
import lombok.Data;
import java.util.Map;
/**
* 向量块实体
* 文本块+向量数据的组合体,用于批量写入ES
*/
@Data
public class VectorChunk {
/** 行级聚合文本内容 */
private String content;
/** 元数据(继承自TextChunk) */
private Map<String, Object> metadata;
/** 文本对应的向量数据(768维度,匹配bce-embedding-base_v1模型) */
private float[] vector;
}
四、核心实现代码(完整四层流程)
第一层:Excel文件解析工具类(通用适配)
封装通用解析方法,支持「指定表头行数/默认无表头」双模式,自动适配所有Excel格式,无内存溢出风险
java
import com.alibaba.excel.EasyExcel;
import java.io.File;
import java.util.List;
/**
* Excel通用解析工具类
* ✅ 适配.xls/.xlsx | ✅ 支持表头配置 | ✅ 自动过滤空单元格 | ✅ 无内存溢出
*/
public class ExcelParserUtil {
/**
* 通用Excel解析方法(推荐)
* @param excelFile 待解析的Excel文件
* @param headRowNumber 表头行数【核心】:有表头填1,纯数据无表头填0
* @return 所有有效单元格数据列表(已过滤空值)
*/
public static List<ExcelCellData> parseExcel(File excelFile, int headRowNumber) {
ExcelAnalysisListener listener = new ExcelAnalysisListener();
// EasyExcel 3.x 标准写法,一行代码完成全表读取
EasyExcel.read(excelFile)
.registerReadListener(listener) // 注册监听器接收解析数据
.headRowNumber(headRowNumber) // 指定表头行数,避免表头混入数据
.doReadAll(); // 读取Excel中所有工作表
return listener.getCellDataList();
}
/**
* 重载方法:默认无表头(headRowNumber=0),简化无表头Excel调用
*/
public static List<ExcelCellData> parseExcel(File excelFile) {
return parseExcel(excelFile, 0);
}
}
第二层:Excel解析监听器(核心回调)
EasyExcel专用监听器,逐行读取Excel数据、过滤空单元格、封装单元格实体,是解析效率与数据质量的核心保障
java
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Excel解析监听器(EasyExcel核心回调类)
* ✅ 逐行解析 | ✅ 过滤空单元格 | ✅ 封装ExcelCellData | ✅ 记录工作表元数据
*/
public class ExcelAnalysisListener extends AnalysisEventListener<Map<String, Object>> {
// 存储所有有效单元格数据
private final List<ExcelCellData> cellDataList = new ArrayList<>();
// 当前解析的工作表名称
private String currentSheetName;
/**
* 核心回调:逐行解析Excel数据(每行触发一次)
*/
@Override
public void invoke(Map<String, Object> rowDataMap, AnalysisContext context) {
// 1. 获取元数据:工作表名、当前行号(+1 匹配Excel视觉行号)
currentSheetName = context.readSheetHolder().getSheetName();
int rowNum = context.readRowHolder().getRowIndex() + 1;
// 2. 遍历当前行所有单元格,过滤空值并封装实体
int colNum = 1; // 列号从1开始计数,符合使用习惯
for (Map.Entry<String, Object> entry : rowDataMap.entrySet()) {
Object cellValueObj = entry.getValue();
// 跳过空单元格,避免无效数据占用内存
if (cellValueObj == null || cellValueObj.toString().trim().isEmpty()) {
colNum++;
continue;
}
// 3. 封装单元格数据+全量元数据
String cellValue = cellValueObj.toString().trim();
ExcelCellData cellData = new ExcelCellData();
cellData.setSheetName(currentSheetName);
cellData.setRowNum(rowNum);
cellData.setColNum(colNum);
cellData.setCellValue(cellValue);
cellDataList.add(cellData);
colNum++;
}
}
/**
* 解析完成回调:所有工作表解析完毕后触发
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.printf("✅ Excel解析完成 | 工作表:%s | 提取有效单元格数:%d%n",
currentSheetName, cellDataList.size());
}
// 获取最终解析结果
public List<ExcelCellData> getCellDataList() {
return cellDataList;
}
}
第三层:文本分块工具类(行级聚合)
将分散的单元格数据,按「工作表+行号」分组聚合为结构化文本块,保证单行数据的语义完整性,适配向量化需求
java
import java.util.*;
import java.util.stream.Collectors;
/**
* Excel文本分块工具类
* ✅ 按工作表+行分组 | ✅ 结构化拼接文本 | ✅ 封装元数据 | ✅ 生成TextChunk
*/
@Component
public class ExcelTextSplitter {
/**
* 核心方法:将单元格数据 → 按行聚合为文本块
* @param cellDataList 解析后的单元格数据列表
* @return 行级结构化文本块列表
*/
public List<TextChunk> splitByRow(List<ExcelCellData> cellDataList) {
// 双层Map:key1=工作表名,key2=行号 → value=该行拼接文本
Map<String, Map<Integer, StringBuilder>> rowTextMap = new HashMap<>(16);
List<TextChunk> textChunks = new ArrayList<>();
// 1. 按「工作表+行号」分组,拼接单行所有单元格数据
cellDataList.forEach(cellData -> {
String sheetName = cellData.getSheetName();
Integer rowNum = cellData.getRowNum();
// 不存在则初始化层级Map
rowTextMap.computeIfAbsent(sheetName, k -> new HashMap<>(32));
StringBuilder rowText = rowTextMap.get(sheetName)
.computeIfAbsent(rowNum, k -> new StringBuilder());
// 结构化拼接:列X:值; → 保证文本可读性与向量化准确性
rowText.append("列").append(cellData.getColNum())
.append(":").append(cellData.getCellValue()).append("; ");
});
// 2. 生成TextChunk,封装聚合文本+元数据
rowTextMap.forEach((sheetName, rowMap) -> {
rowMap.forEach((rowNum, rowText) -> {
// 拼接最终行文本(带工作表+行号标识)
String chunkText = String.format("工作表[%s] 行[%d] 内容: %s",
sheetName, rowNum, rowText);
TextChunk chunk = new TextChunk();
chunk.setContent(chunkText);
// 封装元数据:用于ES存储、检索溯源、结果过滤
chunk.setMetadata(Map.of(
"sheetName", sheetName,
"rowNum", rowNum.toString(),
"fileType", "excel",
"dataSource", "excel-row"
));
textChunks.add(chunk);
});
});
return textChunks;
}
}
第四层:向量化+ES存储服务(核心业务)
整合「文本向量化」与「批量写入ES」能力,实现Excel数据向量化的最终落地,适配Spring AI向量库规范
java
import lombok.RequiredArgsConstructor;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.IntStream;
/**
* Excel向量化核心服务
* ✅ 文本块批量向量化 | ✅ 向量数据封装 | ✅ 批量写入Elasticsearch
*/
@Service
@RequiredArgsConstructor // 构造器注入依赖,避免空指针
public class ExcelVectorStoreService {
// Spring AI 嵌入模型(注入bce-embedding-base_v1)
private final EmbeddingModel embeddingModel;
// Spring AI 向量库(已配置ES实现,直接调用)
private final VectorStore vectorStore;
/**
* 文本块批量向量化:调用嵌入模型生成向量数据
*/
public List<VectorChunk> generateVector(List<TextChunk> textChunks) {
// 提取所有文本内容,批量向量化(效率远高于单条调用)
List<String> texts = textChunks.stream()
.map(TextChunk::getContent)
.collect(Collectors.toList());
List<float[]> vectorList = embeddingModel.embed(texts);
// 文本块 + 元数据 + 向量 组合封装
return IntStream.range(0, textChunks.size())
.mapToObj(i -> {
TextChunk textChunk = textChunks.get(i);
VectorChunk vectorChunk = new VectorChunk();
vectorChunk.setContent(textChunk.getContent());
vectorChunk.setMetadata(textChunk.getMetadata());
vectorChunk.setVector(vectorList.get(i));
return vectorChunk;
}).collect(Collectors.toList());
}
/**
* 向量数据批量写入ES:适配Spring AI VectorStore规范,自动处理向量存储
*/
public void saveToElasticsearch(List<VectorChunk> vectorChunks) {
List<Document> esDocList = new ArrayList<>(vectorChunks.size());
vectorChunks.forEach(vectorChunk -> {
// 生成分片唯一ID(UUID去横线,避免ES索引ID特殊字符问题)
String chunkId = UUID.randomUUID().toString().replace("-", "");
// 封装为Spring AI标准Document,直接写入ES
Document document = new Document(chunkId, vectorChunk.getContent(), vectorChunk.getMetadata());
esDocList.add(document);
});
// 批量写入ES,适配配置的bulk-size,提升写入效率
vectorStore.add(esDocList);
System.out.printf("✅ Excel向量数据写入完成 | 共写入分片数:%d%n", esDocList.size());
}
}
五、完整调用示例(一站式流程)
整合「解析→分块→向量化→存储」全流程,提供标准调用入口,可直接嵌入业务代码(如文件上传、定时任务)
java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.List;
/**
* Excel向量化一站式调用服务
* 对外提供统一入口,屏蔽底层四层实现细节
*/
@Service
@RequiredArgsConstructor
public class ExcelVectorizationService {
private final ExcelTextSplitter excelTextSplitter;
private final ExcelVectorStoreService excelVectorStoreService;
/**
* Excel文件向量化一站式入口
* @param excelFilePath Excel文件绝对路径
* @param headRowNumber 表头行数(有表头=1,无表头=0)
*/
public void vectorizeExcel(String excelFilePath, int headRowNumber) {
try {
File excelFile = new File(excelFilePath);
// 1. 解析Excel:单元格数据
List<ExcelCellData> cellDataList = ExcelParserUtil.parseExcel(excelFile, headRowNumber);
// 2. 文本分块:按行聚合为TextChunk
List<TextChunk> textChunks = excelTextSplitter.splitByRow(cellDataList);
// 3. 向量化:生成向量数据
List<VectorChunk> vectorChunks = excelVectorStoreService.generateVector(textChunks);
// 4. 写入ES:批量存储向量+文本+元数据
excelVectorStoreService.saveToElasticsearch(vectorChunks);
} catch (Exception e) {
throw new RuntimeException("❌ Excel文件向量化失败:" + e.getMessage(), e);
}
}
/**
* 重载方法:默认无表头(headRowNumber=0),简化调用
*/
public void vectorizeExcel(String excelFilePath) {
vectorizeExcel(excelFilePath, 0);
}
}
// ========== 调用示例 ==========
// @Autowired
// private ExcelVectorizationService excelVectorizationService;
//
// // 无表头Excel调用
// excelVectorizationService.vectorizeExcel("D:/数据报表.xlsx");
// // 有表头Excel调用
// excelVectorizationService.vectorizeExcel("D:/用户信息表.xlsx", 1);
六、核心设计亮点 & 优化说明
✅ 关键设计优势(解决传统Excel处理痛点)
- 内存安全:基于EasyExcel流式解析,无需一次性加载全量Excel数据,支持GB级超大Excel解析,无OOM风险;
- 数据纯净:自动过滤空单元格、首尾空格,避免无效数据参与向量化,提升检索准确率;
- 语义完整:按「行」聚合单元格数据,保证单行业务数据的关联性(如一行是一条用户信息,完整聚合不拆分);
- 溯源能力:全程保留工作表名、行号、列号元数据,检索时可精准定位数据在Excel中的位置;
- 效率最优 :批量向量化+批量写入ES,匹配Spring AI配置的
bulk-size,大幅减少模型调用与ES请求次数。
✅ 核心参数说明(可按需调整)
| 参数名 | 取值建议 | 作用说明 |
|---|---|---|
headRowNumber |
0/1 | 必配!0=无表头,1=有表头,避免表头数据混入业务数据 |
| 向量维度 | 768 | 固定匹配lrs33/bce-embedding-base_v1模型,与ES配置一致 |
| 批量写入大小 | 50 | 复用application.yml中bulk-size配置,无需单独调整 |
| 列号/行号计数 | 从1开始 | 与Excel视觉行号/列号一致,符合业务使用习惯 |
七、注意事项 & 最佳实践
⚠️ 开发/部署注意事项
- 格式支持 :完美适配
.xls(Excel 97-2003)、.xlsx(Excel 2007+),不支持WPS专属格式(.et); - 表头处理 :有表头的Excel必须传
headRowNumber=1,否则表头会被当作业务数据解析; - 数据类型兼容:Excel中数字、日期、布尔值会自动转为字符串,统一处理无类型异常;
- 大文件优化:解析10万行+超大Excel时,建议异步执行(结合线程池),避免阻塞主线程;
- 权限校验 :解析文件前需校验文件读取权限,避免
AccessDeniedException; - ES索引匹配 :确保ES索引的
text-field-name/vector-field-name与配置一致,否则存储失败。
✅ 最佳实践建议
- 统一调用入口 :使用
ExcelVectorizationService一站式调用,屏蔽底层细节,便于维护; - 异步处理 :大批量Excel文件向量化时,结合
@Async实现异步执行,提升系统吞吐量; - 日志增强:在关键步骤增加日志(文件路径、解析行数、写入分片数、耗时),便于问题排查;
- 容错机制:对损坏的Excel文件增加重试逻辑,或返回友好提示,避免服务崩溃;
- 数据过滤:可扩展「行过滤」逻辑(如跳过指定行、过滤指定内容),适配个性化业务需求。
八、扩展能力规划
- 按工作表过滤:支持指定解析单个/多个工作表,无需解析全表;
- 自定义分块策略:支持「按列分组」「按固定行数分块」,适配不同Excel数据结构;
- 数据校验:增加单元格数据格式校验(如手机号、邮箱、日期),提升数据质量;
- 进度监控:增加解析/向量化进度回调,支持前端展示处理进度;
- 结果去重:基于Excel文件MD5去重,避免重复向量化写入ES。