项目名称: schedule-embedding-api 文档版本: v2.2 最后更新: 2026-03-13 服务地址: http://localhost:8084
本系统提供基于 Elasticsearch 8.x 和 Embedding API 的智能文档向量化与检索服务,主要功能包括:
ES动态映射 + 业务字段完全动态
metadata 动态扩展,支持任意类型和字段| 序号 | 接口名称 | 请求方法 | 接口路径 | 功能说明 |
|---|---|---|---|---|
| 1 | 健康检查 | GET | /actuator/health | 检查服务健康状态 |
| 2 | 单个文档入库 | POST | /api/v1/documents/index | 将单个文档向量化后存入ES |
| 3 | 批量文档入库 | POST | /api/v1/documents/batch-index | 批量将文档向量化后存入ES |
| 4 | 向量搜索 | POST | /api/v1/search | 基于语义的向量搜索 |
| 5 | 混合搜索 | POST | /api/v1/search/hybrid | 向量搜索 + 业务字段过滤 |
| 6 | 查询文档 | GET | /api/v1/documents/{docId} | 查询文档的所有chunks |
| 7 | 删除文档 | DELETE | /api/v1/documents/{docId} | 删除文档的所有chunks |
| 8 | 删除Chunk | DELETE | /api/v1/documents/chunk/{chunkId} | 删除单个chunk |
/actuator/healthGETcurl -X GET http://localhost:8084/actuator/health
状态码: 200 OK
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": {
"total": 994662584320,
"free": 110714380288,
"threshold": 10485760,
"exists": true
}
},
"elasticsearch": {
"status": "UP",
"details": {
"cluster_name": "docker-cluster",
"status": "yellow",
"number_of_nodes": 1,
"number_of_data_nodes": 1
}
},
"ping": {
"status": "UP"
}
}
}
| 用例编号 | 测试场景 | 预期结果 |
|---|---|---|
| TC-001 | 正常访问健康检查接口 | 返回200,status为UP |
/api/v1/documents/indexPOSTapplication/json| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| docId | String | 是 | 文档唯一标识 | "contract-001" |
| fileName | String | 否 | 文件名 | "合同.pdf" |
| fullText | String | 是 | 文档全文内容 | "这是合同内容..." |
| filePath | String | 否 | 文件路径 | "/data/contracts/001.pdf" |
| fileSize | Long | 否 | 文件大小(字节) | 1024000 |
| fileType | String | 否 | 文件类型 | "pdf" |
| metadata | Map | 否 | 完全动态的业务元数据 | 见下方示例 |
重要: metadata 是完全动态的,支持任意字段!
合同类文档示例:
{
"contractType": "消费贷贷款合同",
"partyA": "A公司",
"partyB": "B公司",
"contractAmount": 1000000.00,
"signDate": "2026-01-15"
}
音频类文档示例:
{
"duration": 1800,
"speaker": "张三",
"language": "zh-CN",
"sampleRate": 44100,
"bitrate": 128000,
"format": "MP3",
"transcript": "音频转写文本..."
}
视频类文档示例:
{
"duration": 3600,
"resolution": "1920x1080",
"frameRate": 30,
"codec": "H.264",
"bitrate": 5000000,
"subtitles": ["中文字幕", "英文字幕"]
}
图片类文档示例:
{
"width": 1920,
"height": 1080,
"format": "JPEG",
"colorSpace": "RGB",
"dpi": 300,
"description": "图片描述..."
}
示例 1: 合同类文档入库
curl -X POST http://localhost:8084/api/v1/documents/index \
-H "Content-Type: application/json" \
-d '{
"docId": "contract-001",
"fileName": "永续贷产品贷款合同.pdf",
"fullText": "永续贷产品贷款合同\n\n第一章:总则\n本合同由甲方A公司与乙方B公司签订,合同金额为100万元。\n\n第二章:贷款条款\n2.1 贷款利率按照央行基准利率执行。\n2.2 贷款期限为36个月。\n2.3 还款方式为等额本息。",
"filePath": "/home/data/contracts/contract-001.pdf",
"fileSize": 1289748,
"fileType": "pdf",
"metadata": {
"contractType": "消费贷贷款合同",
"partyA": "A公司",
"partyB": "B公司",
"contractAmount": 1000000.00,
"signDate": "2026-01-15"
}
}'
示例 2: 音频类文档入库
curl -X POST http://localhost:8084/api/v1/documents/index \
-H "Content-Type: application/json" \
-d '{
"docId": "audio-001",
"fileName": "会议录音.mp3",
"fullText": "这是会议的完整转写文本内容,包含所有发言人的讲话记录...",
"filePath": "/home/data/audio/meeting_001.mp3",
"fileSize": 5120000,
"fileType": "mp3",
"metadata": {
"duration": 1800,
"speaker": "张三",
"language": "zh-CN",
"sampleRate": 44100,
"format": "MP3"
}
}'
示例 3: 视频类文档入库
curl -X POST http://localhost:8084/api/v1/documents/index \
-H "Content-Type: application/json" \
-d '{
"docId": "video-001",
"fileName": "培训课程.mp4",
"fullText": "这是视频的字幕文本内容...",
"filePath": "/home/data/video/training_001.mp4",
"fileSize": 51200000,
"fileType": "mp4",
"metadata": {
"duration": 3600,
"resolution": "1920x1080",
"frameRate": 30,
"codec": "H.264",
"subtitles": ["中文字幕", "英文字幕"]
}
}'
成功响应 - 200 OK
{
"success": true,
"docId": "contract-001",
"chunkCount": 1,
"message": "文档入库成功",
"error": null
}
失败响应 - 400 Bad Request
{
"code": 400,
"success": false,
"message": "参数校验失败",
"errors": {
"docId": "文档ID不能为空"
},
"timestamp": 1773306699906
}
{
"code": 400,
"success": false,
"message": "参数校验失败",
"errors": {
"fullText": "全文内容不能为空"
},
"timestamp": 1773306699937
}
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-002 | 正常数据入库(合同) | 完整的合同信息 | 200,chunkCount=1 |
| TC-003 | 缺少docId | 不传docId字段 | 400,提示"文档ID不能为空" |
| TC-004 | 空文本内容 | fullText为空字符串 | 400,提示"全文内容不能为空" |
| TC-005 | 音频数据入库 | 音频元数据(duration、speaker等) | 200,正常入库 |
| TC-006 | 视频数据入库 | 视频元数据(resolution、codec等) | 200,正常入库 |
| TC-007 | 动态字段扩展 | 新增任意自定义字段 | 200,零代码扩展成功 |
/api/v1/documents/batch-indexPOSTapplication/json| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| items | Array<IndexRequest> | 是 | 文档列表(最多100个) | 见下方示例 |
IndexRequest 结构同"单个文档入库"接口。
curl -X POST http://localhost:8084/api/v1/documents/batch-index \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"docId": "batch-doc-001",
"fileName": "合同1.pdf",
"fullText": "这是第一个合同文档的内容。包含重要条款和条件。",
"fileType": "pdf",
"metadata": {
"contractType": "消费贷",
"partyA": "A公司"
}
},
{
"docId": "batch-doc-002",
"fileName": "音频1.mp3",
"fullText": "这是音频的转写文本内容。",
"fileType": "mp3",
"metadata": {
"duration": 1800,
"speaker": "张三"
}
},
{
"docId": "batch-doc-003",
"fileName": "视频1.mp4",
"fullText": "这是视频的字幕内容。",
"fileType": "mp4",
"metadata": {
"duration": 3600,
"resolution": "1920x1080"
}
}
]
}'
成功响应 - 200 OK
{
"totalCount": 3,
"successCount": 3,
"failedCount": 0,
"failedItems": []
}
部分失败响应 - 200 OK
{
"totalCount": 3,
"successCount": 2,
"failedCount": 1,
"failedItems": [
{
"docId": "batch-doc-003",
"error": "全文内容不能为空"
}
]
}
失败响应 - 400 Bad Request
{
"code": 400,
"success": false,
"message": "参数校验失败",
"errors": {
"items": "文档列表不能为空"
},
"timestamp": 1773306700683
}
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-008 | 正常批量入库 | 3个不同类型文档 | 200,successCount=3 |
| TC-009 | 空列表 | items=[] | 400,提示"文档列表不能为空" |
| TC-010 | 大批量入库 | 10个文档 | 200,successCount=10 |
| TC-011 | 部分失败 | 3个文档,1个缺少fullText | 200,successCount=2,failedCount=1 |
/api/v1/searchPOSTapplication/json基于语义的向量搜索,使用 Embedding 模型将查询文本转换为 4096 维向量,然后在 ES 中进行 KNN 搜索,返回最相似的结果。
| 参数名 | 类型 | 必填 | 说明 | 示例值 | 默认值 |
|---|---|---|---|---|---|
| query | String | 是 | 查询文本 | "A公司的消费贷合同利率是多少" | - |
| topK | Integer | 否 | 返回结果数量 | 5 | 10 |
curl -X POST http://localhost:8084/api/v1/search \
-H "Content-Type: application/json" \
-d '{
"query": "A公司的消费贷合同利率是多少",
"topK": 5
}'
成功响应 - 200 OK
[
{
"docId": "company-a-test",
"chunkId": "company-a-test_chunk_0",
"chunkIndex": 0,
"content": "A公司与B公司签订消费贷合同,合同金额100万元,贷款利率按照央行基准利率执行,贷款期限36个月",
"score": 0.8871919,
"metadata": {
"chunk_index": 0,
"file_path": "/tmp/test.pdf",
"create_time": "2026-03-12T17:06:51",
"doc_id": "company-a-test",
"chunk_id": "company-a-test_chunk_0",
"content": "A公司与B公司签订消费贷合同,合同金额100万元,贷款利率按照央行基准利率执行,贷款期限36个月",
"file_size": 1024,
"file_size_mb": 0.0009765625,
"chunk_count": 0,
"update_time": "2026-03-12T17:06:51",
"file_type": "pdf",
"contract_type": "消费贷贷款合同",
"party_a": "A公司",
"party_b": "B公司"
}
}
]
注意: 响应中不包含 embedding 字段,以节省带宽和提高性能。
无结果响应 - 200 OK
[]
失败响应 - 400 Bad Request
{
"code": 400,
"success": false,
"message": "参数校验失败",
"errors": {
"query": "查询文本不能为空"
},
"timestamp": 1773306701000
}
| 字段名 | 类型 | 说明 |
|---|---|---|
| docId | String | 文档ID |
| chunkId | String | 分片ID |
| chunkIndex | Integer | 分片序号 |
| content | String | 匹配的内容片段 |
| score | Float | 相似度分数(0-1,越高越相似) |
| metadata | Map | 完整的元数据信息(不包含embedding) |
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-012 | 语义搜索 | query="合同利率", topK=5 | 200,返回相关合同片段 |
| TC-013 | 空查询 | 不传query | 400,提示"查询文本不能为空" |
| TC-014 | 指定topK | query="贷款", topK=3 | 200,返回最多3条结果 |
| TC-015 | 无匹配结果 | query="不存在的特定内容" | 200,返回空数组[] |
| TC-016 | 中文模糊搜索 | query="多少钱" | 200,返回包含金额信息的片段 |
/api/v1/search/hybridPOSTapplication/json结合向量搜索和业务字段过滤,先进行 KNN 搜索,再根据指定的业务字段过滤结果。
| 参数名 | 类型 | 必填 | 说明 | 示例值 | 默认值 |
|---|---|---|---|---|---|
| query | String | 是 | 查询文本 | "贷款利率和还款方式" | - |
| topK | Integer | 否 | 返回结果数量 | 10 | 10 |
| filters | Map | 否 | 动态过滤条件,支持任意字段 | 见下方示例 | - |
重要: filters 是完全动态的,可以根据入库时的 metadata 任意过滤!
合同过滤示例:
{
"contractType": "消费贷贷款合同",
"partyA": "A公司"
}
音频过滤示例:
{
"speaker": "张三",
"language": "zh-CN"
}
视频过滤示例:
{
"resolution": "1920x1080",
"codec": "H.264"
}
示例 1: 合同过滤
curl -X POST http://localhost:8084/api/v1/search/hybrid \
-H "Content-Type: application/json" \
-d '{
"query": "贷款利率和还款方式",
"topK": 10,
"filters": {
"contractType": "消费贷贷款合同",
"partyA": "A公司"
}
}'
示例 2: 音频过滤
curl -X POST http://localhost:8084/api/v1/search/hybrid \
-H "Content-Type: application/json" \
-d '{
"query": "会议讨论的重点内容",
"topK": 5,
"filters": {
"speaker": "张三",
"language": "zh-CN"
}
}'
示例 3: 视频过滤
curl -X POST http://localhost:8084/api/v1/search/hybrid \
-H "Content-Type: application/json" \
-d '{
"query": "培训课程的核心要点",
"topK": 10,
"filters": {
"resolution": "1920x1080",
"codec": "H.264"
}
}'
成功响应 - 200 OK
[
{
"docId": "test-doc-001",
"chunkId": "test-doc-001_chunk_0",
"chunkIndex": 0,
"content": "永续贷产品贷款合同\n\n第二章:贷款条款\n2.1 贷款利率按照央行基准利率执行。\n2.2 贷款期限为36个月。\n2.3 还款方式为等额本息。",
"score": 0.8512345,
"metadata": {
"contract_type": "消费贷贷款合同",
"party_a": "A公司",
"party_b": "B公司",
"contract_amount": 1000000.0
}
}
]
无匹配结果 - 200 OK
[]
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-017 | 合同字段过滤 | query="贷款利率", filters={contractType:"消费贷",partyA:"A公司"} | 200,返回A公司的消费贷合同 |
| TC-018 | 音频字段过滤 | query="会议记录", filters={speaker:"张三"} | 200,返回张三发言的音频 |
| TC-019 | 视频字段过滤 | query="课程", filters={resolution:"1920x1080"} | 200,返回高清视频 |
| TC-020 | 无匹配过滤 | query="利率", filters={contractType:"不存在的类型"} | 200,返回空数组[] |
| TC-021 | 无过滤条件 | query="合同", 不传filters | 200,等同于普通向量搜索 |
/api/v1/documents/{docId}GET| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| docId | String | 是 | 文档ID(路径参数) | "contract-001" |
curl -X GET http://localhost:8084/api/v1/documents/contract-001
成功响应 - 200 OK
[
{
"docId": "contract-001",
"chunkId": "contract-001_chunk_0",
"chunkIndex": 0,
"content": "永续贷产品贷款合同\n\n第一章:总则\n本合同由甲方A公司与乙方B公司签订。",
"filePath": "/data/contracts/contract-001.pdf",
"fileType": "pdf",
"createTime": "2026-03-12 17:10:15",
"updateTime": "2026-03-12 17:10:15",
"extendedFields": {
"contractType": "消费贷贷款合同",
"partyA": "A公司",
"partyB": "B公司"
}
}
]
文档不存在 - 200 OK
[]
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-022 | 查询存在的文档 | docId="contract-001" | 200,返回文档的所有chunks |
| TC-023 | 查询不存在的文档 | docId="non-existent" | 200,返回空数组[] |
/api/v1/documents/{docId}DELETE| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| docId | String | 是 | 文档ID(路径参数) | "contract-001" |
curl -X DELETE http://localhost:8084/api/v1/documents/contract-001
成功响应 - 200 OK
{
"success": true,
"docId": "contract-001",
"deletedCount": 1,
"message": "删除成功"
}
文档不存在 - 200 OK
{
"success": true,
"docId": "non-existent-doc",
"deletedCount": 0,
"message": "删除成功"
}
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-024 | 删除存在的文档 | docId="contract-001" | 200,deletedCount>0 |
| TC-025 | 删除不存在的文档 | docId="non-existent" | 200,deletedCount=0 |
/api/v1/documents/chunk/{chunkId}DELETE| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
| chunkId | String | 是 | 分片ID(路径参数) | "contract-001_chunk_0" |
curl -X DELETE http://localhost:8084/api/v1/documents/chunk/contract-001_chunk_0
成功响应 - 200 OK
{
"success": true,
"chunkId": "contract-001_chunk_0",
"message": "删除成功"
}
chunk不存在 - 200 OK
{
"success": false,
"chunkId": "non-existent-chunk",
"message": "删除失败"
}
| 用例编号 | 测试场景 | 请求参数 | 预期结果 |
|---|---|---|---|
| TC-026 | 删除存在的chunk | chunkId="contract-001_chunk_0" | 200,success=true |
| TC-027 | 删除不存在的chunk | chunkId="non-existent-chunk" | 200,success=false |
| 状态码 | 说明 |
|---|---|
| 200 | 请求成功 |
| 400 | 请求参数错误 |
| 500 | 服务器内部错误 |
| 错误码 | 说明 | 示例 |
|---|---|---|
| 400 | 参数校验失败 | 缺少必填字段、字段格式错误 |
{
"code": 400,
"success": false,
"message": "参数校验失败",
"errors": {
"fieldName": "具体错误信息"
},
"timestamp": 1773306699906
}
本系统使用 ES动态映射(DynamicMapping.True),索引结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| doc_id | keyword | 文档唯一标识 |
| chunk_id | keyword | 分片唯一标识 |
| chunk_index | long | 分片序号 |
| content | text | 文档内容(支持全文搜索) |
| embedding | dense_vector | 4096维向量(用于KNN搜索) |
| create_time | date | 创建时间 |
| update_time | date | 更新时间 |
除上述固定字段外,所有其他字段都是动态的,包括:
示例:传入以下 metadata:
{
"contractType": "贷款合同",
"partyA": "A公司",
"contractAmount": 1000000,
"signDate": "2026-01-15"
}
ES会自动创建并索引这些字段,无需修改代码。
{
"docId": "string (必填)",
"fileName": "string (可选)",
"fullText": "string (必填)",
"filePath": "string (可选)",
"fileSize": "long (可选)",
"fileType": "string (可选)",
"metadata": {
"任意业务字段": "任意值(完全动态)"
}
}
{
"query": "string (必填)",
"topK": "integer (可选, 默认10)",
"filters": {
"任意字段": "任意值(完全动态)"
}
}
{
"docId": "string",
"chunkId": "string",
"chunkIndex": "integer",
"content": "string",
"score": "float",
"metadata": {
"所有文档元数据字段(不包含embedding)"
}
}
注意: metadata 中不包含 embedding 字段,以节省带宽。
# 运行完整测试套件
bash test_api.sh
# 查看测试结果
cat api_test_results.txt
| 模块 | 用例数 | 涵盖场景 |
|---|---|---|
| 健康检查 | 1 | 服务状态检查 |
| 文档入库 | 6 | 正常入库、参数校验、多数据类型、动态字段 |
| 批量入库 | 4 | 正常批量、空列表、大批量、部分失败 |
| 向量搜索 | 5 | 语义搜索、空查询、指定数量、无结果 |
| 混合搜索 | 5 | 合同过滤、音频过滤、视频过滤、多条件 |
| 文档管理 | 5 | 查询、删除文档和chunk |
| 合计 | 26 | 覆盖所有核心场景 |
# 启动应用
java -jar schedule-embedding-api.jar
# 或使用 Maven
mvn spring-boot:run
curl http://localhost:8084/actuator/health
curl -X POST http://localhost:8084/api/v1/documents/index \
-H "Content-Type: application/json" \
-d '{
"docId": "test-001",
"fullText": "测试文档内容"
}'
curl -X POST http://localhost:8084/api/v1/search \
-H "Content-Type: application/json" \
-d '{
"query": "测试",
"topK": 5
}'
A: 可能的原因:
A: 建议:
A:
A:
A:
| 指标 | 数值 | 说明 |
|---|---|---|
| 单文档入库 | < 2s | 包含向量化时间 |
| 批量入库(10个) | < 5s | 并发处理 |
| 向量搜索 | < 1s | topK=10 |
| 混合搜索 | < 1.5s | 包含过滤 |
| 向量维度 | 4096 | Qwen3-Embedding-8B |
| 响应大小优化 | ~16KB/结果 | 移除embedding字段 |
本系统使用 Elasticsearch 的 动态映射 功能,实现真正的零代码扩展:
// 创建索引时启用动态映射
.mappings(m -> m
.dynamic(DynamicMapping.True) // 关键配置!
.properties("doc_id", p -> p.keyword(k -> k))
.properties("chunk_id", p -> p.keyword(k -> k))
// ... 核心字段
// 其他字段自动识别!
)
效果:
旧设计:
// 每个字段都要判断
if (metadata.containsKey("contractType")) {
doc.setContractType((String) metadata.get("contractType"));
}
if (metadata.containsKey("duration")) {
doc.setDuration((Integer) metadata.get("duration"));
}
// ... 无穷无尽的 if
新设计:
// 所有字段自动处理,只有 1 行代码!
doc.addExtendedFields(request.getMetadata());
| 操作 | 旧设计 | 新设计 |
|---|---|---|
| 新增合同字段 | 修改 ES Mapping + 代码 | 直接在 metadata 加字段 |
| 新增音频字段 | 修改 ES Mapping + 代码 | 直接在 metadata 加字段 |
| 新增视频字段 | 修改 ES Mapping + 代码 | 直接在 metadata 加字段 |
| 新增任意字段 | 修改 ES Mapping + 代码 | 直接在 metadata 加字段 |
基于解析结果示例材料的典型入库请求:
curl -X POST http://localhost:8084/api/v1/documents/index \
-H "Content-Type: application/json" \
-d '{
"docId": "bank-flow-001",
"fileName": "111111133.png",
"fullText": "账户明细...交易记录...",
"filePath": "/data/files/20260313_8f9e7d6c5b4a.png",
"fileSize": 102400,
"fileType": "png",
"metadata": {
"file_unique_id": "20260313_8f9e7d6c5b4a",
"business_topic": "金融-信贷审批",
"document_type": "银行流水",
"belong_department": "风控部",
"tags": ["信贷", "流水", "合规", "农业"]
}
}'
curl -X POST http://localhost:8084/api/v1/documents/index \
-H "Content-Type: application/json" \
-d '{
"docId": "manual-001",
"fileName": "宇信科技操作手册.docx",
"fullText": "宇信科技管理数智化星云平台 快速入门操作手册...",
"filePath": "/data/files/manual.docx",
"fileSize": 2048000,
"fileType": "docx",
"metadata": {
"business_topic": "金融-系统文件",
"document_type": "操作手册",
"belong_department": "产品部",
"tags": ["金融", "企管", "星云", "操作手册"]
}
}'
| 字段名 | 类型 | 说明 | 示例值 |
|---|---|---|---|
| file_unique_id | String | 文件唯一标识 | "20260313_8f9e7d6c5b4a" |
| business_topic | String | 业务主题 | "金融-信贷审批" |
| document_type | String | 文档类型 | "银行流水"、"操作手册"、"贷款合同" |
| belong_department | String | 所属部门 | "风控部"、"信贷部"、"审计部" |
| tags | Array | 标签数组 | ["信贷", "流水", "合规"] |
向量搜索:
curl -X POST http://localhost:8084/api/v1/search \
-H "Content-Type: application/json" \
-d '{
"query": "银行流水交易记录",
"topK": 5
}'
混合搜索(按业务维度过滤):
curl -X POST http://localhost:8084/api/v1/search/hybrid \
-H "Content-Type: application/json" \
-d '{
"query": "风险评估",
"topK": 5,
"filters": {
"business_topic": "金融-风险管理",
"belong_department": "风控部"
}
}'
文档结束