feat(phase1): 实现文档分块向量化索引

Task 5.6: 向量化索引实现
- VectorIndexService 新增方法:
  - indexDocumentChunks(docId, chunks): 索引文档分块到 Milvus
  - deleteDocumentChunks(docId): 删除文档的所有向量
  - buildDocumentMetadata(): 构建文档元数据(区分文件索引)

核心流程:
1. 上传时:文本提取 → 分块 → 向量化 → 存入 Milvus + MySQL
2. 检索时:问题向量化 → Milvus 语义检索 → 返回相似文档
3. 删除时:删除元数据 + 删除向量索引

实现细节:
- 复用 indexSingleFile 的向量化逻辑
- metadata.docId 标识文档来源(区分 upload: 和 file:)
- 删除表达式:metadata["docId"] == "xxx"
- 自动去重:上传前删除旧向量数据

DocumentManagementService 完整实现:
- uploadDocument: 完整向量化流程(移除 TODO)
- deleteDocument: 同步删除向量索引(移除 TODO)

编译验证:BUILD SUCCESS

Progress: 32/34 tasks completed (94%)
This commit is contained in:
zhuyongxin
2026-06-23 16:08:50 +08:00
parent 26aaf149d8
commit 4ef8d87961
3 changed files with 119 additions and 12 deletions
@@ -167,6 +167,110 @@ public class VectorIndexService {
logger.info("文件索引完成: {}, 共 {} 个分片", filePath, chunks.size());
}
/**
* 索引文档分块(用于上传文档的向量化)
*
* @param docId 文档ID
* @param chunks 文档分块列表
* @throws Exception 索引失败时抛出异常
*/
public void indexDocumentChunks(String docId, List<DocumentChunk> chunks) throws Exception {
if (chunks == null || chunks.isEmpty()) {
throw new IllegalArgumentException("文档分块列表为空");
}
logger.info("开始索引文档分块,docId: {}, 分块数: {}", docId, chunks.size());
// 1. 删除该文档的旧数据(如果存在)
deleteDocumentChunks(docId);
// 2. 为每个分块生成向量并插入 Milvus
for (int i = 0; i < chunks.size(); i++) {
DocumentChunk chunk = chunks.get(i);
try {
// 生成向量
List<Float> vector = embeddingService.generateEmbedding(chunk.getContent());
// 构建元数据(使用 docId 作为来源标识)
Map<String, Object> metadata = buildDocumentMetadata(docId, chunk, chunks.size());
// 插入到 Milvus
insertToMilvus(chunk.getContent(), vector, metadata, chunk.getChunkIndex());
logger.info("✓ 文档分块 {}/{} 索引成功,docId: {}", i + 1, chunks.size(), docId);
} catch (Exception e) {
logger.error("✗ 文档分块 {}/{} 索引失败,docId: {}", i + 1, chunks.size(), docId, e);
throw new RuntimeException("文档分块索引失败: " + e.getMessage(), e);
}
}
logger.info("文档索引完成,docId: {}, 共 {} 个分块", docId, chunks.size());
}
/**
* 删除文档的所有分块(根据 docId)
*/
public void deleteDocumentChunks(String docId) {
try {
// 构建删除表达式:metadata["docId"] == "xxx"
String expr = String.format("metadata[\"docId\"] == \"%s\"", docId);
logger.info("准备删除文档旧数据,docId: {}, 表达式: {}", docId, expr);
// 确保 collection 已加载
R<RpcStatus> loadResponse = milvusClient.loadCollection(
LoadCollectionParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.build()
);
if (loadResponse.getStatus() != 0 && loadResponse.getStatus() != 65535) {
logger.warn("加载 collection 失败: {}", loadResponse.getMessage());
return;
}
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME)
.withExpr(expr)
.build();
R<MutationResult> deleteResponse = milvusClient.delete(deleteParam);
if (deleteResponse.getStatus() == 0) {
logger.info("删除文档旧数据成功,docId: {}", docId);
} else {
logger.warn("删除文档旧数据失败,docId: {}, 原因: {}", docId, deleteResponse.getMessage());
}
} catch (Exception e) {
logger.warn("删除文档旧数据异常,docId: {}", docId, e);
}
}
/**
* 构建文档元数据(用于上传文档)
*/
private Map<String, Object> buildDocumentMetadata(String docId, DocumentChunk chunk, int totalChunks) {
Map<String, Object> metadata = new HashMap<>();
// 文档标识
metadata.put("docId", docId);
metadata.put("_source", "upload:" + docId); // 区分文件索引和上传文档
// 分片信息
metadata.put("chunkIndex", chunk.getChunkIndex());
metadata.put("totalChunks", totalChunks);
// 标题信息
if (chunk.getTitle() != null && !chunk.getTitle().isEmpty()) {
metadata.put("title", chunk.getTitle());
}
return metadata;
}
/**
* 删除文件的旧数据(根据 metadata._source)
*/