配置信息:
http://10.192.72.11:18081/v11Qwen3-Embedding-8B4096128测试结果: ✅ 可用
请求示例:
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": "测试文本内容"
}'
配置信息:
http://10.192.72.13:92008.12.0测试结果: ✅ 可用且支持向量字段
创建向量索引示例:
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" }
}
}
}
┌─────────────────────────────────────────────────────────┐
│ Controller层 │
│ 文本入库接口 / 向量搜索接口 / 批量操作接口 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Service层 │
│ 业务逻辑编排 / 批量处理策略 / 异常处理与重试 │
└─────────────────────────────────────────────────────────┘
↓
┌────────────────────────┬────────────────────────────────┐
│ EmbeddingClient层 │ ElasticsearchRepository层 │
│ 调用向量化接口 │ ES操作封装 │
│ 批量请求优化 │ 索引管理/文档CRUD/KNN搜索 │
└────────────────────────┴────────────────────────────────┘
文本入库流程:
原始文本
↓
文本预处理(长度限制、特殊字符处理)
↓
调用Embedding接口获取4096维向量
↓
构建ES文档(包含原文、向量、元数据)
↓
批量写入ES(Bulk API)
↓
返回入库结果(成功数、失败数、文档ID)
向量搜索流程:
用户输入查询文本
↓
调用Embedding接口向量化
↓
KNN搜索(cosine相似度)
↓
返回最相似的Top-K文档及相似度分数
步骤0:环境准备
↓
步骤1:创建ES索引(定义表结构)
↓
步骤2:准备文本数据
↓
步骤3:文本向量化(调用Embedding接口)
↓
步骤4:数据入库ES(插入文档)
↓
步骤5:向量搜索(KNN查询)
↓
步骤6:验证结果
| 服务 | 检查命令 | 预期结果 |
|---|---|---|
| Embedding接口 | curl http://10.192.72.11:18081/v1 |
返回API信息 |
| Elasticsearch | curl http://10.192.72.13:9200 |
返回ES版本信息 |
| 配置项 | 值 |
|---|---|
| Embedding接口地址 | http://10.192.72.11:18081/v1 |
| API Key | 1 |
| 模型名称 | Qwen3-Embedding-8B |
| 向量维度 | 4096 |
| ES地址 | http://10.192.72.13:9200 |
| 索引名称 | text_vectors |
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"
}
}
}
}'
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "text_vectors"
}
curl -X GET "http://10.192.72.13:9200/text_vectors/_mapping?pretty"
| 字段名 | 类型 | 说明 | 示例值 |
|---|---|---|---|
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" |
输入: 全量文本(未分段)
原始输入:一份完整的合同文档(5000字)
输出: 分段后的文本列表
分段输出:
- Chunk 0: 第1-500字(合同基本信息)
- Chunk 1: 第501-1000字(条款1)
- Chunk 2: 第1001-1500字(条款2)
- ...
- Chunk 9: 第4501-5000字(签署信息)
为什么要分段?
策略对比表:
| 分段策略 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| 固定长度分段 | 按字符数/token数切分 | 通用文档 | ✅ 简单 ❌ 可能切断语义 |
| 段落分段 | 按自然段落切分 | 结构化文档 | ✅ 保持语义完整 ❌ 段落长度不均 |
| 语义分段 | 基于语义相似度 | 重要文档 | ✅ 语义完整 ❌ 需要额外模型 |
| 滑动窗口 | 重叠切分 | 避免边界丢失 | ✅ 防止信息丢失 ❌ 数据冗余 |
| 混合策略 | 综合多种方法 | 生产环境推荐 | ✅ 平衡效果和性能 |
分段流程:
全量文本(5000字)
↓
1. 预处理(去除特殊字符、统一格式)
↓
2. 按段落切分(得到15个自然段)
↓
3. 每段检查长度
↓
4. 超长段落继续切分(最大500字)
↓
5. 添加重叠(前后50字重叠)
↓
6. 生成chunk列表(最终20个chunk)
↓
7. 每个chunk添加元数据(chunk序号、页码等)
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 最大chunk长度 | 500-800字 | 确保不超过模型token限制 |
| 最小chunk长度 | 100字 | 避免过短无意义的chunk |
| 重叠大小 | 50-100字 | 防止边界信息丢失 |
| 分段模式 | paragraph | 按段落优先,超长再切分 |
| 保留换行 | false | 转换为空格,便于向量化 |
输入文本(全量):
永续贷产品贷款合同
第一章:总则
本合同由甲方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 |
TextSplitter.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
@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;
}
【原始输入】
完整合同文档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
场景: 上传完整合同文档
{
"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"
}
}
经过分段处理后:
[
{
"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
]
[
{
"business_id": "doc-ai-001",
"content": "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。"
},
{
"business_id": "doc-ml-001",
"content": "机器学习是人工智能的核心技术之一,通过算法让计算机从数据中学习规律,做出预测或决策。"
},
{
"business_id": "doc-dl-001",
"content": "深度学习使用神经网络模拟人脑的学习过程,在图像识别、自然语言处理等领域取得突破。"
},
{
"business_id": "doc-nlp-001",
"content": "自然语言处理使计算机能够理解、解释和生成人类语言,应用于翻译、情感分析、智能客服等。"
}
]
| 预处理项 | 操作 | 示例 |
|---|---|---|
| 去除多余空格 | text.trim() |
" 文本 " → "文本" |
| 统一换行符 | 替换为空格 | "文本1\n文本2" → "文本1 文本2" |
| 长度限制 | 截断或分批 | 超过8192字符时处理 |
| HTML标签去除 | 正则替换 | "<p>文本</p>" → "文本" |
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": "人工智能是计算机科学的一个分支"
}'
{
"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
}
}
| 属性 | 值 | 说明 |
|---|---|---|
| 向量维度 | 4096 | 固定维度 |
| 数据类型 | 浮点数 | float类型 |
| 取值范围 | -1 ~ 1 | 通常在-1到1之间 |
| 向量数量 | 1个 | 单个文本返回1个向量 |
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维
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"
}'
{
"_index": "text_vectors",
"_id": "doc-ai-001",
"_version": 1,
"result": "created",
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
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"}
'
{
"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"}}
]
}
# 查看文档数量
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"
用户查询:"什么是AI技术"
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维)
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
}'
{
"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"]
}
}
]
}
}
| 排名 | 文档ID | 相似度分数 | 内容摘要 |
|---|---|---|---|
| 🥇 1 | doc-ai-001 | 0.9234 | 人工智能是计算机科学的一个分支... |
| 🥈 2 | doc-ml-001 | 0.8891 | 机器学习是人工智能的核心技术... |
| 🥉 3 | doc-dl-001 | 0.8756 | 深度学习使用神经网络模拟人脑... |
说明:
# 查看索引信息
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"
# 查看所有文档
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
}'
| 问题 | 检查方法 | 解决方案 |
|---|---|---|
| 索引不存在 | GET /text_vectors |
执行步骤1创建索引 |
| 向量维度错误 | 检查返回的向量长度 | 确保4096维 |
| 搜索无结果 | 检查文档是否入库 | 执行验证命令 |
| 相似度分数异常 | 检查向量是否归一化 | 确认ES配置 |
| 步骤 | 操作 | 命令/验证 | 状态 |
|---|---|---|---|
| ✅ 0 | 环境准备 | curl检查服务 | [ ] |
| ✅ 1 | 创建ES索引 | PUT /text_vectors | [ ] |
| ✅ 2 | 准备文本数据 | 准备JSON数组 | [ ] |
| ✅ 3 | 文本向量化 | POST /embeddings | [ ] |
| ✅ 4 | 数据入库ES | POST /text_vectors/_doc | [ ] |
| ✅ 5 | 向量搜索 | POST /text_vectors/_search | [ ] |
| ✅ 6 | 验证结果 | 检查返回数据 | [ ] |
假设我们有一个关于"人工智能"的文档库,需要实现:
| 项目 | 值 |
|---|---|
| 输入文本 | "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。" |
| 业务ID | doc-ai-001 |
| 项目 | 值 |
|---|---|
| 输入文本 | "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。" |
| 业务ID | doc-ai-001 |
| 项目 | 说明 |
|---|---|
| 调用接口 | POST http://10.192.72.11:18081/v1/embeddings |
| 输入 | 文本字符串 |
| 输出 | 4096维浮点数数组(简化展示) |
| 向量示例 | [0.0234, -0.0123, 0.0456, 0.0789, -0.0321, ..., 共4096个] |
本质理解:
把这段话翻译成4096个数字,作为这段话的"数学坐标"
| 字段名 | 值 | 说明 |
|---|---|---|
content |
"人工智能是计算机科学的一个分支..." | 原始文本 |
embedding |
[0.0234, -0.0123, ..., 共4096个] | 4096维向量 |
business_id |
doc-ai-001 | 业务唯一标识(作为ES文档ID) |
create_time |
2026-03-12T10:30:00 | 创建时间 |
| 文档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相关,向量接近 |
| 项目 | 值 |
|---|---|
| 查询文本 | "什么是AI技术" |
| Top-K | 3 |
| 项目 | 说明 |
|---|---|
| 查询文本 | "什么是AI技术" |
| 查询向量 | [0.0245, -0.0118, 0.0445, ..., 共4096个] |
| 对比文档 | 余弦相似度 | 相似程度 |
|---|---|---|
| doc-ai-001 (人工智能...) | 0.92 | ⭐⭐⭐⭐⭐ 最相似 |
| doc-ml-001 (机器学习...) | 0.89 | ⭐⭐⭐⭐ 很相似 |
| doc-life-001 (天气...) | 0.23 | ❌ 不相关 |
| doc-dl-001 (深度学习...) | 0.87 | ⭐⭐⭐⭐ 很相似 |
| 排名 | 文档ID | 内容摘要 | 相似度分数 |
|---|---|---|---|
| 🥇 1 | doc-ai-001 | 人工智能是计算机科学的一个分支... | 0.92 |
| 🥈 2 | doc-ml-001 | 机器学习是人工智能的核心技术... | 0.89 |
| 🥉 3 | doc-dl-001 | 深度学习使用神经网络模拟人脑... | 0.87 |
| 阶段 | 输入 | 输出 | 本质 |
|---|---|---|---|
| 向量化 | 文本:"人工智能技术..." | 4096个数字:[0.0234, -0.0123, ...] |
文本 → 数学坐标 |
| 入库 | 文本 + 向量 | ES文档 | 存储到数据库 |
| 搜索 | 查询:"什么是AI" | 相似文档列表 | 找向量最近的点 |
想象一个4096维的空间:
📍 "人工智能技术发展" → 坐标点A [0.02, 0.03, ...]
📍 "AI技术进展" → 坐标点B [0.021, 0.029, ...]
两点距离很近 ✅ 相似度高
📍 "人工智能技术发展" → 坐标点A [0.02, 0.03, ...]
📍 "今天天气不错" → 坐标点C [-0.1, 0.08, ...]
两点距离很远 ❌ 相似度低
对于企业级应用,特别是RAG(检索增强生成)场景,推荐采用融合设计(宽表设计),将元数据、业务数据、向量字段存储在同一个索引中。
传统方案(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 查询一次搞定 ✅
| 优势 | 说明 | 评分 |
|---|---|---|
| 避免JOIN | ES不支持JOIN,融合设计避免了应用层关联 | ⭐⭐⭐⭐⭐ |
| 查询性能 | 一次查询返回所有数据,无需多次查询 | ⭐⭐⭐⭐⭐ |
| 混合过滤 | 支持向量搜索 + 业务字段过滤 | ⭐⭐⭐⭐⭐ |
| 文件级操作 | 通过doc_id可以操作文件的所有chunk | ⭐⭐⭐⭐⭐ |
| 扩展性 | 易于添加新字段,无需修改表结构 | ⭐⭐⭐⭐ |
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": "更新时间"
}
}
}
}
| 字段分类 | 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 | ✅ 相同 |
{
"_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"
}
}
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
# 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"}
}
}
PDF文件上传
↓
文档解析(提取文本、识别页数)
↓
文本分段(按页/按段落,生成5个chunk)
↓
提取业务数据(合同类型、甲方、乙方等)
↓
批量向量化(调用Embedding接口,5个向量)
↓
批量入库ES(5个文档,每个包含完整的业务数据)
↓
验证入库(查询doc_id,应返回5个chunk)
用户查询:"A公司的消费贷合同利率是多少"
↓
1. 查询文本向量化
↓
2. ES混合查询
- KNN向量搜索(相似度)
- 过滤条件:contract_type="消费贷" AND party_a="A公司"
↓
3. 返回Top-K chunk(按相似度排序)
↓
4. 应用层处理
- 按doc_id分组(避免同一文件多个chunk)
- 提取最相关的chunk内容
- 返回给用户
| 方面 | 传统设计 | 融合设计 | 提升 |
|---|---|---|---|
| 查询性能 | 需要2次查询+JOIN | 1次查询搞定 | 50%+ |
| 应用层复杂度 | 需要组装数据 | 直接返回完整数据 | -80% |
| 混合查询 | 难以实现 | 原生支持 | ✅ |
| 文件级操作 | 需要多次操作 | 批量操作 | ✅ |
| 问题 | 影响 | 解决方案 |
|---|---|---|
| 数据冗余 | 存储空间增加约30% | 定期归档,只保留必要字段 |
| 更新成本 | 文件变更需更新所有chunk | 使用_update_by_query批量更新 |
| 单文档大小 | 单个文档较大 | 使用_source过滤返回字段 |
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| ✅ RAG检索 | 强烈推荐 | 需要向量搜索+元数据过滤 |
| ✅ 多段落文档 | 强烈推荐 | 需要chunk级别的检索 |
| ✅ 视频/音频 | 强烈推荐 | 需要时间戳+内容检索 |
| ✅ 企业知识库 | 强烈推荐 | 需要复杂的业务过滤 |
| ❌ 简单全文搜索 | 不推荐 | 冗余数据浪费空间 |
// 根据document_type字段区分不同类型的业务字段
{
"doc_id": "xxx",
"document_type": "contract", // 或 invoice、report等
// 通用字段
"content": "...",
"embedding": [...],
"file_path": "...",
// 合同专用字段
"contract_type": "...",
"party_a": "...",
// 发票专用字段(可能为空)
"invoice_number": null,
"invoice_amount": null
}
{
"doc_id": "xxx",
"version": 2,
"is_latest": true,
// 支持查询历史版本
"content": "更新后的内容...",
"embedding": [新向量]
}
| 实践项 | 建议 | 说明 |
|---|---|---|
| ID设计 | doc_id + chunk_id | 支持文件级操作 |
| 字段选择 | 只存储必要的业务字段 | 减少冗余 |
| 更新策略 | 批量更新所有chunk | 保证数据一致性 |
| 查询优化 | 使用filter替代must | 提高性能 |
| 分页处理 | 使用search_after | 大数据量友好 |
完整项目结构:
<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>
完整目录结构:
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
<?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>
模块交互图:
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
│ 监控告警服务 │
└─────────────────────────────────────────────────────────────────┘
| 功能模块 | 类名 | 职责 |
|---|---|---|
| 文本分段 | TextSplitter | 将全量文本分段为chunks |
| 向量化 | EmbeddingClient | 调用Embedding接口获取向量 |
| 入库服务 | TextEmbeddingService | 协调分段、向量化、入库流程 |
| ES操作 | ElasticsearchRepository | 封装ES操作(索引、查询、删除) |
| 向量搜索 | VectorSearchService | KNN向量搜索服务 |
| 文档管理 | DocumentService | 文档级别的CRUD操作 |
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示例:
# 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
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
<!-- 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>
TextEmbeddingController.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层搜索
// 返回相似文档列表
}
}
TextEmbeddingService.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);
}
}
EmbeddingClient.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) {
// 失败重试逻辑
}
}
ElasticsearchRepository.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相似文档
}
}
application.yml
# 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
{
"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"
}
{
"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"
}
}
}
}
批次大小控制:
分批示例:
// 假设有500条文本
List<List<IndexItem>> batches = Lists.partition(items, 128);
// 得到4个批次:128, 128, 128, 116
for (List<IndexItem> batch : batches) {
processBatch(batch); // 每批独立处理
}
Embedding接口调用:
ES写入异常:
使用businessId作为ES文档ID:
// 相同的businessId重复入库会覆盖,不会产生重复数据
String docId = businessId; // 业务ID作为ES文档ID
IndexRequest request = new IndexRequest(indexName)
.id(docId) // 设置文档ID
.source(buildSource(doc));
Embedding接口:
ES写入优化:
KNN搜索优化:
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();
}
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();
}
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;
}
必需配置:
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:索引副本数应用启动时检查:
Embedding接口:
ES操作:
关键日志点:
高可用性:
高性能:
可扩展性:
代码实现:
性能测试:
优化迭代:
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": "测试文本内容"
}'
curl -X GET "http://10.192.72.13:9200/_cluster/health?pretty"
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" }
}
}
}'
curl -X GET "http://10.192.72.13:9200/text_vectors/_mapping?pretty"
文档版本: v1.5 编写日期: 2026-03-12 更新日期: 2026-03-12 更新内容: