# 文本向量化与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完整架构)
**测试环境:** 内网环境验证通过