向量化入库方案.md 69 KB

文本向量化与Elasticsearch入库方案

一、环境测试结果

1.1 Embedding接口测试

配置信息:

  • 接口地址:http://10.192.72.11:18081/v1
  • API Key:1
  • 模型:Qwen3-Embedding-8B
  • 向量维度:4096
  • 批处理大小:128

测试结果: ✅ 可用

  • HTTP状态码:200
  • 响应时间:< 1秒
  • 向量维度验证:正确返回4096维浮点数数组
  • 支持批量请求

请求示例:

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向量搜索支持
  • 向量维度严格验证

创建向量索引示例:

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 创建索引命令

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 预期返回

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "text_vectors"
}

3.3.3 验证索引创建成功

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

@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;
}

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 准备示例文本(全量)

场景: 上传完整合同文档

{
  "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
]

3.4.9 文本预处理(可选)

[
  {
    "business_id": "doc-ai-001",
    "content": "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务。包括语言理解、视觉感知、决策制定等。"
  },
  {
    "business_id": "doc-ml-001",
    "content": "机器学习是人工智能的核心技术之一,通过算法让计算机从数据中学习规律,做出预测或决策。"
  },
  {
    "business_id": "doc-dl-001",
    "content": "深度学习使用神经网络模拟人脑的学习过程,在图像识别、自然语言处理等领域取得突破。"
  },
  {
    "business_id": "doc-nlp-001",
    "content": "自然语言处理使计算机能够理解、解释和生成人类语言,应用于翻译、情感分析、智能客服等。"
  }
]

3.4.2 文本预处理(可选)

预处理项 操作 示例
去除多余空格 text.trim() " 文本 ""文本"
统一换行符 替换为空格 "文本1\n文本2""文本1 文本2"
长度限制 截断或分批 超过8192字符时处理
HTML标签去除 正则替换 "<p>文本</p>""文本"

3.5 步骤3:文本向量化

3.5.1 调用Embedding接口(单个文本)

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 返回的向量数据

{
  "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 批量向量化(可选,提高效率)

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 插入单个文档

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 预期返回

{
  "_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)

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 批量插入返回

{
  "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 验证数据入库成功

# 查看文档数量
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 查询文本向量化

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搜索

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 搜索结果

{
  "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 验证索引状态

# 查看索引信息
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 查看已入库文档

# 查看所有文档
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 索引结构

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示例

{
  "_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 混合查询(向量搜索 + 业务过滤)

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 文件级别操作

# 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 支持多种文档类型

// 根据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 版本管理

{
  "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多模块结构

完整项目结构:

<project>
    <modules>
        <!-- 现有模块 -->
        <module>schedule-producer</module>
        <module>schedule-consumer</module>
        <module>schedule-manager</module>
        <module>schedule-monitor</module>

        <!-- 新增模块:文本向量化与检索API -->
        <module>schedule-embedding-api</module>
    </modules>
</project>

6.1.2 schedule-embedding-api 模块结构

完整目录结构:

schedule-embedding-api/
├── pom.xml                                    Maven配置
│
├── src/main/java/com/schedule/embedding/
│   ├── ScheduleEmbeddingApplication.java    启动类
│   │
│   ├── controller/                           控制器层(REST API)
│   │   ├── TextEmbeddingController.java      文本入库API
│   │   ├── VectorSearchController.java       向量搜索API
│   │   └── DocumentController.java           文档管理API
│   │
│   ├── service/                              服务层(业务逻辑)
│   │   ├── TextEmbeddingService.java        文本向量化服务
│   │   ├── VectorSearchService.java         向量搜索服务
│   │   ├── TextSplitter.java                文本分段器
│   │   └── DocumentService.java             文档管理服务
│   │
│   ├── client/                               外部接口客户端
│   │   └── EmbeddingClient.java             Embedding API客户端
│   │
│   ├── repository/                           ES操作层
│   │   └── ElasticsearchRepository.java     ES封装
│   │
│   ├── model/                                数据模型
│   │   ├── dto/
│   │   │   ├── IndexRequest.java            入库请求DTO
│   │   │   ├── BatchIndexRequest.java       批量入库请求DTO
│   │   │   ├── SearchRequest.java           搜索请求DTO
│   │   │   └── SearchResult.java            搜索结果DTO
│   │   ├── entity/
│   │   │   ├── TextDocument.java            文档实体
│   │   │   └── Chunk.java                   分段实体
│   │   └── vo/
│   │       ├── IndexResult.java             入库结果VO
│   │       └── BatchIndexResult.java        批量入库结果VO
│   │
│   ├── config/                               配置类
│   │   ├── ElasticsearchConfig.java         ES配置
│   │   ├── EmbeddingConfig.java             Embedding配置
│   │   └── ChunkConfig.java                 分段配置
│   │
│   └── exception/                            异常处理
│       ├── EmbeddingException.java          向量化异常
│       └── ElasticsearchException.java      ES操作异常
│
├── src/main/resources/
│   ├── application.yml                       配置文件
│   └── logback-spring.xml                    日志配置
│
└── src/test/java/                            测试代码
    └── com/schedule/embedding/
        └── service/
            └── TextEmbeddingServiceTest.java

6.1.3 pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.schedule</groupId>
        <artifactId>schedule-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>schedule-embedding-api</artifactId>
    <name>schedule-embedding-api</name>
    <description>文本向量化与向量检索服务</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Elasticsearch Rest High Level Client -->
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.17.15</version>
        </dependency>

        <!-- HTTP客户端(调用Embedding接口) -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.3</version>
        </dependency>

        <!-- JSON处理 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <!-- Lombok(减少样板代码) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Apache Commons(工具类) -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

6.1.4 与其他模块的交互关系

模块交互图:

┌─────────────────────────────────────────────────────────────────┐
│                        schedule-producer                          │
│                      文档上传任务生产者                              │
└──────────────────────────────┬──────────────────────────────────┘
                               │ 发送文档入库消息
                               ↓
┌─────────────────────────────────────────────────────────────────┐
│                       schedule-consumer                           │
│                       文档入库任务消费者                              │
└──────────────────────────────┬──────────────────────────────────┘
                               │ 调用入库API
                               ↓
┌─────────────────────────────────────────────────────────────────┐
│                   schedule-embedding-api                          │
│                  文本向量化与向量检索API服务                          │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ 1. 文本分段(TextSplitter)                                 │ │
│  │ 2. 向量化(EmbeddingClient → Embedding接口)               │ │
│  │ 3. 入库ES(ElasticsearchRepository)                         │ │
│  │ 4. 向量搜索(KNN查询)                                       │ │
│  └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
                               │
                ┌──────────────┴──────────────┐
                ↓                             ↓
┌───────────────────────┐  ┌─────────────────────────┐
│   Embedding接口        │  │    Elasticsearch         │
│   http://10.192.72.11  │  │    http://10.192.72.13   │
│      :18081/v1         │  │         :9200            │
└───────────────────────┘  └─────────────────────────┘
                ↑                             ↑
                │ 监控                        │ 监控
                └──────────────┬──────────────┘
                               ↓
┌─────────────────────────────────────────────────────────────────┐
│                      schedule-monitor                             │
│                      监控告警服务                                   │
└─────────────────────────────────────────────────────────────────┘

6.1.5 核心功能模块

功能模块 类名 职责
文本分段 TextSplitter 将全量文本分段为chunks
向量化 EmbeddingClient 调用Embedding接口获取向量
入库服务 TextEmbeddingService 协调分段、向量化、入库流程
ES操作 ElasticsearchRepository 封装ES操作(索引、查询、删除)
向量搜索 VectorSearchService KNN向量搜索服务
文档管理 DocumentService 文档级别的CRUD操作

6.1.6 API接口设计

RESTful API列表:

功能 HTTP方法 路径 说明
单个文档入库 POST /api/v1/documents/index 上传完整文档,自动分段入库
批量文档入库 POST /api/v1/documents/batch-index 批量上传文档
向量搜索 POST /api/v1/search 向量相似度搜索
混合搜索 POST /api/v1/search/hybrid 向量+业务条件过滤
查询文档 GET /api/v1/documents/{docId} 查询文档的所有chunks
删除文档 DELETE /api/v1/documents/{docId} 删除文档的所有chunks
更新文档 PUT /api/v1/documents/{docId} 更新文档的所有chunks
健康检查 GET /actuator/health 服务健康状态

API示例:

# 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 配置

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 核心依赖

<!-- Elasticsearch Rest High Level Client (JDK1.8兼容) -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.17.15</version>
</dependency>

<!-- HTTP客户端(调用Embedding接口) -->
<dependency>
    <groupId>okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.3</version>
</dependency>

<!-- JSON处理 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

6.2 代码结构设计

6.2.1 Controller层

TextEmbeddingController.java

@RestController
@RequestMapping("/api/text-embedding")
public class TextEmbeddingController {

    @Autowired
    private TextEmbeddingService embeddingService;

    /**
     * 单个文本入库
     * POST /api/text-embedding/index
     * Body: { "content": "文本内容", "businessId": "业务ID" }
     */
    @PostMapping("/index")
    public ResponseEntity<IndexResult> indexText(@RequestBody IndexRequest request) {
        // 参数校验
        // 调用Service层
        // 返回结果
    }

    /**
     * 批量文本入库
     * POST /api/text-embedding/batch-index
     * Body: { "items": [{"content": "...", "businessId": "..."}] }
     */
    @PostMapping("/batch-index")
    public ResponseEntity<BatchIndexResult> batchIndex(@RequestBody BatchIndexRequest request) {
        // 批量处理(最多128条)
        // 返回批量结果
    }

    /**
     * 向量相似度搜索
     * POST /api/text-embedding/search
     * Body: { "query": "查询文本", "topK": 10 }
     */
    @PostMapping("/search")
    public ResponseEntity<List<SearchResult>> search(@RequestBody SearchRequest request) {
        // 调用Service层搜索
        // 返回相似文档列表
    }
}

6.2.2 Service层

TextEmbeddingService.java

@Service
public class TextEmbeddingService {

    @Value("${embedding.api.url}")
    private String embeddingApiUrl;

    @Value("${embedding.api.key}")
    private String embeddingApiKey;

    @Value("${embedding.batch.size:128}")
    private int batchSize;

    @Autowired
    private EmbeddingClient embeddingClient;

    @Autowired
    private ElasticsearchRepository esRepository;

    /**
     * 单个文本入库流程
     */
    public IndexResult indexText(String content, String businessId) {
        // 1. 文本预处理
        String processedText = preprocessText(content);

        // 2. 调用Embedding接口
        float[] vector = embeddingClient.getEmbedding(processedText);

        // 3. 构建ES文档
        TextDocument doc = buildDocument(processedText, vector, businessId);

        // 4. 写入ES
        String docId = esRepository.indexDocument(doc);

        return IndexResult.success(docId);
    }

    /**
     * 批量入库(支持分批处理)
     */
    public BatchIndexResult batchIndex(List<IndexItem> items) {
        // 按batchSize分批
        // 每批调用embedding接口批量向量化
        // 批量写入ES(Bulk API)
        // 返回汇总结果
    }

    /**
     * 向量搜索
     */
    public List<SearchResult> search(String query, int topK) {
        // 1. 查询文本向量化
        float[] queryVector = embeddingClient.getEmbedding(query);

        // 2. KNN搜索
        return esRepository.knnSearch(queryVector, topK);
    }
}

6.2.3 EmbeddingClient层

EmbeddingClient.java

@Component
public class EmbeddingClient {

    @Value("${embedding.api.url}")
    private String apiUrl;

    @Value("${embedding.api.key}")
    private String apiKey;

    private OkHttpClient httpClient;
    private ObjectMapper objectMapper;

    /**
     * 单个文本向量化
     */
    public float[] getEmbedding(String text) {
        // 构建请求
        // HTTP POST调用
        // 解析返回的4096维向量
        // 返回float[]
    }

    /**
     * 批量文本向量化
     */
    public List<float[]> getBatchEmbeddings(List<String> texts) {
        // 批量请求(最多128条)
        // 解析批量响应
        // 返回向量列表
    }

    /**
     * 重试机制(指数退避)
     */
    private float[] callWithRetry(String text, int maxRetries) {
        // 失败重试逻辑
    }
}

6.2.4 ElasticsearchRepository层

ElasticsearchRepository.java

@Component
public class ElasticsearchRepository {

    @Value("${elasticsearch.host}")
    private String esHost;

    private RestHighLevelClient client;

    /**
     * 创建向量索引
     */
    public boolean createIndex(String indexName) {
        // 使用dense_vector字段类型
        // 设置cosine相似度
    }

    /**
     * 索引单个文档
     */
    public String indexDocument(TextDocument doc) {
        // 使用businessId作为文档ID(幂等性)
        // IndexRequest
    }

    /**
     * 批量索引文档
     */
    public BulkResponse bulkIndex(List<TextDocument> docs) {
        // BulkRequest
        // 批量写入优化
    }

    /**
     * KNN向量搜索
     */
    public List<SearchResult> knnSearch(float[] queryVector, int topK) {
        // 使用KNN查询
        // 返回Top-K相似文档
    }
}

6.3 配置文件

application.yml

# 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文档结构

{
  "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 索引映射设计

{
  "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条自动分批处理
  • 每批独立处理,失败不影响其他批次

分批示例:

// 假设有500条文本
List<List<IndexItem>> batches = Lists.partition(items, 128);
// 得到4个批次:128, 128, 128, 116
for (List<IndexItem> batch : batches) {
    processBatch(batch);  // 每批独立处理
}

6.2 异常处理与重试

Embedding接口调用:

  • 超时时间:30秒
  • 最大重试次数:3次
  • 重试策略:指数退避(1s, 2s, 4s)
  • 失败记录:记录失败文本,支持手动重试

ES写入异常:

  • Bulk部分失败:记录失败文档ID
  • 网络异常:自动重试
  • 索引不存在:自动创建索引

6.3 幂等性设计

使用businessId作为ES文档ID:

// 相同的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 文本入库详细流程

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 批量入库详细流程

public BatchIndexResult batchIndex(List<IndexItem> items) {
    // 步骤1:参数校验
    if (items == null || items.isEmpty()) {
        throw new IllegalArgumentException("批次不能为空");
    }

    // 步骤2:分批处理
    List<List<IndexItem>> batches = Lists.partition(items, batchSize);

    int totalSuccess = 0;
    int totalFailed = 0;
    List<String> failedItems = new ArrayList<>();

    // 步骤3:逐批处理
    for (List<IndexItem> batch : batches) {
        try {
            // 3.1 批量向量化
            List<String> texts = batch.stream()
                .map(IndexItem::getContent)
                .collect(Collectors.toList());
            List<float[]> vectors = embeddingClient.getBatchEmbeddings(texts);

            // 3.2 构建文档列表
            List<TextDocument> docs = new ArrayList<>();
            for (int i = 0; i < batch.size(); i++) {
                TextDocument doc = buildDocument(
                    batch.get(i),
                    vectors.get(i)
                );
                docs.add(doc);
            }

            // 3.3 批量写入ES
            BulkResponse response = esRepository.bulkIndex(docs);

            // 3.4 统计结果
            if (response.hasFailures()) {
                // 处理失败项
                totalFailed += countFailed(response);
                failedItems.addAll(getFailedItems(response));
            } else {
                totalSuccess += docs.size();
            }

        } catch (Exception e) {
            // 整批失败
            totalFailed += batch.size();
            batch.forEach(item -> failedItems.add(item.getBusinessId()));
            log.error("批次处理失败", e);
        }
    }

    // 步骤4:返回汇总结果
    return BatchIndexResult.builder()
        .totalCount(items.size())
        .successCount(totalSuccess)
        .failedCount(totalFailed)
        .failedItems(failedItems)
        .build();
}

9.3 向量搜索详细流程

public List<SearchResult> search(String query, int topK) {
    // 步骤1:查询向量化
    float[] queryVector = embeddingClient.getEmbedding(query);

    // 步骤2:KNN搜索
    SearchRequest searchRequest = new SearchRequest(indexName);
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

    // 构建KNN查询
    sourceBuilder.query(QueryBuilders.knnQuery(
        "embedding",
        queryVector,
        topK
    ).numCandidates(100));  // 候选文档数

    searchRequest.source(sourceBuilder);

    // 步骤3:执行搜索
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

    // 步骤4:解析结果
    List<SearchResult> results = new ArrayList<>();
    for (SearchHit hit : response.getHits().getHits()) {
        Map<String, Object> sourceMap = hit.getSourceAsMap();
        float score = hit.getScore();

        SearchResult result = SearchResult.builder()
            .docId(hit.getId())
            .content((String) sourceMap.get("content"))
            .businessId((String) sourceMap.get("business_id"))
            .score(score)
            .build();

        results.add(result);
    }

    return results;
}

十、部署与配置

6.1 应用配置清单

必需配置:

  • embedding.api.url:Embedding接口地址
  • embedding.api.key:API密钥
  • embedding.batch.size:批量大小(默认128)
  • elasticsearch.host:ES地址
  • elasticsearch.index.name:索引名称

可选配置:

  • embedding.timeout:接口超时时间(毫秒)
  • elasticsearch.index.shards:索引分片数
  • elasticsearch.index.replicas:索引副本数

6.2 初始化检查

应用启动时检查:

  1. Embedding接口连通性
  2. ES集群连接状态
  3. 索引是否存在(不存在则创建)
  4. 索引映射是否正确

十一、监控与日志

5.1 关键指标监控

  • Embedding接口:

    • 调用次数
    • 平均响应时间
    • 失败率
  • ES操作:

    • 文档写入数量
    • 搜索请求次数
    • 平均查询时间

5.2 日志记录

关键日志点:

  • 入库请求:businessId、内容长度
  • Embedding调用:请求ID、响应时间
  • ES写入:文档ID、写入结果
  • 异常情况:异常类型、错误信息、堆栈

十二、总结

6.1 方案优势

  1. 高可用性:

    • 异常重试机制
    • 批量处理失败隔离
    • 幂等性设计
  2. 高性能:

    • 批量向量化(128条/批)
    • ES Bulk API批量写入
    • HTTP连接池复用
  3. 可扩展性:

    • 分层架构清晰
    • 配置灵活
    • 易于扩展新功能

5.2 下一步工作

  1. 代码实现:

    • 按照上述结构编写完整代码
    • 单元测试覆盖
    • 集成测试验证
  2. 性能测试:

    • 压力测试
    • 批量入库性能测试
    • 搜索性能测试
  3. 优化迭代:

    • 根据测试结果优化参数
    • 添加缓存机制(如需要)
    • 优化索引配置

十三、附录:接口测试命令

13.1 Embedding接口测试

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集群状态测试

curl -X GET "http://10.192.72.13:9200/_cluster/health?pretty"

13.3 创建向量索引

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 查看索引映射

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完整架构) 测试环境: 内网环境验证通过