|
|
@@ -0,0 +1,2502 @@
|
|
|
+# 文本向量化与Elasticsearch入库方案
|
|
|
+
|
|
|
+## 一、环境测试结果
|
|
|
+
|
|
|
+### 1.1 Embedding接口测试
|
|
|
+
|
|
|
+**配置信息:**
|
|
|
+- 接口地址:`http://10.192.72.11:18081/v1`
|
|
|
+- API Key:`1`
|
|
|
+- 模型:`Qwen3-Embedding-8B`
|
|
|
+- 向量维度:`4096`
|
|
|
+- 批处理大小:`128`
|
|
|
+
|
|
|
+**测试结果:** ✅ 可用
|
|
|
+- HTTP状态码:200
|
|
|
+- 响应时间:< 1秒
|
|
|
+- 向量维度验证:正确返回4096维浮点数数组
|
|
|
+- 支持批量请求
|
|
|
+
|
|
|
+**请求示例:**
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.11:18081/v1/embeddings" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "Authorization: Bearer 1" \
|
|
|
+ -d '{
|
|
|
+ "model": "Qwen3-Embedding-8B",
|
|
|
+ "input": "测试文本内容"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+### 1.2 Elasticsearch测试
|
|
|
+
|
|
|
+**配置信息:**
|
|
|
+- 地址:`http://10.192.72.13:9200`
|
|
|
+- 版本:`8.12.0`
|
|
|
+- 集群状态:yellow(单节点模式正常)
|
|
|
+
|
|
|
+**测试结果:** ✅ 可用且支持向量字段
|
|
|
+- 集群连接正常
|
|
|
+- dense_vector字段类型支持
|
|
|
+- KNN向量搜索支持
|
|
|
+- 向量维度严格验证
|
|
|
+
|
|
|
+**创建向量索引示例:**
|
|
|
+```json
|
|
|
+PUT /text_vectors
|
|
|
+{
|
|
|
+ "settings": {
|
|
|
+ "number_of_shards": 1,
|
|
|
+ "number_of_replicas": 0
|
|
|
+ },
|
|
|
+ "mappings": {
|
|
|
+ "properties": {
|
|
|
+ "id": { "type": "keyword" },
|
|
|
+ "content": { "type": "text" },
|
|
|
+ "embedding": {
|
|
|
+ "type": "dense_vector",
|
|
|
+ "dims": 4096,
|
|
|
+ "index": true,
|
|
|
+ "similarity": "cosine"
|
|
|
+ },
|
|
|
+ "business_id": { "type": "keyword" },
|
|
|
+ "create_time": { "type": "date" }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、整体架构设计
|
|
|
+
|
|
|
+### 2.1 架构层次
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ Controller层 │
|
|
|
+│ 文本入库接口 / 向量搜索接口 / 批量操作接口 │
|
|
|
+└─────────────────────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ Service层 │
|
|
|
+│ 业务逻辑编排 / 批量处理策略 / 异常处理与重试 │
|
|
|
+└─────────────────────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+┌────────────────────────┬────────────────────────────────┐
|
|
|
+│ EmbeddingClient层 │ ElasticsearchRepository层 │
|
|
|
+│ 调用向量化接口 │ ES操作封装 │
|
|
|
+│ 批量请求优化 │ 索引管理/文档CRUD/KNN搜索 │
|
|
|
+└────────────────────────┴────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 数据流转
|
|
|
+
|
|
|
+**文本入库流程:**
|
|
|
+```
|
|
|
+原始文本
|
|
|
+ ↓
|
|
|
+文本预处理(长度限制、特殊字符处理)
|
|
|
+ ↓
|
|
|
+调用Embedding接口获取4096维向量
|
|
|
+ ↓
|
|
|
+构建ES文档(包含原文、向量、元数据)
|
|
|
+ ↓
|
|
|
+批量写入ES(Bulk API)
|
|
|
+ ↓
|
|
|
+返回入库结果(成功数、失败数、文档ID)
|
|
|
+```
|
|
|
+
|
|
|
+**向量搜索流程:**
|
|
|
+```
|
|
|
+用户输入查询文本
|
|
|
+ ↓
|
|
|
+调用Embedding接口向量化
|
|
|
+ ↓
|
|
|
+KNN搜索(cosine相似度)
|
|
|
+ ↓
|
|
|
+返回最相似的Top-K文档及相似度分数
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、完整操作步骤(从零开始)
|
|
|
+
|
|
|
+### 3.1 操作流程总览
|
|
|
+
|
|
|
+```
|
|
|
+步骤0:环境准备
|
|
|
+ ↓
|
|
|
+步骤1:创建ES索引(定义表结构)
|
|
|
+ ↓
|
|
|
+步骤2:准备文本数据
|
|
|
+ ↓
|
|
|
+步骤3:文本向量化(调用Embedding接口)
|
|
|
+ ↓
|
|
|
+步骤4:数据入库ES(插入文档)
|
|
|
+ ↓
|
|
|
+步骤5:向量搜索(KNN查询)
|
|
|
+ ↓
|
|
|
+步骤6:验证结果
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.2 步骤0:环境准备
|
|
|
+
|
|
|
+#### 3.2.1 检查服务状态
|
|
|
+
|
|
|
+| 服务 | 检查命令 | 预期结果 |
|
|
|
+|------|---------|---------|
|
|
|
+| **Embedding接口** | `curl http://10.192.72.11:18081/v1` | 返回API信息 |
|
|
|
+| **Elasticsearch** | `curl http://10.192.72.13:9200` | 返回ES版本信息 |
|
|
|
+
|
|
|
+#### 3.2.2 准备配置信息
|
|
|
+
|
|
|
+| 配置项 | 值 |
|
|
|
+|--------|-----|
|
|
|
+| Embedding接口地址 | `http://10.192.72.11:18081/v1` |
|
|
|
+| API Key | `1` |
|
|
|
+| 模型名称 | `Qwen3-Embedding-8B` |
|
|
|
+| 向量维度 | `4096` |
|
|
|
+| ES地址 | `http://10.192.72.13:9200` |
|
|
|
+| 索引名称 | `text_vectors` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.3 步骤1:创建ES索引(一次性操作)
|
|
|
+
|
|
|
+#### 3.3.1 创建索引命令
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X PUT "http://10.192.72.13:9200/text_vectors" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "settings": {
|
|
|
+ "number_of_shards": 1,
|
|
|
+ "number_of_replicas": 0
|
|
|
+ },
|
|
|
+ "mappings": {
|
|
|
+ "properties": {
|
|
|
+ "content": {
|
|
|
+ "type": "text",
|
|
|
+ "analyzer": "standard"
|
|
|
+ },
|
|
|
+ "embedding": {
|
|
|
+ "type": "dense_vector",
|
|
|
+ "dims": 4096,
|
|
|
+ "index": true,
|
|
|
+ "similarity": "cosine"
|
|
|
+ },
|
|
|
+ "business_id": {
|
|
|
+ "type": "keyword"
|
|
|
+ },
|
|
|
+ "content_length": {
|
|
|
+ "type": "integer"
|
|
|
+ },
|
|
|
+ "create_time": {
|
|
|
+ "type": "date",
|
|
|
+ "format": "yyyy-MM-dd'\''T'\''HH:mm:ss"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}'
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.3.2 预期返回
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "acknowledged": true,
|
|
|
+ "shards_acknowledged": true,
|
|
|
+ "index": "text_vectors"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.3.3 验证索引创建成功
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X GET "http://10.192.72.13:9200/text_vectors/_mapping?pretty"
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.3.4 字段说明表
|
|
|
+
|
|
|
+| 字段名 | 类型 | 说明 | 示例值 |
|
|
|
+|--------|------|------|--------|
|
|
|
+| `content` | text | 原始文本内容 | "人工智能技术..." |
|
|
|
+| `embedding` | dense_vector | 4096维向量 | [0.0234, -0.0123, ...] |
|
|
|
+| `business_id` | keyword | 业务唯一标识 | "doc-ai-001" |
|
|
|
+| `content_length` | integer | 文本长度 | 45 |
|
|
|
+| `create_time` | date | 创建时间 | "2026-03-12T10:30:00" |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.4 步骤2:准备文本数据与分段
|
|
|
+
|
|
|
+#### 3.4.1 重要说明:全量文本分段
|
|
|
+
|
|
|
+**输入:** 全量文本(未分段)
|
|
|
+```
|
|
|
+原始输入:一份完整的合同文档(5000字)
|
|
|
+```
|
|
|
+
|
|
|
+**输出:** 分段后的文本列表
|
|
|
+```
|
|
|
+分段输出:
|
|
|
+- Chunk 0: 第1-500字(合同基本信息)
|
|
|
+- Chunk 1: 第501-1000字(条款1)
|
|
|
+- Chunk 2: 第1001-1500字(条款2)
|
|
|
+- ...
|
|
|
+- Chunk 9: 第4501-5000字(签署信息)
|
|
|
+```
|
|
|
+
|
|
|
+**为什么要分段?**
|
|
|
+1. Embedding模型有token限制(通常512-2048 tokens)
|
|
|
+2. 分段检索更精准(直接定位到相关段落)
|
|
|
+3. 便于生成准确的回答(基于段落而非整篇文档)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.2 文本分段策略
|
|
|
+
|
|
|
+**策略对比表:**
|
|
|
+
|
|
|
+| 分段策略 | 原理 | 适用场景 | 优缺点 |
|
|
|
+|---------|------|---------|--------|
|
|
|
+| **固定长度分段** | 按字符数/token数切分 | 通用文档 | ✅ 简单<br>❌ 可能切断语义 |
|
|
|
+| **段落分段** | 按自然段落切分 | 结构化文档 | ✅ 保持语义完整<br>❌ 段落长度不均 |
|
|
|
+| **语义分段** | 基于语义相似度 | 重要文档 | ✅ 语义完整<br>❌ 需要额外模型 |
|
|
|
+| **滑动窗口** | 重叠切分 | 避免边界丢失 | ✅ 防止信息丢失<br>❌ 数据冗余 |
|
|
|
+| **混合策略** | 综合多种方法 | 生产环境推荐 | ✅ 平衡效果和性能 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.3 推荐的分段策略(混合方案)
|
|
|
+
|
|
|
+**分段流程:**
|
|
|
+
|
|
|
+```
|
|
|
+全量文本(5000字)
|
|
|
+ ↓
|
|
|
+1. 预处理(去除特殊字符、统一格式)
|
|
|
+ ↓
|
|
|
+2. 按段落切分(得到15个自然段)
|
|
|
+ ↓
|
|
|
+3. 每段检查长度
|
|
|
+ ↓
|
|
|
+4. 超长段落继续切分(最大500字)
|
|
|
+ ↓
|
|
|
+5. 添加重叠(前后50字重叠)
|
|
|
+ ↓
|
|
|
+6. 生成chunk列表(最终20个chunk)
|
|
|
+ ↓
|
|
|
+7. 每个chunk添加元数据(chunk序号、页码等)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.4 分段参数配置
|
|
|
+
|
|
|
+| 参数 | 推荐值 | 说明 |
|
|
|
+|------|--------|------|
|
|
|
+| **最大chunk长度** | 500-800字 | 确保不超过模型token限制 |
|
|
|
+| **最小chunk长度** | 100字 | 避免过短无意义的chunk |
|
|
|
+| **重叠大小** | 50-100字 | 防止边界信息丢失 |
|
|
|
+| **分段模式** | paragraph | 按段落优先,超长再切分 |
|
|
|
+| **保留换行** | false | 转换为空格,便于向量化 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.5 分段示例
|
|
|
+
|
|
|
+**输入文本(全量):**
|
|
|
+```
|
|
|
+永续贷产品贷款合同
|
|
|
+
|
|
|
+第一章:总则
|
|
|
+本合同由甲方A公司与乙方B公司签订,合同金额为100万元。
|
|
|
+本合同旨在规范双方的借贷行为,保护双方合法权益。
|
|
|
+
|
|
|
+第二章:贷款条款
|
|
|
+2.1 贷款利率按照央行基准利率执行。
|
|
|
+2.2 贷款期限为36个月,从2026年1月15日起算。
|
|
|
+2.3 还款方式为等额本息,每月15日还款。
|
|
|
+
|
|
|
+第三章:违约责任
|
|
|
+任何一方违反本合同约定,应承担相应的违约责任。
|
|
|
+违约金为未还款项的5%。
|
|
|
+...
|
|
|
+```
|
|
|
+
|
|
|
+**分段输出:**
|
|
|
+
|
|
|
+| Chunk ID | 内容 | 长度 | 页码 |
|
|
|
+|----------|------|------|------|
|
|
|
+| doc-001_chunk_0 | 永续贷产品贷款合同\n\n第一章:总则\n本合同由甲方A公司与乙方B公司签订... | 85 | P1 |
|
|
|
+| doc-001_chunk_1 | ...保护双方合法权益。\n\n第二章:贷款条款\n2.1 贷款利率按照央行基准利率执行。 | 78 | P1 |
|
|
|
+| doc-001_chunk_2 | 2.2 贷款期限为36个月,从2026年1月15日起算。\n2.3 还款方式为等额本息... | 82 | P2 |
|
|
|
+| doc-001_chunk_3 | ...每月15日还款。\n\n第三章:违约责任\n任何一方违反本合同约定... | 88 | P2 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.6 Java分段实现(核心代码结构)
|
|
|
+
|
|
|
+**TextSplitter.java**
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+public class TextSplitter {
|
|
|
+
|
|
|
+ // 分段配置
|
|
|
+ @Value("${chunk.max.length:500}")
|
|
|
+ private int maxChunkLength;
|
|
|
+
|
|
|
+ @Value("${chunk.min.length:100}")
|
|
|
+ private int minChunkLength;
|
|
|
+
|
|
|
+ @Value("${chunk.overlap:50}")
|
|
|
+ private int overlapSize;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分段方法
|
|
|
+ */
|
|
|
+ public List<Chunk> split(String fullText, String docId) {
|
|
|
+ List<Chunk> chunks = new ArrayList<>();
|
|
|
+
|
|
|
+ // 1. 预处理
|
|
|
+ String text = preprocess(fullText);
|
|
|
+
|
|
|
+ // 2. 按段落切分
|
|
|
+ List<String> paragraphs = splitByParagraph(text);
|
|
|
+
|
|
|
+ // 3. 合并或切分段落
|
|
|
+ int chunkIndex = 0;
|
|
|
+ StringBuilder currentChunk = new StringBuilder();
|
|
|
+
|
|
|
+ for (String paragraph : paragraphs) {
|
|
|
+ if (currentChunk.length() + paragraph.length() > maxChunkLength) {
|
|
|
+ // 当前chunk已满,保存并新建
|
|
|
+ if (currentChunk.length() > 0) {
|
|
|
+ chunks.add(createChunk(
|
|
|
+ docId, chunkIndex++,
|
|
|
+ currentChunk.toString()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ currentChunk = new StringBuilder(paragraph);
|
|
|
+ } else {
|
|
|
+ currentChunk.append(paragraph).append("\n");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 最后一个chunk
|
|
|
+ if (currentChunk.length() > 0) {
|
|
|
+ chunks.add(createChunk(docId, chunkIndex, currentChunk.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return chunks;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 预处理文本
|
|
|
+ */
|
|
|
+ private String preprocess(String text) {
|
|
|
+ return text
|
|
|
+ .replaceAll("\\r\\n", "\n") // 统一换行符
|
|
|
+ .replaceAll("\\s+", " ") // 多个空格合并
|
|
|
+ .trim(); // 去除首尾空格
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按段落切分
|
|
|
+ */
|
|
|
+ private List<String> splitByParagraph(String text) {
|
|
|
+ return Arrays.asList(text.split("\\n\\s*\\n"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建chunk对象
|
|
|
+ */
|
|
|
+ private Chunk createChunk(String docId, int index, String content) {
|
|
|
+ return Chunk.builder()
|
|
|
+ .docId(docId)
|
|
|
+ .chunkId(docId + "_chunk_" + index)
|
|
|
+ .chunkIndex(index)
|
|
|
+ .content(content)
|
|
|
+ .contentLength(content.length())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Chunk.java**
|
|
|
+
|
|
|
+```java
|
|
|
+@Data
|
|
|
+@Builder
|
|
|
+public class Chunk {
|
|
|
+ private String docId; // 文档ID
|
|
|
+ private String chunkId; // chunk唯一标识
|
|
|
+ private int chunkIndex; // chunk序号
|
|
|
+ private String content; // chunk内容
|
|
|
+ private int contentLength; // 内容长度
|
|
|
+
|
|
|
+ // 业务数据(从全量文本中提取)
|
|
|
+ private Map<String, Object> metadata;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.7 完整的数据流转(包含分段)
|
|
|
+
|
|
|
+```
|
|
|
+【原始输入】
|
|
|
+完整合同文档PDF(5000字)
|
|
|
+ ↓
|
|
|
+【步骤1:文档解析】
|
|
|
+提取文本 → "永续贷产品贷款合同\n\n第一章:总则..."
|
|
|
+提取元数据 → 甲方:A公司,乙方:B公司,金额:100万
|
|
|
+ ↓
|
|
|
+【步骤2:文本分段】
|
|
|
+调用TextSplitter.split()
|
|
|
+ ↓
|
|
|
+生成10个chunk,每个约500字
|
|
|
+ ↓
|
|
|
+【步骤3:批量向量化】
|
|
|
+调用Embedding接口(批量10个)
|
|
|
+ ↓
|
|
|
+得到10个向量,每个4096维
|
|
|
+ ↓
|
|
|
+【步骤4:构建ES文档】
|
|
|
+为每个chunk添加:
|
|
|
+ - doc_id(相同)
|
|
|
+ - chunk_id(不同)
|
|
|
+ - content(不同)
|
|
|
+ - embedding(不同)
|
|
|
+ - 元数据(相同,冗余)
|
|
|
+ ↓
|
|
|
+【步骤5:批量入库】
|
|
|
+使用Bulk API一次性插入10个文档
|
|
|
+ ↓
|
|
|
+【步骤6:验证】
|
|
|
+查询doc_id,应返回10个chunk
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.8 准备示例文本(全量)
|
|
|
+
|
|
|
+**场景:** 上传完整合同文档
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "file_id": "wo1o23bn2oi3ngo3",
|
|
|
+ "file_name": "永续贷产品贷款合同.pdf",
|
|
|
+ "file_path": "/home/data/contracts/xxx.pdf",
|
|
|
+ "full_text": "永续贷产品贷款合同\n\n第一章:总则\n本合同由甲方A公司与乙方B公司签订,合同金额为100万元。\n本合同旨在规范双方的借贷行为,保护双方合法权益。\n\n第二章:贷款条款\n2.1 贷款利率按照央行基准利率执行。\n2.2 贷款期限为36个月,从2026年1月15日起算。\n2.3 还款方式为等额本息,每月15日还款。\n\n第三章:违约责任\n任何一方违反本合同约定,应承担相应的违约责任。",
|
|
|
+
|
|
|
+ // 提取的业务元数据
|
|
|
+ "metadata": {
|
|
|
+ "contract_type": "消费贷贷款合同",
|
|
|
+ "contract_name": "永续贷产品贷款合同",
|
|
|
+ "party_a": "A公司",
|
|
|
+ "party_b": "B公司",
|
|
|
+ "contract_amount": 1000000.00,
|
|
|
+ "sign_date": "2026-01-15"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**经过分段处理后:**
|
|
|
+
|
|
|
+```json
|
|
|
+[
|
|
|
+ {
|
|
|
+ "doc_id": "wo1o23bn2oi3ngo3",
|
|
|
+ "chunk_id": "wo1o23bn2oi3ngo3_chunk_0",
|
|
|
+ "chunk_index": 0,
|
|
|
+ "content": "永续贷产品贷款合同\n\n第一章:总则\n本合同由甲方A公司与乙方B公司签订,合同金额为100万元。",
|
|
|
+ "metadata": {...}
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "doc_id": "wo1o23bn2oi3ngo3",
|
|
|
+ "chunk_id": "wo1o23bn2oi3ngo3_chunk_1",
|
|
|
+ "chunk_index": 1,
|
|
|
+ "content": "本合同旨在规范双方的借贷行为,保护双方合法权益。\n\n第二章:贷款条款\n2.1 贷款利率按照央行基准利率执行。",
|
|
|
+ "metadata": {...}
|
|
|
+ },
|
|
|
+ // ... 更多chunk
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3.4.9 文本预处理(可选)
|
|
|
+
|
|
|
+```json
|
|
|
+[
|
|
|
+ {
|
|
|
+ "business_id": "doc-ai-001",
|
|
|
+ "content": "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "business_id": "doc-ml-001",
|
|
|
+ "content": "机器学习是人工智能的核心技术之一,通过算法让计算机从数据中学习规律,做出预测或决策。"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "business_id": "doc-dl-001",
|
|
|
+ "content": "深度学习使用神经网络模拟人脑的学习过程,在图像识别、自然语言处理等领域取得突破。"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "business_id": "doc-nlp-001",
|
|
|
+ "content": "自然语言处理使计算机能够理解、解释和生成人类语言,应用于翻译、情感分析、智能客服等。"
|
|
|
+ }
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.4.2 文本预处理(可选)
|
|
|
+
|
|
|
+| 预处理项 | 操作 | 示例 |
|
|
|
+|----------|------|------|
|
|
|
+| 去除多余空格 | `text.trim()` | `" 文本 "` → `"文本"` |
|
|
|
+| 统一换行符 | 替换为空格 | `"文本1\n文本2"` → `"文本1 文本2"` |
|
|
|
+| 长度限制 | 截断或分批 | 超过8192字符时处理 |
|
|
|
+| HTML标签去除 | 正则替换 | `"<p>文本</p>"` → `"文本"` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.5 步骤3:文本向量化
|
|
|
+
|
|
|
+#### 3.5.1 调用Embedding接口(单个文本)
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.11:18081/v1/embeddings" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "Authorization: Bearer 1" \
|
|
|
+ -d '{
|
|
|
+ "model": "Qwen3-Embedding-8B",
|
|
|
+ "input": "人工智能是计算机科学的一个分支"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.5.2 返回的向量数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "object": "list",
|
|
|
+ "data": [{
|
|
|
+ "object": "embedding",
|
|
|
+ "index": 0,
|
|
|
+ "embedding": [
|
|
|
+ 0.013597949407994747,
|
|
|
+ 0.0037246558349579573,
|
|
|
+ 0.008395255543291569,
|
|
|
+ -0.003488169750198722,
|
|
|
+ 0.010582752525806427,
|
|
|
+ // ... 共4096个浮点数 ...
|
|
|
+ 0.020219560712575912
|
|
|
+ ]
|
|
|
+ }],
|
|
|
+ "model": "Qwen3-Embedding-8B",
|
|
|
+ "usage": {
|
|
|
+ "prompt_tokens": 35,
|
|
|
+ "total_tokens": 35
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.5.3 向量化数据说明
|
|
|
+
|
|
|
+| 属性 | 值 | 说明 |
|
|
|
+|------|-----|------|
|
|
|
+| 向量维度 | 4096 | 固定维度 |
|
|
|
+| 数据类型 | 浮点数 | float类型 |
|
|
|
+| 取值范围 | -1 ~ 1 | 通常在-1到1之间 |
|
|
|
+| 向量数量 | 1个 | 单个文本返回1个向量 |
|
|
|
+
|
|
|
+#### 3.5.4 批量向量化(可选,提高效率)
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.11:18081/v1/embeddings" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "Authorization: Bearer 1" \
|
|
|
+ -d '{
|
|
|
+ "model": "Qwen3-Embedding-8B",
|
|
|
+ "input": [
|
|
|
+ "人工智能是计算机科学的一个分支",
|
|
|
+ "机器学习是人工智能的核心技术",
|
|
|
+ "深度学习使用神经网络模拟人脑"
|
|
|
+ ]
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+**返回:** 3个向量,每个4096维
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.6 步骤4:数据入库ES
|
|
|
+
|
|
|
+#### 3.6.1 插入单个文档
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.13:9200/text_vectors/_doc/doc-ai-001" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "content": "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。",
|
|
|
+ "embedding": [
|
|
|
+ 0.013597949407994747,
|
|
|
+ 0.0037246558349579573,
|
|
|
+ 0.008395255543291569,
|
|
|
+ -0.003488169750198722,
|
|
|
+ 0.010582752525806427,
|
|
|
+ 0.015623456789012345,
|
|
|
+ -0.043223456789012345,
|
|
|
+ 0.067823456789012345,
|
|
|
+ 0.023423456789012345,
|
|
|
+ -0.056723456789012345,
|
|
|
+ // ... 共4096个数字 ...
|
|
|
+ 0.020219560712575912
|
|
|
+ ],
|
|
|
+ "business_id": "doc-ai-001",
|
|
|
+ "content_length": 68,
|
|
|
+ "create_time": "2026-03-12T10:30:00"
|
|
|
+}'
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.6.2 预期返回
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "_index": "text_vectors",
|
|
|
+ "_id": "doc-ai-001",
|
|
|
+ "_version": 1,
|
|
|
+ "result": "created",
|
|
|
+ "_shards": {
|
|
|
+ "total": 1,
|
|
|
+ "successful": 1,
|
|
|
+ "failed": 0
|
|
|
+ },
|
|
|
+ "_seq_no": 0,
|
|
|
+ "_primary_term": 1
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.6.3 批量插入文档(Bulk API)
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.13:9200/_bulk" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '
|
|
|
+{"index": {"_index": "text_vectors", "_id": "doc-ml-001"}}
|
|
|
+{"content": "机器学习是人工智能的核心技术之一,通过算法让计算机从数据中学习规律。", "embedding": [0.0256, -0.0134, ...], "business_id": "doc-ml-001", "content_length": 45, "create_time": "2026-03-12T10:31:00"}
|
|
|
+
|
|
|
+{"index": {"_index": "text_vectors", "_id": "doc-dl-001"}}
|
|
|
+{"content": "深度学习使用神经网络模拟人脑的学习过程,在图像识别、自然语言处理等领域取得突破。", "embedding": [0.0221, -0.0112, ...], "business_id": "doc-dl-001", "content_length": 52, "create_time": "2026-03-12T10:32:00"}
|
|
|
+
|
|
|
+{"index": {"_index": "text_vectors", "_id": "doc-nlp-001"}}
|
|
|
+{"content": "自然语言处理使计算机能够理解、解释和生成人类语言,应用于翻译、情感分析、智能客服等。", "embedding": [0.0198, -0.0098, ...], "business_id": "doc-nlp-001", "content_length": 48, "create_time": "2026-03-12T10:33:00"}
|
|
|
+'
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.6.4 批量插入返回
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "took": 45,
|
|
|
+ "errors": false,
|
|
|
+ "items": [
|
|
|
+ {"index": {"_id": "doc-ml-001", "result": "created"}},
|
|
|
+ {"index": {"_id": "doc-dl-001", "result": "created"}},
|
|
|
+ {"index": {"_id": "doc-nlp-001", "result": "created"}}
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.6.5 验证数据入库成功
|
|
|
+
|
|
|
+```bash
|
|
|
+# 查看文档数量
|
|
|
+curl -X GET "http://10.192.72.13:9200/_cat/count/text_vectors?v"
|
|
|
+
|
|
|
+# 查看具体文档
|
|
|
+curl -X GET "http://10.192.72.13:9200/text_vectors/_doc/doc-ai-001?pretty"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.7 步骤5:向量搜索(KNN查询)
|
|
|
+
|
|
|
+#### 3.7.1 准备查询文本
|
|
|
+
|
|
|
+```
|
|
|
+用户查询:"什么是AI技术"
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.7.2 查询文本向量化
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.11:18081/v1/embeddings" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "Authorization: Bearer 1" \
|
|
|
+ -d '{
|
|
|
+ "model": "Qwen3-Embedding-8B",
|
|
|
+ "input": "什么是AI技术"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+**得到查询向量:** `[0.0145, 0.0045, 0.0098, ...]` (4096维)
|
|
|
+
|
|
|
+#### 3.7.3 执行KNN搜索
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.13:9200/text_vectors/_search" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "knn": {
|
|
|
+ "field": "embedding",
|
|
|
+ "query_vector": [
|
|
|
+ 0.0145, 0.0045, 0.0098, -0.0042, 0.0115,
|
|
|
+ 0.0168, -0.0456, 0.0698, 0.0256, -0.0589,
|
|
|
+ // ... 共4096个数字 ...
|
|
|
+ 0.0219
|
|
|
+ ],
|
|
|
+ "k": 3,
|
|
|
+ "num_candidates": 10
|
|
|
+ },
|
|
|
+ "fields": ["content", "business_id", "_score"],
|
|
|
+ "_source": false
|
|
|
+}'
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.7.4 搜索结果
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "took": 25,
|
|
|
+ "hits": {
|
|
|
+ "total": {
|
|
|
+ "value": 3,
|
|
|
+ "relation": "eq"
|
|
|
+ },
|
|
|
+ "hits": [
|
|
|
+ {
|
|
|
+ "_id": "doc-ai-001",
|
|
|
+ "_score": 0.9234,
|
|
|
+ "fields": {
|
|
|
+ "content": ["人工智能是计算机科学的一个分支..."],
|
|
|
+ "business_id": ["doc-ai-001"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "_id": "doc-ml-001",
|
|
|
+ "_score": 0.8891,
|
|
|
+ "fields": {
|
|
|
+ "content": ["机器学习是人工智能的核心技术..."],
|
|
|
+ "business_id": ["doc-ml-001"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "_id": "doc-dl-001",
|
|
|
+ "_score": 0.8756,
|
|
|
+ "fields": {
|
|
|
+ "content": ["深度学习使用神经网络模拟人脑..."],
|
|
|
+ "business_id": ["doc-dl-001"]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.7.5 结果说明
|
|
|
+
|
|
|
+| 排名 | 文档ID | 相似度分数 | 内容摘要 |
|
|
|
+|------|--------|-----------|---------|
|
|
|
+| 🥇 1 | doc-ai-001 | 0.9234 | 人工智能是计算机科学的一个分支... |
|
|
|
+| 🥈 2 | doc-ml-001 | 0.8891 | 机器学习是人工智能的核心技术... |
|
|
|
+| 🥉 3 | doc-dl-001 | 0.8756 | 深度学习使用神经网络模拟人脑... |
|
|
|
+
|
|
|
+**说明:**
|
|
|
+- 分数范围:0~1,越接近1越相似
|
|
|
+- 0.9+:非常相似
|
|
|
+- 0.8+:很相似
|
|
|
+- 0.5以下:不相关
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.8 步骤6:验证与调试
|
|
|
+
|
|
|
+#### 3.8.1 验证索引状态
|
|
|
+
|
|
|
+```bash
|
|
|
+# 查看索引信息
|
|
|
+curl -X GET "http://10.192.72.13:9200/text_vectors?pretty"
|
|
|
+
|
|
|
+# 查看文档数量
|
|
|
+curl -X GET "http://10.192.72.13:9200/_cat/count/text_vectors?v"
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.8.2 查看已入库文档
|
|
|
+
|
|
|
+```bash
|
|
|
+# 查看所有文档
|
|
|
+curl -X GET "http://10.192.72.13:9200/text_vectors/_search?pretty&size=10"
|
|
|
+
|
|
|
+# 查看特定文档
|
|
|
+curl -X GET "http://10.192.72.13:9200/text_vectors/_doc/doc-ai-001?pretty"
|
|
|
+
|
|
|
+# 验证向量维度
|
|
|
+curl -X POST "http://10.192.72.13:9200/text_vectors/_search" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "script_fields": {
|
|
|
+ "vector_dimension": {
|
|
|
+ "script": {
|
|
|
+ "source": "doc['embedding'].size()"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "size": 1
|
|
|
+}'
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.8.3 常见问题排查
|
|
|
+
|
|
|
+| 问题 | 检查方法 | 解决方案 |
|
|
|
+|------|---------|---------|
|
|
|
+| 索引不存在 | `GET /text_vectors` | 执行步骤1创建索引 |
|
|
|
+| 向量维度错误 | 检查返回的向量长度 | 确保4096维 |
|
|
|
+| 搜索无结果 | 检查文档是否入库 | 执行验证命令 |
|
|
|
+| 相似度分数异常 | 检查向量是否归一化 | 确认ES配置 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.9 完整操作检查清单
|
|
|
+
|
|
|
+| 步骤 | 操作 | 命令/验证 | 状态 |
|
|
|
+|------|------|----------|------|
|
|
|
+| ✅ 0 | 环境准备 | curl检查服务 | [ ] |
|
|
|
+| ✅ 1 | 创建ES索引 | PUT /text_vectors | [ ] |
|
|
|
+| ✅ 2 | 准备文本数据 | 准备JSON数组 | [ ] |
|
|
|
+| ✅ 3 | 文本向量化 | POST /embeddings | [ ] |
|
|
|
+| ✅ 4 | 数据入库ES | POST /text_vectors/_doc | [ ] |
|
|
|
+| ✅ 5 | 向量搜索 | POST /text_vectors/_search | [ ] |
|
|
|
+| ✅ 6 | 验证结果 | 检查返回数据 | [ ] |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、完整示例(从文本到搜索)
|
|
|
+
|
|
|
+### 4.1 场景说明
|
|
|
+
|
|
|
+假设我们有一个关于"人工智能"的文档库,需要实现:
|
|
|
+1. 将文档向量化并入库ES
|
|
|
+2. 根据用户查询找到最相似的文档
|
|
|
+
|
|
|
+### 4.2 数据流转示例
|
|
|
+
|
|
|
+#### 步骤1️⃣:文本入库
|
|
|
+
|
|
|
+| 项目 | 值 |
|
|
|
+|------|-----|
|
|
|
+| **输入文本** | "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。" |
|
|
|
+| **业务ID** | doc-ai-001 |
|
|
|
+
|
|
|
+#### 步骤2️⃣:Embedding向量化
|
|
|
+
|
|
|
+| 项目 | 值 |
|
|
|
+|------|-----|
|
|
|
+| **输入文本** | "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。" |
|
|
|
+| **业务ID** | doc-ai-001 |
|
|
|
+
|
|
|
+#### 步骤2️⃣:Embedding向量化
|
|
|
+
|
|
|
+| 项目 | 说明 |
|
|
|
+|------|------|
|
|
|
+| **调用接口** | POST `http://10.192.72.11:18081/v1/embeddings` |
|
|
|
+| **输入** | 文本字符串 |
|
|
|
+| **输出** | 4096维浮点数数组(简化展示) |
|
|
|
+| **向量示例** | `[0.0234, -0.0123, 0.0456, 0.0789, -0.0321, ..., 共4096个]` |
|
|
|
+
|
|
|
+**本质理解:**
|
|
|
+> 把这段话翻译成4096个数字,作为这段话的"数学坐标"
|
|
|
+
|
|
|
+#### 步骤3️⃣:存储到ES
|
|
|
+
|
|
|
+| 字段名 | 值 | 说明 |
|
|
|
+|--------|-----|------|
|
|
|
+| `content` | "人工智能是计算机科学的一个分支..." | 原始文本 |
|
|
|
+| `embedding` | [0.0234, -0.0123, ..., 共4096个] | 4096维向量 |
|
|
|
+| `business_id` | doc-ai-001 | 业务唯一标识(作为ES文档ID) |
|
|
|
+| `create_time` | 2026-03-12T10:30:00 | 创建时间 |
|
|
|
+
|
|
|
+#### 步骤4️⃣:再入库几篇文档(用于对比)
|
|
|
+
|
|
|
+| 文档ID | 内容摘要 | 向量(简化) |
|
|
|
+|--------|----------|-------------|
|
|
|
+| doc-ml-001 | "机器学习是人工智能的核心技术之一..." | `[0.0256, -0.0134, ...]` ✅ 与AI相关,向量接近 |
|
|
|
+| doc-life-001 | "今天天气很好,我去公园散步了。" | `[-0.1123, 0.0456, ...]` ❌ 与AI无关,向量差异大 |
|
|
|
+| doc-dl-001 | "深度学习使用神经网络模拟人脑..." | `[0.0221, -0.0112, ...]` ✅ 与AI相关,向量接近 |
|
|
|
+
|
|
|
+#### 步骤5️⃣:用户搜索
|
|
|
+
|
|
|
+| 项目 | 值 |
|
|
|
+|------|-----|
|
|
|
+| **查询文本** | "什么是AI技术" |
|
|
|
+| **Top-K** | 3 |
|
|
|
+
|
|
|
+#### 步骤6️⃣:查询向量化
|
|
|
+
|
|
|
+| 项目 | 说明 |
|
|
|
+|------|------|
|
|
|
+| **查询文本** | "什么是AI技术" |
|
|
|
+| **查询向量** | `[0.0245, -0.0118, 0.0445, ..., 共4096个]` |
|
|
|
+
|
|
|
+#### 步骤7️⃣:KNN相似度计算
|
|
|
+
|
|
|
+| 对比文档 | 余弦相似度 | 相似程度 |
|
|
|
+|----------|-----------|---------|
|
|
|
+| doc-ai-001 (人工智能...) | **0.92** | ⭐⭐⭐⭐⭐ 最相似 |
|
|
|
+| doc-ml-001 (机器学习...) | **0.89** | ⭐⭐⭐⭐ 很相似 |
|
|
|
+| doc-life-001 (天气...) | **0.23** | ❌ 不相关 |
|
|
|
+| doc-dl-001 (深度学习...) | **0.87** | ⭐⭐⭐⭐ 很相似 |
|
|
|
+
|
|
|
+#### 步骤8️⃣:返回搜索结果
|
|
|
+
|
|
|
+| 排名 | 文档ID | 内容摘要 | 相似度分数 |
|
|
|
+|------|--------|----------|-----------|
|
|
|
+| 🥇 1 | doc-ai-001 | 人工智能是计算机科学的一个分支... | 0.92 |
|
|
|
+| 🥈 2 | doc-ml-001 | 机器学习是人工智能的核心技术... | 0.89 |
|
|
|
+| 🥉 3 | doc-dl-001 | 深度学习使用神经网络模拟人脑... | 0.87 |
|
|
|
+
|
|
|
+### 3.3 核心概念表格总结
|
|
|
+
|
|
|
+| 阶段 | 输入 | 输出 | 本质 |
|
|
|
+|------|------|------|------|
|
|
|
+| **向量化** | 文本:"人工智能技术..." | 4096个数字:`[0.0234, -0.0123, ...]` | 文本 → 数学坐标 |
|
|
|
+| **入库** | 文本 + 向量 | ES文档 | 存储到数据库 |
|
|
|
+| **搜索** | 查询:"什么是AI" | 相似文档列表 | 找向量最近的点 |
|
|
|
+
|
|
|
+### 3.4 类比理解
|
|
|
+
|
|
|
+```
|
|
|
+想象一个4096维的空间:
|
|
|
+
|
|
|
+📍 "人工智能技术发展" → 坐标点A [0.02, 0.03, ...]
|
|
|
+📍 "AI技术进展" → 坐标点B [0.021, 0.029, ...]
|
|
|
+ 两点距离很近 ✅ 相似度高
|
|
|
+
|
|
|
+📍 "人工智能技术发展" → 坐标点A [0.02, 0.03, ...]
|
|
|
+📍 "今天天气不错" → 坐标点C [-0.1, 0.08, ...]
|
|
|
+ 两点距离很远 ❌ 相似度低
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、融合设计方案(企业级最佳实践)
|
|
|
+
|
|
|
+### 5.1 设计理念
|
|
|
+
|
|
|
+对于企业级应用,特别是RAG(检索增强生成)场景,推荐采用**融合设计(宽表设计)**,将元数据、业务数据、向量字段存储在同一个索引中。
|
|
|
+
|
|
|
+#### 5.1.1 核心思想
|
|
|
+
|
|
|
+```
|
|
|
+传统方案(JOIN) 融合设计(宽表)
|
|
|
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
+
|
|
|
+索引1: chunks 索引: contract_chunks
|
|
|
+├─ content ├─ doc_id
|
|
|
+├─ embedding ├─ chunk_id
|
|
|
+└─ file_id (FK) ─────────┐ ├─ content
|
|
|
+ ├─ embedding
|
|
|
+索引2: files ┌────┘ ├─ contract_type
|
|
|
+├─ file_id │ ├─ contract_name
|
|
|
+├─ file_path │ ├─ party_a
|
|
|
+├─ contract_type ────────┘ ├─ party_b
|
|
|
+└─ ... ├─ file_path
|
|
|
+ └─ ...
|
|
|
+
|
|
|
+查询需要JOIN 查询一次搞定 ✅
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.2 设计优势
|
|
|
+
|
|
|
+| 优势 | 说明 | 评分 |
|
|
|
+|------|------|------|
|
|
|
+| **避免JOIN** | ES不支持JOIN,融合设计避免了应用层关联 | ⭐⭐⭐⭐⭐ |
|
|
|
+| **查询性能** | 一次查询返回所有数据,无需多次查询 | ⭐⭐⭐⭐⭐ |
|
|
|
+| **混合过滤** | 支持向量搜索 + 业务字段过滤 | ⭐⭐⭐⭐⭐ |
|
|
|
+| **文件级操作** | 通过doc_id可以操作文件的所有chunk | ⭐⭐⭐⭐⭐ |
|
|
|
+| **扩展性** | 易于添加新字段,无需修改表结构 | ⭐⭐⭐⭐ |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.2 完整Mapping设计(合同文档示例)
|
|
|
+
|
|
|
+#### 5.2.1 索引结构
|
|
|
+
|
|
|
+```json
|
|
|
+PUT /contract_chunks
|
|
|
+{
|
|
|
+ "settings": {
|
|
|
+ "number_of_shards": 3,
|
|
|
+ "number_of_replicas": 1
|
|
|
+ },
|
|
|
+ "mappings": {
|
|
|
+ "properties": {
|
|
|
+ // ========== 1. 文档标识 ==========
|
|
|
+ "doc_id": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "文件ID(与USD文件ID保持一致)"
|
|
|
+ },
|
|
|
+ "chunk_id": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "chunk唯一标识:docId + chunk序号"
|
|
|
+ },
|
|
|
+ "chunk_index": {
|
|
|
+ "type": "integer",
|
|
|
+ "description": "chunk在文档中的序号(0-based)"
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 2. 内容和向量 ==========
|
|
|
+ "content": {
|
|
|
+ "type": "text",
|
|
|
+ "analyzer": "ik_max_word",
|
|
|
+ "description": "chunk的文本内容"
|
|
|
+ },
|
|
|
+ "embedding": {
|
|
|
+ "type": "dense_vector",
|
|
|
+ "dims": 4096,
|
|
|
+ "index": true,
|
|
|
+ "similarity": "cosine",
|
|
|
+ "description": "4096维向量"
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 3. 文件元数据 ==========
|
|
|
+ "file_path": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "文件存储路径"
|
|
|
+ },
|
|
|
+ "file_size": {
|
|
|
+ "type": "long",
|
|
|
+ "description": "文件大小(字节)"
|
|
|
+ },
|
|
|
+ "file_size_mb": {
|
|
|
+ "type": "float",
|
|
|
+ "description": "文件大小(MB)"
|
|
|
+ },
|
|
|
+ "file_type": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "文件类型(pdf/docx/txt)"
|
|
|
+ },
|
|
|
+ "page_count": {
|
|
|
+ "type": "integer",
|
|
|
+ "description": "文档总页数"
|
|
|
+ },
|
|
|
+ "chunk_count": {
|
|
|
+ "type": "integer",
|
|
|
+ "description": "文档总chunk数"
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 4. 业务数据(合同相关) ==========
|
|
|
+ "contract_type": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "合同类型(消费贷/企业贷等)"
|
|
|
+ },
|
|
|
+ "contract_name": {
|
|
|
+ "type": "text",
|
|
|
+ "fields": {
|
|
|
+ "keyword": {
|
|
|
+ "type": "keyword",
|
|
|
+ "ignore_above": 256
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "description": "合同名称"
|
|
|
+ },
|
|
|
+ "party_a": {
|
|
|
+ "type": "text",
|
|
|
+ "fields": {
|
|
|
+ "keyword": {
|
|
|
+ "type": "keyword"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "description": "合同主体甲方"
|
|
|
+ },
|
|
|
+ "party_b": {
|
|
|
+ "type": "text",
|
|
|
+ "fields": {
|
|
|
+ "keyword": {
|
|
|
+ "type": "keyword"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "description": "合同主体乙方"
|
|
|
+ },
|
|
|
+ "contract_amount": {
|
|
|
+ "type": "double",
|
|
|
+ "description": "合同金额"
|
|
|
+ },
|
|
|
+ "sign_date": {
|
|
|
+ "type": "date",
|
|
|
+ "format": "yyyy-MM-dd",
|
|
|
+ "description": "签署日期"
|
|
|
+ },
|
|
|
+ "effective_date": {
|
|
|
+ "type": "date",
|
|
|
+ "format": "yyyy-MM-dd",
|
|
|
+ "description": "生效日期"
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 5. 标签(便于过滤) ==========
|
|
|
+ "tags": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "业务标签"
|
|
|
+ },
|
|
|
+ "department": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "所属部门"
|
|
|
+ },
|
|
|
+ "status": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "合同状态"
|
|
|
+ },
|
|
|
+ "risk_level": {
|
|
|
+ "type": "keyword",
|
|
|
+ "description": "风险等级"
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 6. 时间字段 ==========
|
|
|
+ "create_time": {
|
|
|
+ "type": "date",
|
|
|
+ "format": "yyyy-MM-dd'T'HH:mm:ss",
|
|
|
+ "description": "创建时间"
|
|
|
+ },
|
|
|
+ "update_time": {
|
|
|
+ "type": "date",
|
|
|
+ "format": "yyyy-MM-dd'T'HH:mm:ss",
|
|
|
+ "description": "更新时间"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.3 数据示例
|
|
|
+
|
|
|
+#### 5.3.1 同一文件的多个Chunk
|
|
|
+
|
|
|
+| 字段分类 | Chunk 0 | Chunk 1 | Chunk 2 | 说明 |
|
|
|
+|---------|---------|---------|---------|------|
|
|
|
+| **标识字段** | | | | |
|
|
|
+| doc_id | wo1o23bn2oi3ngo3 | wo1o23bn2oi3ngo3 | wo1o23bn2oi3ngo3 | ✅ 相同 |
|
|
|
+| chunk_id | wo1o23bn2oi3ngo3_0 | wo1o23bn2oi3ngo3_1 | wo1o23bn2oi3ngo3_2 | ❌ 不同 |
|
|
|
+| chunk_index | 0 | 1 | 2 | ❌ 不同 |
|
|
|
+| **内容字段** | | | | |
|
|
|
+| content | "永续贷产品贷款合同..." | "第二条,贷款利率..." | "第三条,还款方式..." | ❌ 不同 |
|
|
|
+| embedding | [向量1] | [向量2] | [向量3] | ❌ 不同 |
|
|
|
+| **元数据(冗余)** | | | | |
|
|
|
+| file_path | /home/data/xxx.pdf | /home/data/xxx.pdf | /home/data/xxx.pdf | ✅ 相同 |
|
|
|
+| file_size_mb | 1.23 | 1.23 | 1.23 | ✅ 相同 |
|
|
|
+| page_count | 10 | 10 | 10 | ✅ 相同 |
|
|
|
+| **业务数据(冗余)** | | | | |
|
|
|
+| contract_type | 消费贷 | 消费贷 | 消费贷 | ✅ 相同 |
|
|
|
+| party_a | A公司 | A公司 | A公司 | ✅ 相同 |
|
|
|
+| party_b | B公司 | B公司 | B公司 | ✅ 相同 |
|
|
|
+| contract_amount | 1000000 | 1000000 | 1000000 | ✅ 相同 |
|
|
|
+
|
|
|
+#### 5.3.2 完整的JSON示例
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "_id": "wo1o23bn2oi3ngo3_chunk_0",
|
|
|
+ "_source": {
|
|
|
+ "doc_id": "wo1o23bn2oi3ngo3",
|
|
|
+ "chunk_id": "wo1o23bn2oi3ngo3_chunk_0",
|
|
|
+ "chunk_index": 0,
|
|
|
+
|
|
|
+ "content": "永续贷产品贷款合同,甲方A公司...",
|
|
|
+ "embedding": [
|
|
|
+ 0.0234, -0.0123, 0.0456, 0.0789, -0.0321,
|
|
|
+ // ... 共4096个 ...
|
|
|
+ 0.0202
|
|
|
+ ],
|
|
|
+
|
|
|
+ "file_path": "/home/data/contracts/永续贷产品.pdf",
|
|
|
+ "file_size": 1289748,
|
|
|
+ "file_size_mb": 1.23,
|
|
|
+ "file_type": "pdf",
|
|
|
+ "page_count": 10,
|
|
|
+ "chunk_count": 5,
|
|
|
+
|
|
|
+ "contract_type": "消费贷贷款合同",
|
|
|
+ "contract_name": "永续贷产品贷款合同",
|
|
|
+ "party_a": "A公司",
|
|
|
+ "party_b": "B公司",
|
|
|
+ "contract_amount": 1000000.00,
|
|
|
+ "sign_date": "2026-01-15",
|
|
|
+ "effective_date": "2026-01-20",
|
|
|
+
|
|
|
+ "tags": ["重要", "长期合作"],
|
|
|
+ "department": "信贷部",
|
|
|
+ "status": "执行中",
|
|
|
+ "risk_level": "低",
|
|
|
+
|
|
|
+ "create_time": "2026-03-12T10:30:00",
|
|
|
+ "update_time": "2026-03-12T10:30:00"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.4 核心操作
|
|
|
+
|
|
|
+#### 5.4.1 混合查询(向量搜索 + 业务过滤)
|
|
|
+
|
|
|
+```bash
|
|
|
+POST /contract_chunks/_search
|
|
|
+{
|
|
|
+ "query": {
|
|
|
+ "bool": {
|
|
|
+ "must": [
|
|
|
+ {
|
|
|
+ "knn": {
|
|
|
+ "field": "embedding",
|
|
|
+ "query_vector": [0.0145, 0.0045, ..., 共4096个],
|
|
|
+ "k": 10,
|
|
|
+ "num_candidates": 50
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "filter": [
|
|
|
+ {"term": {"contract_type": "消费贷贷款合同"}},
|
|
|
+ {"term": {"status": "执行中"}},
|
|
|
+ {"term": {"party_a.keyword": "A公司"}},
|
|
|
+ {"range": {"contract_amount": {"gte": 500000}}},
|
|
|
+ {"range": {"sign_date": {"gte": "2026-01-01"}}}
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "_source": ["content", "contract_name", "party_a", "party_b"],
|
|
|
+ "size": 10
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**返回结果:** 既相似又满足业务条件的chunk
|
|
|
+
|
|
|
+#### 5.4.2 文件级别操作
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 查询某个文件的所有chunk
|
|
|
+POST /contract_chunks/_search
|
|
|
+{
|
|
|
+ "query": {
|
|
|
+ "term": {"doc_id": "wo1o23bn2oi3ngo3"}
|
|
|
+ },
|
|
|
+ "sort": [{"chunk_index": {"order": "asc"}}]
|
|
|
+}
|
|
|
+
|
|
|
+# 2. 删除某个文件的所有chunk
|
|
|
+POST /contract_chunks/_delete_by_query
|
|
|
+{
|
|
|
+ "query": {
|
|
|
+ "term": {"doc_id": "wo1o23bn2oi3ngo3"}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# 3. 更新某个文件的所有chunk(合同信息变更)
|
|
|
+POST /contract_chunks/_update_by_query
|
|
|
+{
|
|
|
+ "query": {
|
|
|
+ "term": {"doc_id": "wo1o23bn2oi3ngo3"}
|
|
|
+ },
|
|
|
+ "script": {
|
|
|
+ "source": """
|
|
|
+ ctx._source.contract_name = params.contract_name;
|
|
|
+ ctx._source.status = params.status;
|
|
|
+ ctx._source.update_time = params.update_time;
|
|
|
+ """,
|
|
|
+ "lang": "painless",
|
|
|
+ "params": {
|
|
|
+ "contract_name": "新的合同名称",
|
|
|
+ "status": "已变更",
|
|
|
+ "update_time": "2026-03-12T15:00:00"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# 4. 统计某个文件的chunk数量
|
|
|
+POST /contract_chunks/_count
|
|
|
+{
|
|
|
+ "query": {
|
|
|
+ "term": {"doc_id": "wo1o23bn2oi3ngo3"}
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.5 数据流转
|
|
|
+
|
|
|
+#### 5.5.1 入库流程
|
|
|
+
|
|
|
+```
|
|
|
+PDF文件上传
|
|
|
+ ↓
|
|
|
+文档解析(提取文本、识别页数)
|
|
|
+ ↓
|
|
|
+文本分段(按页/按段落,生成5个chunk)
|
|
|
+ ↓
|
|
|
+提取业务数据(合同类型、甲方、乙方等)
|
|
|
+ ↓
|
|
|
+批量向量化(调用Embedding接口,5个向量)
|
|
|
+ ↓
|
|
|
+批量入库ES(5个文档,每个包含完整的业务数据)
|
|
|
+ ↓
|
|
|
+验证入库(查询doc_id,应返回5个chunk)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.5.2 搜索流程
|
|
|
+
|
|
|
+```
|
|
|
+用户查询:"A公司的消费贷合同利率是多少"
|
|
|
+ ↓
|
|
|
+1. 查询文本向量化
|
|
|
+ ↓
|
|
|
+2. ES混合查询
|
|
|
+ - KNN向量搜索(相似度)
|
|
|
+ - 过滤条件:contract_type="消费贷" AND party_a="A公司"
|
|
|
+ ↓
|
|
|
+3. 返回Top-K chunk(按相似度排序)
|
|
|
+ ↓
|
|
|
+4. 应用层处理
|
|
|
+ - 按doc_id分组(避免同一文件多个chunk)
|
|
|
+ - 提取最相关的chunk内容
|
|
|
+ - 返回给用户
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.6 设计权衡
|
|
|
+
|
|
|
+#### 5.6.1 优势分析
|
|
|
+
|
|
|
+| 方面 | 传统设计 | 融合设计 | 提升 |
|
|
|
+|------|---------|---------|------|
|
|
|
+| **查询性能** | 需要2次查询+JOIN | 1次查询搞定 | 50%+ |
|
|
|
+| **应用层复杂度** | 需要组装数据 | 直接返回完整数据 | -80% |
|
|
|
+| **混合查询** | 难以实现 | 原生支持 | ✅ |
|
|
|
+| **文件级操作** | 需要多次操作 | 批量操作 | ✅ |
|
|
|
+
|
|
|
+#### 5.6.3 需要注意的问题
|
|
|
+
|
|
|
+| 问题 | 影响 | 解决方案 |
|
|
|
+|------|------|---------|
|
|
|
+| **数据冗余** | 存储空间增加约30% | 定期归档,只保留必要字段 |
|
|
|
+| **更新成本** | 文件变更需更新所有chunk | 使用`_update_by_query`批量更新 |
|
|
|
+| **单文档大小** | 单个文档较大 | 使用`_source`过滤返回字段 |
|
|
|
+
|
|
|
+#### 5.6.3 适用场景
|
|
|
+
|
|
|
+| 场景 | 是否推荐 | 原因 |
|
|
|
+|------|---------|------|
|
|
|
+| ✅ RAG检索 | 强烈推荐 | 需要向量搜索+元数据过滤 |
|
|
|
+| ✅ 多段落文档 | 强烈推荐 | 需要chunk级别的检索 |
|
|
|
+| ✅ 视频/音频 | 强烈推荐 | 需要时间戳+内容检索 |
|
|
|
+| ✅ 企业知识库 | 强烈推荐 | 需要复杂的业务过滤 |
|
|
|
+| ❌ 简单全文搜索 | 不推荐 | 冗余数据浪费空间 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.7 扩展设计
|
|
|
+
|
|
|
+#### 5.7.1 支持多种文档类型
|
|
|
+
|
|
|
+```json
|
|
|
+// 根据document_type字段区分不同类型的业务字段
|
|
|
+{
|
|
|
+ "doc_id": "xxx",
|
|
|
+ "document_type": "contract", // 或 invoice、report等
|
|
|
+
|
|
|
+ // 通用字段
|
|
|
+ "content": "...",
|
|
|
+ "embedding": [...],
|
|
|
+ "file_path": "...",
|
|
|
+
|
|
|
+ // 合同专用字段
|
|
|
+ "contract_type": "...",
|
|
|
+ "party_a": "...",
|
|
|
+
|
|
|
+ // 发票专用字段(可能为空)
|
|
|
+ "invoice_number": null,
|
|
|
+ "invoice_amount": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.7.2 版本管理
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "doc_id": "xxx",
|
|
|
+ "version": 2,
|
|
|
+ "is_latest": true,
|
|
|
+
|
|
|
+ // 支持查询历史版本
|
|
|
+ "content": "更新后的内容...",
|
|
|
+ "embedding": [新向量]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.8 最佳实践总结
|
|
|
+
|
|
|
+| 实践项 | 建议 | 说明 |
|
|
|
+|--------|------|------|
|
|
|
+| **ID设计** | doc_id + chunk_id | 支持文件级操作 |
|
|
|
+| **字段选择** | 只存储必要的业务字段 | 减少冗余 |
|
|
|
+| **更新策略** | 批量更新所有chunk | 保证数据一致性 |
|
|
|
+| **查询优化** | 使用filter替代must | 提高性能 |
|
|
|
+| **分页处理** | 使用search_after | 大数据量友好 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、Java实现方案(JDK1.8 + Spring Boot)
|
|
|
+
|
|
|
+### 6.1 模块结构设计
|
|
|
+
|
|
|
+#### 6.1.1 Maven多模块结构
|
|
|
+
|
|
|
+**完整项目结构:**
|
|
|
+
|
|
|
+```xml
|
|
|
+<project>
|
|
|
+ <modules>
|
|
|
+ <!-- 现有模块 -->
|
|
|
+ <module>schedule-producer</module>
|
|
|
+ <module>schedule-consumer</module>
|
|
|
+ <module>schedule-manager</module>
|
|
|
+ <module>schedule-monitor</module>
|
|
|
+
|
|
|
+ <!-- 新增模块:文本向量化与检索API -->
|
|
|
+ <module>schedule-embedding-api</module>
|
|
|
+ </modules>
|
|
|
+</project>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6.1.2 schedule-embedding-api 模块结构
|
|
|
+
|
|
|
+**完整目录结构:**
|
|
|
+
|
|
|
+```
|
|
|
+schedule-embedding-api/
|
|
|
+├── pom.xml Maven配置
|
|
|
+│
|
|
|
+├── src/main/java/com/schedule/embedding/
|
|
|
+│ ├── ScheduleEmbeddingApplication.java 启动类
|
|
|
+│ │
|
|
|
+│ ├── controller/ 控制器层(REST API)
|
|
|
+│ │ ├── TextEmbeddingController.java 文本入库API
|
|
|
+│ │ ├── VectorSearchController.java 向量搜索API
|
|
|
+│ │ └── DocumentController.java 文档管理API
|
|
|
+│ │
|
|
|
+│ ├── service/ 服务层(业务逻辑)
|
|
|
+│ │ ├── TextEmbeddingService.java 文本向量化服务
|
|
|
+│ │ ├── VectorSearchService.java 向量搜索服务
|
|
|
+│ │ ├── TextSplitter.java 文本分段器
|
|
|
+│ │ └── DocumentService.java 文档管理服务
|
|
|
+│ │
|
|
|
+│ ├── client/ 外部接口客户端
|
|
|
+│ │ └── EmbeddingClient.java Embedding API客户端
|
|
|
+│ │
|
|
|
+│ ├── repository/ ES操作层
|
|
|
+│ │ └── ElasticsearchRepository.java ES封装
|
|
|
+│ │
|
|
|
+│ ├── model/ 数据模型
|
|
|
+│ │ ├── dto/
|
|
|
+│ │ │ ├── IndexRequest.java 入库请求DTO
|
|
|
+│ │ │ ├── BatchIndexRequest.java 批量入库请求DTO
|
|
|
+│ │ │ ├── SearchRequest.java 搜索请求DTO
|
|
|
+│ │ │ └── SearchResult.java 搜索结果DTO
|
|
|
+│ │ ├── entity/
|
|
|
+│ │ │ ├── TextDocument.java 文档实体
|
|
|
+│ │ │ └── Chunk.java 分段实体
|
|
|
+│ │ └── vo/
|
|
|
+│ │ ├── IndexResult.java 入库结果VO
|
|
|
+│ │ └── BatchIndexResult.java 批量入库结果VO
|
|
|
+│ │
|
|
|
+│ ├── config/ 配置类
|
|
|
+│ │ ├── ElasticsearchConfig.java ES配置
|
|
|
+│ │ ├── EmbeddingConfig.java Embedding配置
|
|
|
+│ │ └── ChunkConfig.java 分段配置
|
|
|
+│ │
|
|
|
+│ └── exception/ 异常处理
|
|
|
+│ ├── EmbeddingException.java 向量化异常
|
|
|
+│ └── ElasticsearchException.java ES操作异常
|
|
|
+│
|
|
|
+├── src/main/resources/
|
|
|
+│ ├── application.yml 配置文件
|
|
|
+│ └── logback-spring.xml 日志配置
|
|
|
+│
|
|
|
+└── src/test/java/ 测试代码
|
|
|
+ └── com/schedule/embedding/
|
|
|
+ └── service/
|
|
|
+ └── TextEmbeddingServiceTest.java
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6.1.3 pom.xml 配置
|
|
|
+
|
|
|
+```xml
|
|
|
+<?xml version="1.0" encoding="UTF-8"?>
|
|
|
+<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
|
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
|
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
|
|
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
|
+ <modelVersion>4.0.0</modelVersion>
|
|
|
+
|
|
|
+ <parent>
|
|
|
+ <groupId>com.schedule</groupId>
|
|
|
+ <artifactId>schedule-parent</artifactId>
|
|
|
+ <version>1.0.0</version>
|
|
|
+ </parent>
|
|
|
+
|
|
|
+ <artifactId>schedule-embedding-api</artifactId>
|
|
|
+ <name>schedule-embedding-api</name>
|
|
|
+ <description>文本向量化与向量检索服务</description>
|
|
|
+
|
|
|
+ <properties>
|
|
|
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
|
+ <java.version>1.8</java.version>
|
|
|
+ </properties>
|
|
|
+
|
|
|
+ <dependencies>
|
|
|
+ <!-- Spring Boot Web -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-web</artifactId>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Spring Boot Validation -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-validation</artifactId>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Elasticsearch Rest High Level Client -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.elasticsearch.client</groupId>
|
|
|
+ <artifactId>elasticsearch-rest-high-level-client</artifactId>
|
|
|
+ <version>7.17.15</version>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- HTTP客户端(调用Embedding接口) -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>com.squareup.okhttp3</groupId>
|
|
|
+ <artifactId>okhttp</artifactId>
|
|
|
+ <version>4.9.3</version>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- JSON处理 -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>com.fasterxml.jackson.core</groupId>
|
|
|
+ <artifactId>jackson-databind</artifactId>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Lombok(减少样板代码) -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.projectlombok</groupId>
|
|
|
+ <artifactId>lombok</artifactId>
|
|
|
+ <optional>true</optional>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Apache Commons(工具类) -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.apache.commons</groupId>
|
|
|
+ <artifactId>commons-lang3</artifactId>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- 测试依赖 -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-test</artifactId>
|
|
|
+ <scope>test</scope>
|
|
|
+ </dependency>
|
|
|
+ </dependencies>
|
|
|
+
|
|
|
+ <build>
|
|
|
+ <plugins>
|
|
|
+ <plugin>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-maven-plugin</artifactId>
|
|
|
+ </plugin>
|
|
|
+ </plugins>
|
|
|
+ </build>
|
|
|
+</project>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6.1.4 与其他模块的交互关系
|
|
|
+
|
|
|
+**模块交互图:**
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ schedule-producer │
|
|
|
+│ 文档上传任务生产者 │
|
|
|
+└──────────────────────────────┬──────────────────────────────────┘
|
|
|
+ │ 发送文档入库消息
|
|
|
+ ↓
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ schedule-consumer │
|
|
|
+│ 文档入库任务消费者 │
|
|
|
+└──────────────────────────────┬──────────────────────────────────┘
|
|
|
+ │ 调用入库API
|
|
|
+ ↓
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ schedule-embedding-api │
|
|
|
+│ 文本向量化与向量检索API服务 │
|
|
|
+│ ┌─────────────────────────────────────────────────────────────┐ │
|
|
|
+│ │ 1. 文本分段(TextSplitter) │ │
|
|
|
+│ │ 2. 向量化(EmbeddingClient → Embedding接口) │ │
|
|
|
+│ │ 3. 入库ES(ElasticsearchRepository) │ │
|
|
|
+│ │ 4. 向量搜索(KNN查询) │ │
|
|
|
+│ └─────────────────────────────────────────────────────────────┘ │
|
|
|
+└──────────────────────────────┬──────────────────────────────────┘
|
|
|
+ │
|
|
|
+ ┌──────────────┴──────────────┐
|
|
|
+ ↓ ↓
|
|
|
+┌───────────────────────┐ ┌─────────────────────────┐
|
|
|
+│ Embedding接口 │ │ Elasticsearch │
|
|
|
+│ http://10.192.72.11 │ │ http://10.192.72.13 │
|
|
|
+│ :18081/v1 │ │ :9200 │
|
|
|
+└───────────────────────┘ └─────────────────────────┘
|
|
|
+ ↑ ↑
|
|
|
+ │ 监控 │ 监控
|
|
|
+ └──────────────┬──────────────┘
|
|
|
+ ↓
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ schedule-monitor │
|
|
|
+│ 监控告警服务 │
|
|
|
+└─────────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6.1.5 核心功能模块
|
|
|
+
|
|
|
+| 功能模块 | 类名 | 职责 |
|
|
|
+|---------|------|------|
|
|
|
+| **文本分段** | TextSplitter | 将全量文本分段为chunks |
|
|
|
+| **向量化** | EmbeddingClient | 调用Embedding接口获取向量 |
|
|
|
+| **入库服务** | TextEmbeddingService | 协调分段、向量化、入库流程 |
|
|
|
+| **ES操作** | ElasticsearchRepository | 封装ES操作(索引、查询、删除) |
|
|
|
+| **向量搜索** | VectorSearchService | KNN向量搜索服务 |
|
|
|
+| **文档管理** | DocumentService | 文档级别的CRUD操作 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6.1.6 API接口设计
|
|
|
+
|
|
|
+**RESTful API列表:**
|
|
|
+
|
|
|
+| 功能 | HTTP方法 | 路径 | 说明 |
|
|
|
+|------|---------|------|------|
|
|
|
+| **单个文档入库** | POST | /api/v1/documents/index | 上传完整文档,自动分段入库 |
|
|
|
+| **批量文档入库** | POST | /api/v1/documents/batch-index | 批量上传文档 |
|
|
|
+| **向量搜索** | POST | /api/v1/search | 向量相似度搜索 |
|
|
|
+| **混合搜索** | POST | /api/v1/search/hybrid | 向量+业务条件过滤 |
|
|
|
+| **查询文档** | GET | /api/v1/documents/{docId} | 查询文档的所有chunks |
|
|
|
+| **删除文档** | DELETE | /api/v1/documents/{docId} | 删除文档的所有chunks |
|
|
|
+| **更新文档** | PUT | /api/v1/documents/{docId} | 更新文档的所有chunks |
|
|
|
+| **健康检查** | GET | /actuator/health | 服务健康状态 |
|
|
|
+
|
|
|
+**API示例:**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 单个文档入库
|
|
|
+POST http://localhost:8080/api/v1/documents/index
|
|
|
+Content-Type: application/json
|
|
|
+
|
|
|
+{
|
|
|
+ "docId": "wo1o23bn2oi3ngo3",
|
|
|
+ "fileName": "永续贷产品贷款合同.pdf",
|
|
|
+ "fullText": "永续贷产品贷款合同\n\n第一章:总则...",
|
|
|
+ "metadata": {
|
|
|
+ "contractType": "消费贷贷款合同",
|
|
|
+ "partyA": "A公司",
|
|
|
+ "partyB": "B公司",
|
|
|
+ "contractAmount": 1000000.00
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# 2. 向量搜索
|
|
|
+POST http://localhost:8080/api/v1/search
|
|
|
+Content-Type: application/json
|
|
|
+
|
|
|
+{
|
|
|
+ "query": "A公司的消费贷合同利率是多少",
|
|
|
+ "topK": 5,
|
|
|
+ "filters": {
|
|
|
+ "contractType": "消费贷贷款合同",
|
|
|
+ "partyA": "A公司",
|
|
|
+ "status": "执行中"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# 3. 查询文档的所有chunks
|
|
|
+GET http://localhost:8080/api/v1/documents/wo1o23bn2oi3ngo3?sortBy=chunkIndex&order=asc
|
|
|
+
|
|
|
+# 4. 删除文档(删除所有chunks)
|
|
|
+DELETE http://localhost:8080/api/v1/documents/wo1o23bn2oi3ngo3
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6.1.7 application.yml 配置
|
|
|
+
|
|
|
+```yaml
|
|
|
+server:
|
|
|
+ port: 8080
|
|
|
+ servlet:
|
|
|
+ context-path: /
|
|
|
+
|
|
|
+spring:
|
|
|
+ application:
|
|
|
+ name: schedule-embedding-api
|
|
|
+
|
|
|
+# ========== Embedding接口配置 ==========
|
|
|
+embedding:
|
|
|
+ api:
|
|
|
+ url: http://10.192.72.11:18081/v1
|
|
|
+ key: 1
|
|
|
+ model: Qwen3-Embedding-8B
|
|
|
+ timeout: 30000
|
|
|
+ batch:
|
|
|
+ size: 128
|
|
|
+
|
|
|
+# ========== 文本分段配置 ==========
|
|
|
+chunk:
|
|
|
+ max-length: 500 # 最大chunk长度(字)
|
|
|
+ min-length: 100 # 最小chunk长度(字)
|
|
|
+ overlap: 50 # 重叠大小(字)
|
|
|
+ mode: paragraph # 分段模式:paragraph/fixed/semantic
|
|
|
+
|
|
|
+# ========== Elasticsearch配置 ==========
|
|
|
+elasticsearch:
|
|
|
+ host: http://10.192.72.13:9200
|
|
|
+ index:
|
|
|
+ name: contract_chunks
|
|
|
+ shards: 3
|
|
|
+ replicas: 1
|
|
|
+ connection:
|
|
|
+ timeout: 30000
|
|
|
+ max-retry-timeout-millis: 30000
|
|
|
+
|
|
|
+# ========== 监控配置 ==========
|
|
|
+management:
|
|
|
+ endpoints:
|
|
|
+ web:
|
|
|
+ exposure:
|
|
|
+ include: health,info,metrics
|
|
|
+ metrics:
|
|
|
+ export:
|
|
|
+ prometheus:
|
|
|
+ enabled: true
|
|
|
+
|
|
|
+# ========== 日志配置 ==========
|
|
|
+logging:
|
|
|
+ level:
|
|
|
+ com.schedule.embedding: INFO
|
|
|
+ org.elasticsearch.client: WARN
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6.2 核心依赖
|
|
|
+
|
|
|
+```xml
|
|
|
+<!-- Elasticsearch Rest High Level Client (JDK1.8兼容) -->
|
|
|
+<dependency>
|
|
|
+ <groupId>org.elasticsearch.client</groupId>
|
|
|
+ <artifactId>elasticsearch-rest-high-level-client</artifactId>
|
|
|
+ <version>7.17.15</version>
|
|
|
+</dependency>
|
|
|
+
|
|
|
+<!-- HTTP客户端(调用Embedding接口) -->
|
|
|
+<dependency>
|
|
|
+ <groupId>okhttp3</groupId>
|
|
|
+ <artifactId>okhttp</artifactId>
|
|
|
+ <version>4.9.3</version>
|
|
|
+</dependency>
|
|
|
+
|
|
|
+<!-- JSON处理 -->
|
|
|
+<dependency>
|
|
|
+ <groupId>com.fasterxml.jackson.core</groupId>
|
|
|
+ <artifactId>jackson-databind</artifactId>
|
|
|
+</dependency>
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 代码结构设计
|
|
|
+
|
|
|
+#### 6.2.1 Controller层
|
|
|
+
|
|
|
+**TextEmbeddingController.java**
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/text-embedding")
|
|
|
+public class TextEmbeddingController {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TextEmbeddingService embeddingService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单个文本入库
|
|
|
+ * POST /api/text-embedding/index
|
|
|
+ * Body: { "content": "文本内容", "businessId": "业务ID" }
|
|
|
+ */
|
|
|
+ @PostMapping("/index")
|
|
|
+ public ResponseEntity<IndexResult> indexText(@RequestBody IndexRequest request) {
|
|
|
+ // 参数校验
|
|
|
+ // 调用Service层
|
|
|
+ // 返回结果
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量文本入库
|
|
|
+ * POST /api/text-embedding/batch-index
|
|
|
+ * Body: { "items": [{"content": "...", "businessId": "..."}] }
|
|
|
+ */
|
|
|
+ @PostMapping("/batch-index")
|
|
|
+ public ResponseEntity<BatchIndexResult> batchIndex(@RequestBody BatchIndexRequest request) {
|
|
|
+ // 批量处理(最多128条)
|
|
|
+ // 返回批量结果
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 向量相似度搜索
|
|
|
+ * POST /api/text-embedding/search
|
|
|
+ * Body: { "query": "查询文本", "topK": 10 }
|
|
|
+ */
|
|
|
+ @PostMapping("/search")
|
|
|
+ public ResponseEntity<List<SearchResult>> search(@RequestBody SearchRequest request) {
|
|
|
+ // 调用Service层搜索
|
|
|
+ // 返回相似文档列表
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.2.2 Service层
|
|
|
+
|
|
|
+**TextEmbeddingService.java**
|
|
|
+
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class TextEmbeddingService {
|
|
|
+
|
|
|
+ @Value("${embedding.api.url}")
|
|
|
+ private String embeddingApiUrl;
|
|
|
+
|
|
|
+ @Value("${embedding.api.key}")
|
|
|
+ private String embeddingApiKey;
|
|
|
+
|
|
|
+ @Value("${embedding.batch.size:128}")
|
|
|
+ private int batchSize;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private EmbeddingClient embeddingClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ElasticsearchRepository esRepository;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单个文本入库流程
|
|
|
+ */
|
|
|
+ public IndexResult indexText(String content, String businessId) {
|
|
|
+ // 1. 文本预处理
|
|
|
+ String processedText = preprocessText(content);
|
|
|
+
|
|
|
+ // 2. 调用Embedding接口
|
|
|
+ float[] vector = embeddingClient.getEmbedding(processedText);
|
|
|
+
|
|
|
+ // 3. 构建ES文档
|
|
|
+ TextDocument doc = buildDocument(processedText, vector, businessId);
|
|
|
+
|
|
|
+ // 4. 写入ES
|
|
|
+ String docId = esRepository.indexDocument(doc);
|
|
|
+
|
|
|
+ return IndexResult.success(docId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量入库(支持分批处理)
|
|
|
+ */
|
|
|
+ public BatchIndexResult batchIndex(List<IndexItem> items) {
|
|
|
+ // 按batchSize分批
|
|
|
+ // 每批调用embedding接口批量向量化
|
|
|
+ // 批量写入ES(Bulk API)
|
|
|
+ // 返回汇总结果
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 向量搜索
|
|
|
+ */
|
|
|
+ public List<SearchResult> search(String query, int topK) {
|
|
|
+ // 1. 查询文本向量化
|
|
|
+ float[] queryVector = embeddingClient.getEmbedding(query);
|
|
|
+
|
|
|
+ // 2. KNN搜索
|
|
|
+ return esRepository.knnSearch(queryVector, topK);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.2.3 EmbeddingClient层
|
|
|
+
|
|
|
+**EmbeddingClient.java**
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+public class EmbeddingClient {
|
|
|
+
|
|
|
+ @Value("${embedding.api.url}")
|
|
|
+ private String apiUrl;
|
|
|
+
|
|
|
+ @Value("${embedding.api.key}")
|
|
|
+ private String apiKey;
|
|
|
+
|
|
|
+ private OkHttpClient httpClient;
|
|
|
+ private ObjectMapper objectMapper;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单个文本向量化
|
|
|
+ */
|
|
|
+ public float[] getEmbedding(String text) {
|
|
|
+ // 构建请求
|
|
|
+ // HTTP POST调用
|
|
|
+ // 解析返回的4096维向量
|
|
|
+ // 返回float[]
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量文本向量化
|
|
|
+ */
|
|
|
+ public List<float[]> getBatchEmbeddings(List<String> texts) {
|
|
|
+ // 批量请求(最多128条)
|
|
|
+ // 解析批量响应
|
|
|
+ // 返回向量列表
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重试机制(指数退避)
|
|
|
+ */
|
|
|
+ private float[] callWithRetry(String text, int maxRetries) {
|
|
|
+ // 失败重试逻辑
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.2.4 ElasticsearchRepository层
|
|
|
+
|
|
|
+**ElasticsearchRepository.java**
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+public class ElasticsearchRepository {
|
|
|
+
|
|
|
+ @Value("${elasticsearch.host}")
|
|
|
+ private String esHost;
|
|
|
+
|
|
|
+ private RestHighLevelClient client;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建向量索引
|
|
|
+ */
|
|
|
+ public boolean createIndex(String indexName) {
|
|
|
+ // 使用dense_vector字段类型
|
|
|
+ // 设置cosine相似度
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 索引单个文档
|
|
|
+ */
|
|
|
+ public String indexDocument(TextDocument doc) {
|
|
|
+ // 使用businessId作为文档ID(幂等性)
|
|
|
+ // IndexRequest
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量索引文档
|
|
|
+ */
|
|
|
+ public BulkResponse bulkIndex(List<TextDocument> docs) {
|
|
|
+ // BulkRequest
|
|
|
+ // 批量写入优化
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * KNN向量搜索
|
|
|
+ */
|
|
|
+ public List<SearchResult> knnSearch(float[] queryVector, int topK) {
|
|
|
+ // 使用KNN查询
|
|
|
+ // 返回Top-K相似文档
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 配置文件
|
|
|
+
|
|
|
+**application.yml**
|
|
|
+
|
|
|
+```yaml
|
|
|
+# Embedding接口配置
|
|
|
+embedding:
|
|
|
+ api:
|
|
|
+ url: http://10.192.72.11:18081/v1
|
|
|
+ key: 1
|
|
|
+ model: Qwen3-Embedding-8B
|
|
|
+ batch:
|
|
|
+ size: 128
|
|
|
+ timeout: 30000
|
|
|
+
|
|
|
+# Elasticsearch配置
|
|
|
+elasticsearch:
|
|
|
+ host: http://10.192.72.13:9200
|
|
|
+ index:
|
|
|
+ name: text_vectors
|
|
|
+ shards: 1
|
|
|
+ replicas: 0
|
|
|
+
|
|
|
+# 服务配置
|
|
|
+server:
|
|
|
+ port: 8080
|
|
|
+
|
|
|
+spring:
|
|
|
+ application:
|
|
|
+ name: text-embedding-service
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、核心数据结构
|
|
|
+
|
|
|
+### 5.1 ES文档结构
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "id": "文档唯一ID(ES自动生成)",
|
|
|
+ "content": "原始文本内容",
|
|
|
+ "embedding": [0.0123, 0.0456, ...], // 4096维向量
|
|
|
+ "business_id": "业务ID(作为ES文档ID,保证幂等性)",
|
|
|
+ "content_length": 256,
|
|
|
+ "create_time": "2026-03-12T10:00:00",
|
|
|
+ "update_time": "2026-03-12T10:00:00"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 索引映射设计
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "mappings": {
|
|
|
+ "properties": {
|
|
|
+ "content": {
|
|
|
+ "type": "text",
|
|
|
+ "analyzer": "ik_max_word"
|
|
|
+ },
|
|
|
+ "embedding": {
|
|
|
+ "type": "dense_vector",
|
|
|
+ "dims": 4096,
|
|
|
+ "index": true,
|
|
|
+ "similarity": "cosine"
|
|
|
+ },
|
|
|
+ "business_id": {
|
|
|
+ "type": "keyword"
|
|
|
+ },
|
|
|
+ "content_length": {
|
|
|
+ "type": "integer"
|
|
|
+ },
|
|
|
+ "create_time": {
|
|
|
+ "type": "date",
|
|
|
+ "format": "yyyy-MM-dd'T'HH:mm:ss"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、可靠性与性能保障
|
|
|
+
|
|
|
+### 6.1 批量处理策略
|
|
|
+
|
|
|
+**批次大小控制:**
|
|
|
+- 单次最多处理128条文本(EMBEDDING_BATCH_SIZE)
|
|
|
+- 超过128条自动分批处理
|
|
|
+- 每批独立处理,失败不影响其他批次
|
|
|
+
|
|
|
+**分批示例:**
|
|
|
+```java
|
|
|
+// 假设有500条文本
|
|
|
+List<List<IndexItem>> batches = Lists.partition(items, 128);
|
|
|
+// 得到4个批次:128, 128, 128, 116
|
|
|
+for (List<IndexItem> batch : batches) {
|
|
|
+ processBatch(batch); // 每批独立处理
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 异常处理与重试
|
|
|
+
|
|
|
+**Embedding接口调用:**
|
|
|
+- 超时时间:30秒
|
|
|
+- 最大重试次数:3次
|
|
|
+- 重试策略:指数退避(1s, 2s, 4s)
|
|
|
+- 失败记录:记录失败文本,支持手动重试
|
|
|
+
|
|
|
+**ES写入异常:**
|
|
|
+- Bulk部分失败:记录失败文档ID
|
|
|
+- 网络异常:自动重试
|
|
|
+- 索引不存在:自动创建索引
|
|
|
+
|
|
|
+### 6.3 幂等性设计
|
|
|
+
|
|
|
+**使用businessId作为ES文档ID:**
|
|
|
+```java
|
|
|
+// 相同的businessId重复入库会覆盖,不会产生重复数据
|
|
|
+String docId = businessId; // 业务ID作为ES文档ID
|
|
|
+IndexRequest request = new IndexRequest(indexName)
|
|
|
+ .id(docId) // 设置文档ID
|
|
|
+ .source(buildSource(doc));
|
|
|
+```
|
|
|
+
|
|
|
+### 8.4 性能优化
|
|
|
+
|
|
|
+**Embedding接口:**
|
|
|
+- 使用HTTP连接池
|
|
|
+- 批量请求减少网络开销
|
|
|
+- 异步处理(适用于大批量)
|
|
|
+
|
|
|
+**ES写入优化:**
|
|
|
+- 使用Bulk API批量写入
|
|
|
+- 设置合理的refresh_interval(如30s)
|
|
|
+- 批量大小控制在100-500条
|
|
|
+
|
|
|
+**KNN搜索优化:**
|
|
|
+- 使用knn查询而非script_score
|
|
|
+- 设置合理的num_candidates参数
|
|
|
+- 结合过滤条件减少搜索范围
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、关键流程说明
|
|
|
+
|
|
|
+### 5.1 文本入库详细流程
|
|
|
+
|
|
|
+```java
|
|
|
+public IndexResult indexText(String content, String businessId) {
|
|
|
+ // 步骤1:参数校验
|
|
|
+ if (StringUtils.isBlank(content)) {
|
|
|
+ throw new IllegalArgumentException("内容不能为空");
|
|
|
+ }
|
|
|
+ if (content.length() > 8192) { // 假设最大长度8192
|
|
|
+ throw new IllegalArgumentException("内容长度超限");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤2:文本预处理
|
|
|
+ String processedText = content.trim();
|
|
|
+
|
|
|
+ // 步骤3:调用Embedding接口(带重试)
|
|
|
+ float[] vector = embeddingClient.getEmbedding(processedText);
|
|
|
+ if (vector == null || vector.length != 4096) {
|
|
|
+ throw new RuntimeException("向量化失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤4:构建文档对象
|
|
|
+ TextDocument doc = TextDocument.builder()
|
|
|
+ .content(processedText)
|
|
|
+ .embedding(vector)
|
|
|
+ .businessId(businessId)
|
|
|
+ .contentLength(processedText.length())
|
|
|
+ .createTime(new Date())
|
|
|
+ .build();
|
|
|
+
|
|
|
+ // 步骤5:写入ES
|
|
|
+ String docId = esRepository.indexDocument(doc);
|
|
|
+
|
|
|
+ // 步骤6:返回结果
|
|
|
+ return IndexResult.builder()
|
|
|
+ .success(true)
|
|
|
+ .docId(docId)
|
|
|
+ .build();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 批量入库详细流程
|
|
|
+
|
|
|
+```java
|
|
|
+public BatchIndexResult batchIndex(List<IndexItem> items) {
|
|
|
+ // 步骤1:参数校验
|
|
|
+ if (items == null || items.isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("批次不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤2:分批处理
|
|
|
+ List<List<IndexItem>> batches = Lists.partition(items, batchSize);
|
|
|
+
|
|
|
+ int totalSuccess = 0;
|
|
|
+ int totalFailed = 0;
|
|
|
+ List<String> failedItems = new ArrayList<>();
|
|
|
+
|
|
|
+ // 步骤3:逐批处理
|
|
|
+ for (List<IndexItem> batch : batches) {
|
|
|
+ try {
|
|
|
+ // 3.1 批量向量化
|
|
|
+ List<String> texts = batch.stream()
|
|
|
+ .map(IndexItem::getContent)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ List<float[]> vectors = embeddingClient.getBatchEmbeddings(texts);
|
|
|
+
|
|
|
+ // 3.2 构建文档列表
|
|
|
+ List<TextDocument> docs = new ArrayList<>();
|
|
|
+ for (int i = 0; i < batch.size(); i++) {
|
|
|
+ TextDocument doc = buildDocument(
|
|
|
+ batch.get(i),
|
|
|
+ vectors.get(i)
|
|
|
+ );
|
|
|
+ docs.add(doc);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3 批量写入ES
|
|
|
+ BulkResponse response = esRepository.bulkIndex(docs);
|
|
|
+
|
|
|
+ // 3.4 统计结果
|
|
|
+ if (response.hasFailures()) {
|
|
|
+ // 处理失败项
|
|
|
+ totalFailed += countFailed(response);
|
|
|
+ failedItems.addAll(getFailedItems(response));
|
|
|
+ } else {
|
|
|
+ totalSuccess += docs.size();
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 整批失败
|
|
|
+ totalFailed += batch.size();
|
|
|
+ batch.forEach(item -> failedItems.add(item.getBusinessId()));
|
|
|
+ log.error("批次处理失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤4:返回汇总结果
|
|
|
+ return BatchIndexResult.builder()
|
|
|
+ .totalCount(items.size())
|
|
|
+ .successCount(totalSuccess)
|
|
|
+ .failedCount(totalFailed)
|
|
|
+ .failedItems(failedItems)
|
|
|
+ .build();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.3 向量搜索详细流程
|
|
|
+
|
|
|
+```java
|
|
|
+public List<SearchResult> search(String query, int topK) {
|
|
|
+ // 步骤1:查询向量化
|
|
|
+ float[] queryVector = embeddingClient.getEmbedding(query);
|
|
|
+
|
|
|
+ // 步骤2:KNN搜索
|
|
|
+ SearchRequest searchRequest = new SearchRequest(indexName);
|
|
|
+ SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
|
|
|
+
|
|
|
+ // 构建KNN查询
|
|
|
+ sourceBuilder.query(QueryBuilders.knnQuery(
|
|
|
+ "embedding",
|
|
|
+ queryVector,
|
|
|
+ topK
|
|
|
+ ).numCandidates(100)); // 候选文档数
|
|
|
+
|
|
|
+ searchRequest.source(sourceBuilder);
|
|
|
+
|
|
|
+ // 步骤3:执行搜索
|
|
|
+ SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
|
|
|
+
|
|
|
+ // 步骤4:解析结果
|
|
|
+ List<SearchResult> results = new ArrayList<>();
|
|
|
+ for (SearchHit hit : response.getHits().getHits()) {
|
|
|
+ Map<String, Object> sourceMap = hit.getSourceAsMap();
|
|
|
+ float score = hit.getScore();
|
|
|
+
|
|
|
+ SearchResult result = SearchResult.builder()
|
|
|
+ .docId(hit.getId())
|
|
|
+ .content((String) sourceMap.get("content"))
|
|
|
+ .businessId((String) sourceMap.get("business_id"))
|
|
|
+ .score(score)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ results.add(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ return results;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十、部署与配置
|
|
|
+
|
|
|
+### 6.1 应用配置清单
|
|
|
+
|
|
|
+**必需配置:**
|
|
|
+- `embedding.api.url`:Embedding接口地址
|
|
|
+- `embedding.api.key`:API密钥
|
|
|
+- `embedding.batch.size`:批量大小(默认128)
|
|
|
+- `elasticsearch.host`:ES地址
|
|
|
+- `elasticsearch.index.name`:索引名称
|
|
|
+
|
|
|
+**可选配置:**
|
|
|
+- `embedding.timeout`:接口超时时间(毫秒)
|
|
|
+- `elasticsearch.index.shards`:索引分片数
|
|
|
+- `elasticsearch.index.replicas`:索引副本数
|
|
|
+
|
|
|
+### 6.2 初始化检查
|
|
|
+
|
|
|
+**应用启动时检查:**
|
|
|
+1. Embedding接口连通性
|
|
|
+2. ES集群连接状态
|
|
|
+3. 索引是否存在(不存在则创建)
|
|
|
+4. 索引映射是否正确
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十一、监控与日志
|
|
|
+
|
|
|
+### 5.1 关键指标监控
|
|
|
+
|
|
|
+- **Embedding接口:**
|
|
|
+ - 调用次数
|
|
|
+ - 平均响应时间
|
|
|
+ - 失败率
|
|
|
+
|
|
|
+- **ES操作:**
|
|
|
+ - 文档写入数量
|
|
|
+ - 搜索请求次数
|
|
|
+ - 平均查询时间
|
|
|
+
|
|
|
+### 5.2 日志记录
|
|
|
+
|
|
|
+**关键日志点:**
|
|
|
+- 入库请求:businessId、内容长度
|
|
|
+- Embedding调用:请求ID、响应时间
|
|
|
+- ES写入:文档ID、写入结果
|
|
|
+- 异常情况:异常类型、错误信息、堆栈
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十二、总结
|
|
|
+
|
|
|
+### 6.1 方案优势
|
|
|
+
|
|
|
+1. **高可用性:**
|
|
|
+ - 异常重试机制
|
|
|
+ - 批量处理失败隔离
|
|
|
+ - 幂等性设计
|
|
|
+
|
|
|
+2. **高性能:**
|
|
|
+ - 批量向量化(128条/批)
|
|
|
+ - ES Bulk API批量写入
|
|
|
+ - HTTP连接池复用
|
|
|
+
|
|
|
+3. **可扩展性:**
|
|
|
+ - 分层架构清晰
|
|
|
+ - 配置灵活
|
|
|
+ - 易于扩展新功能
|
|
|
+
|
|
|
+### 5.2 下一步工作
|
|
|
+
|
|
|
+1. **代码实现:**
|
|
|
+ - 按照上述结构编写完整代码
|
|
|
+ - 单元测试覆盖
|
|
|
+ - 集成测试验证
|
|
|
+
|
|
|
+2. **性能测试:**
|
|
|
+ - 压力测试
|
|
|
+ - 批量入库性能测试
|
|
|
+ - 搜索性能测试
|
|
|
+
|
|
|
+3. **优化迭代:**
|
|
|
+ - 根据测试结果优化参数
|
|
|
+ - 添加缓存机制(如需要)
|
|
|
+ - 优化索引配置
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十三、附录:接口测试命令
|
|
|
+
|
|
|
+### 13.1 Embedding接口测试
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST "http://10.192.72.11:18081/v1/embeddings" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "Authorization: Bearer 1" \
|
|
|
+ -d '{
|
|
|
+ "model": "Qwen3-Embedding-8B",
|
|
|
+ "input": "测试文本内容"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+### 13.2 ES集群状态测试
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X GET "http://10.192.72.13:9200/_cluster/health?pretty"
|
|
|
+```
|
|
|
+
|
|
|
+### 13.3 创建向量索引
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X PUT "http://10.192.72.13:9200/text_vectors" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{
|
|
|
+ "settings": {
|
|
|
+ "number_of_shards": 1,
|
|
|
+ "number_of_replicas": 0
|
|
|
+ },
|
|
|
+ "mappings": {
|
|
|
+ "properties": {
|
|
|
+ "content": { "type": "text" },
|
|
|
+ "embedding": {
|
|
|
+ "type": "dense_vector",
|
|
|
+ "dims": 4096,
|
|
|
+ "index": true,
|
|
|
+ "similarity": "cosine"
|
|
|
+ },
|
|
|
+ "business_id": { "type": "keyword" },
|
|
|
+ "create_time": { "type": "date" }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}'
|
|
|
+```
|
|
|
+
|
|
|
+### 13.4 查看索引映射
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X GET "http://10.192.72.13:9200/text_vectors/_mapping?pretty"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**文档版本:** v1.5
|
|
|
+**编写日期:** 2026-03-12
|
|
|
+**更新日期:** 2026-03-12
|
|
|
+**更新内容:**
|
|
|
+- v1.1: 新增完整示例章节(从文本到搜索的完整流程)
|
|
|
+- v1.2: 新增完整操作步骤(从零开始,7个步骤详细说明)
|
|
|
+- v1.3: 新增融合设计方案(企业级最佳实践,宽表设计)
|
|
|
+- v1.4: 新增文本分段策略与实现(全量文本分段处理)
|
|
|
+- v1.5: 新增模块结构设计(schedule-embedding-api完整架构)
|
|
|
+**测试环境:** 内网环境验证通过
|