# 文本向量化与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数切分 | 通用文档 | ✅ 简单
❌ 可能切断语义 | | **段落分段** | 按自然段落切分 | 结构化文档 | ✅ 保持语义完整
❌ 段落长度不均 | | **语义分段** | 基于语义相似度 | 重要文档 | ✅ 语义完整
❌ 需要额外模型 | | **滑动窗口** | 重叠切分 | 避免边界丢失 | ✅ 防止信息丢失
❌ 数据冗余 | | **混合策略** | 综合多种方法 | 生产环境推荐 | ✅ 平衡效果和性能 | --- #### 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 split(String fullText, String docId) { List chunks = new ArrayList<>(); // 1. 预处理 String text = preprocess(fullText); // 2. 按段落切分 List 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 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 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标签去除 | 正则替换 | `"

文本

"` → `"文本"` | --- ### 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 schedule-producer schedule-consumer schedule-manager schedule-monitor schedule-embedding-api ``` --- #### 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 4.0.0 com.schedule schedule-parent 1.0.0 schedule-embedding-api schedule-embedding-api 文本向量化与向量检索服务 UTF-8 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.elasticsearch.client elasticsearch-rest-high-level-client 7.17.15 com.squareup.okhttp3 okhttp 4.9.3 com.fasterxml.jackson.core jackson-databind org.projectlombok lombok true org.apache.commons commons-lang3 org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ``` --- #### 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 org.elasticsearch.client elasticsearch-rest-high-level-client 7.17.15 okhttp3 okhttp 4.9.3 com.fasterxml.jackson.core jackson-databind ``` ### 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 indexText(@RequestBody IndexRequest request) { // 参数校验 // 调用Service层 // 返回结果 } /** * 批量文本入库 * POST /api/text-embedding/batch-index * Body: { "items": [{"content": "...", "businessId": "..."}] } */ @PostMapping("/batch-index") public ResponseEntity batchIndex(@RequestBody BatchIndexRequest request) { // 批量处理(最多128条) // 返回批量结果 } /** * 向量相似度搜索 * POST /api/text-embedding/search * Body: { "query": "查询文本", "topK": 10 } */ @PostMapping("/search") public ResponseEntity> 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 items) { // 按batchSize分批 // 每批调用embedding接口批量向量化 // 批量写入ES(Bulk API) // 返回汇总结果 } /** * 向量搜索 */ public List 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 getBatchEmbeddings(List 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 docs) { // BulkRequest // 批量写入优化 } /** * KNN向量搜索 */ public List 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> batches = Lists.partition(items, 128); // 得到4个批次:128, 128, 128, 116 for (List 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 items) { // 步骤1:参数校验 if (items == null || items.isEmpty()) { throw new IllegalArgumentException("批次不能为空"); } // 步骤2:分批处理 List> batches = Lists.partition(items, batchSize); int totalSuccess = 0; int totalFailed = 0; List failedItems = new ArrayList<>(); // 步骤3:逐批处理 for (List batch : batches) { try { // 3.1 批量向量化 List texts = batch.stream() .map(IndexItem::getContent) .collect(Collectors.toList()); List vectors = embeddingClient.getBatchEmbeddings(texts); // 3.2 构建文档列表 List 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 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 results = new ArrayList<>(); for (SearchHit hit : response.getHits().getHits()) { Map 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完整架构) **测试环境:** 内网环境验证通过