第9天 — Embedding 向量嵌入基础
Spring AI Alibaba 技术博客系列 | 2026-05-03
本文深入讲解 Embedding 向量嵌入的核心概念、原理、以及在 Spring AI Alibaba 中的完整使用方式。这是进入 RAG(检索增强生成)世界的关键前置知识。
目录
- 什么是 Embedding?
- Embedding 的工作原理
- Spring AI Alibaba 中的 EmbeddingModel 体系
- 快速入门:第一个 Embedding 应用
- 文本相似度计算实战
- 批量 Embedding 生成
- Embedding 模型选择指南
- Embedding 维度与性能调优
- 常见陷阱与最佳实践
- 完整实战项目:文档语义搜索
1. 什么是 Embedding?
Embedding(向量嵌入)是将文本、图像等高维非结构化数据转换为低维稠密向量(Dense Vector)的技术。转换后的向量保留了原始数据的语义信息——语义相近的文本,其向量在空间中的距离也更近。
举个直观的例子:
"猫是一种可爱的动物" → [0.12, -0.34, 0.56, ..., 0.78] (1536维向量)
"狗狗是人类的好朋友" → [0.15, -0.31, 0.52, ..., 0.81] (1536维向量)
"Python是一种编程语言" → [-0.45, 0.67, -0.23, ..., 0.11] (1536维向量)
前两个向量很接近(都在讨论宠物),第三个向量则远离它们(编程话题)。这就是 Embedding 的魔力:机器”理解”了语义。
为什么需要 Embedding?
| 传统文本处理 | Embedding 向量处理 |
|---|---|
| 关键词匹配(BM25) | 语义匹配 |
| “apple” ≠ “水果” | 向量距离近 |
| 无法理解上下文 | 保留语义关系 |
| 精确匹配,召回率低 | 模糊匹配,召回率高 |
| 不支持多语言对齐 | 多语言共享向量空间 |
在 AI 应用中,Embedding 是以下场景的基石:
- 语义搜索:用户搜索”如何做红烧肉”,能匹配到”五花肉烹饪教程”
- RAG 检索增强:从知识库中找到与问题最相关的段落
- 文本聚类:自动将相似文档分组
- 推荐系统:基于内容相似度推荐
- 去重检测:发现语义重复的内容
2. Embedding 的工作原理
2.1 从 One-Hot 到 Dense Vector
早期的文本表示使用 One-Hot 编码:每个词对应一个巨大的稀疏向量,只有一个位置为1,其余全为0。这种方式存在严重问题:
词典大小: 100,000
"猫" → [0, 0, ..., 1, ..., 0, 0] (第2345位为1)
"狗" → [0, 0, ..., 1, ..., 0, 0] (第8912位为1)
这两个向量完全正交(点积为0),无法表达”猫”和”狗”都是宠物这个语义关系。
Embedding 通过神经网络学习,将词汇映射到连续的低维空间:
"猫" → [0.12, -0.34, 0.56, ...]
"狗" → [0.15, -0.31, 0.52, ...]
"汽车" → [-0.45, 0.67, -0.23, ...]
2.2 Transformer 编码器
现代 Embedding 模型(如 OpenAI 的 text-embedding-ada-002、通义千问的 text-embedding-v3)基于 Transformer 架构:
输入文本
↓
Tokenization (分词)
↓
Token Embedding + Position Embedding
↓
Transformer Encoder Layers (多层注意力)
↓
[CLS] Token 的输出向量
↓
归一化 (L2 Normalization)
↓
最终的 Embedding 向量
关键要点:
- 注意力机制让模型能够捕捉词与词之间的长距离依赖关系
- 多层编码逐层提取从局部到全局的语义特征
- [CLS] token 经过所有层后,包含了整个句子的聚合信息
- L2 归一化确保所有向量长度为1,便于用点积计算余弦相似度
2.3 余弦相似度
两个向量的相似度通常用余弦相似度衡量:
cos(A, B) = (A · B) / (|A| × |B|)
由于 Embedding 向量已经经过 L2 归一化(|A| = |B| = 1),公式简化为:
cos(A, B) = A · B (直接点积)
相似度范围:-1(完全相反)到 1(完全相同)。通常 0.7 以上表示高度相关,0.5 以上表示有一定相关性。
3. Spring AI Alibaba 中的 EmbeddingModel 体系
3.1 核心接口
Spring AI Alibaba 的 Embedding 抽象基于 Spring AI 标准接口:
// 核心接口
public interface EmbeddingModel {
// 对单个字符串生成 Embedding
Embedding embed(String text);
// 批量生成 Embedding
Embeddings call(EmbeddingRequest request);
}
// Embedding 对象
public class Embedding {
private List<Double> embedding; // 向量值
private String content; // 原始文本
// ...
}
// 请求对象
public class EmbeddingRequest {
private List<String> prompts; // 待嵌入的文本列表
private EmbeddingOptions options; // 选项配置
// ...
}
3.2 支持的 Embedding 模型
Spring AI Alibaba 支持多种 Embedding 模型提供商:
| 提供商 | 模型 | 维度 | 特点 |
|---|---|---|---|
| 阿里云 DashScope | text-embedding-v3 | 1024 | 多语言,支持细粒度维度选择 |
| 阿里云 DashScope | text-embedding-v2 | 1536 | 经典模型,稳定可靠 |
| OpenAI | text-embedding-ada-002 | 1536 | 生态最广 |
| OpenAI | text-embedding-3-small | 1536 | 性价比最优 |
| OpenAI | text-embedding-3-large | 3072 | 效果最好 |
3.3 依赖配置
<!-- DashScope Embedding(阿里云通义千问) -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>1.0.0-M6.1</version>
</dependency>
<!-- 或者单独的 DashScope 适配器 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.0.0-M6.1</version>
</dependency>
4. 快速入门:第一个 Embedding 应用
4.1 项目初始化
# 使用 Spring Initializr 创建项目
curl https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.3.5 \
-d baseDir=embedding-demo \
-d dependencies=web,spring-ai \
-o embedding-demo.zip
4.2 配置文件
# application.yml
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 从环境变量读取
embedding:
options:
model: text-embedding-v3
dimensions: 1024 # 可选,v3支持自定义维度
# 如果你使用 OpenAI:
# spring:
# ai:
# openai:
# api-key: ${OPENAI_API_KEY}
# embedding:
# options:
# model: text-embedding-3-small
4.3 基础使用示例
package com.example.embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.document.MetadataMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/embedding")
public class EmbeddingController {
private final EmbeddingModel embeddingModel;
@Autowired
public EmbeddingController(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/**
* 对单个文本生成 Embedding
*/
@GetMapping("/single")
public Map<String, Object> singleEmbedding(@RequestParam String text) {
Embedding embedding = embeddingModel.embed(text);
return Map.of(
"text", text,
"dimensions", embedding.getEmbedding().size(),
"vector_preview", embedding.getEmbedding().subList(0, Math.min(5, embedding.getEmbedding().size()))
);
}
/**
* 批量生成 Embedding
*/
@PostMapping("/batch")
public List<Map<String, Object>> batchEmbedding(@RequestBody List<String> texts) {
EmbeddingRequest request = new EmbeddingRequest(texts, null);
var response = embeddingModel.call(request);
return response.getResults().stream()
.map(result -> Map.of(
"text", result.getOutput().getContent(),
"dimensions", result.getOutput().getEmbedding().size(),
"vector_preview", result.getOutput().getEmbedding().subList(0, 5)
))
.toList();
}
}
4.4 测试
# 单个文本 Embedding
curl "http://localhost:8080/api/embedding/single?text=人工智能正在改变世界"
# 批量文本 Embedding
curl -X POST http://localhost:8080/api/embedding/batch \
-H "Content-Type: application/json" \
-d '["猫是可爱的动物", "Python是编程语言", "红烧肉怎么做"]'
5. 文本相似度计算实战
Embedding 最有价值的应用之一就是计算文本之间的语义相似度。
5.1 相似度计算服务
package com.example.embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class SemanticSimilarityService {
private final EmbeddingModel embeddingModel;
public SemanticSimilarityService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/**
* 计算两个文本之间的余弦相似度
*/
public double cosineSimilarity(String text1, String text2) {
// 分别获取两个文本的 Embedding
List<Double> vec1 = embeddingModel.embed(text1).getEmbedding();
List<Double> vec2 = embeddingModel.embed(text2).getEmbedding();
return cosineSimilarity(vec1, vec2);
}
/**
* 计算两个向量的余弦相似度
*/
private double cosineSimilarity(List<Double> vec1, List<Double> vec2) {
if (vec1.size() != vec2.size()) {
throw new IllegalArgumentException("向量维度不一致");
}
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
for (int i = 0; i < vec1.size(); i++) {
double v1 = vec1.get(i);
double v2 = vec2.get(i);
dotProduct += v1 * v2;
norm1 += v1 * v1;
norm2 += v2 * v2;
}
double denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
if (denominator == 0.0) {
return 0.0;
}
return dotProduct / denominator;
}
/**
* 从候选文本中找出与查询最相似的 Top-K
*/
public List<SimilarityResult> findTopK(String query, List<String> candidates, int topK) {
// 获取查询文本的 Embedding
List<Double> queryVec = embeddingModel.embed(query).getEmbedding();
// 计算与所有候选文本的相似度
List<SimilarityResult> results = new ArrayList<>();
for (String candidate : candidates) {
List<Double> candidateVec = embeddingModel.embed(candidate).getEmbedding();
double similarity = cosineSimilarity(queryVec, candidateVec);
results.add(new SimilarityResult(candidate, similarity));
}
// 按相似度降序排序,取 Top-K
return results.stream()
.sorted(Comparator.comparingDouble(SimilarityResult::similarity).reversed())
.limit(topK)
.toList();
}
/**
* 相似度结果记录
*/
public record SimilarityResult(String text, double similarity) {}
}
5.2 相似度 API 接口
@RestController
@RequestMapping("/api/similarity")
public class SimilarityController {
private final SemanticSimilarityService similarityService;
public SimilarityController(SemanticSimilarityService similarityService) {
this.similarityService = similarityService;
}
@GetMapping("/compare")
public Map<String, Object> compare(
@RequestParam String text1,
@RequestParam String text2) {
double similarity = similarityService.cosineSimilarity(text1, text2);
return Map.of(
"text1", text1,
"text2", text2,
"similarity", String.format("%.4f", similarity),
"interpretation", interpret(similarity)
);
}
@PostMapping("/topk")
public List<Map<String, Object>> findTopK(
@RequestParam String query,
@RequestBody List<String> candidates,
@RequestParam(defaultValue = "3") int topK) {
return similarityService.findTopK(query, candidates, topK).stream()
.map(r -> Map.of(
"text", r.text(),
"similarity", String.format("%.4f", r.similarity())
))
.toList();
}
private String interpret(double similarity) {
if (similarity >= 0.85) return "高度相似";
if (similarity >= 0.70) return "较为相似";
if (similarity >= 0.50) return "有一定相关性";
if (similarity >= 0.30) return "弱相关";
return "几乎无关";
}
}
5.3 实际测试效果
# 测试1:语义相近
curl "http://localhost:8080/api/similarity/compare?text1=如何做红烧肉&text2=五花肉烹饪方法"
# 预期输出: similarity ≈ 0.85+,interpretation: 高度相似
# 测试2:语义无关
curl "http://localhost:8080/api/similarity/compare?text1=如何做红烧肉&text2=Java并发编程"
# 预期输出: similarity ≈ 0.20-,interpretation: 几乎无关
# 测试3:跨语言语义(v3模型支持)
curl "http://localhost:8080/api/similarity/compare?text1=人工智能&text2=Artificial+Intelligence"
# 预期输出: similarity ≈ 0.90+,interpretation: 高度相似
6. 批量 Embedding 生成
在生产环境中,通常需要批量处理大量文本。Spring AI Alibaba 支持批量请求。
6.1 批量 Embedding 服务
package com.example.embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.Embedding;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.IntStream;
@Service
public class BatchEmbeddingService {
private final EmbeddingModel embeddingModel;
public BatchEmbeddingService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/**
* 批量生成 Embedding(带分片处理,避免超出 API 限制)
*
* 大多数 Embedding API 对单次请求有数量限制(如 DashScope 最多25条)
*/
public List<EmbeddingResult> batchEmbed(List<String> texts, int batchSize) {
List<EmbeddingResult> results = new ArrayList<>();
// 分片处理
List<List<String>> batches = partition(texts, batchSize);
for (List<String> batch : batches) {
EmbeddingRequest request = new EmbeddingRequest(batch, null);
EmbeddingResponse response = embeddingModel.call(request);
// 将结果与原文本关联
for (int i = 0; i < response.getResults().size(); i++) {
Embedding embedding = response.getResults().get(i).getOutput();
results.add(new EmbeddingResult(
batch.get(i),
embedding.getEmbedding()
));
}
}
return results;
}
/**
* 分片工具方法
*/
private <T> List<List<T>> partition(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
partitions.add(list.subList(i, Math.min(i + size, list.size())));
}
return partitions;
}
/**
* 将 Embedding 结果转为结构化对象
*/
public record EmbeddingResult(String text, List<Double> embedding) {}
}
6.2 性能优化:并行处理
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class ParallelEmbeddingService {
private final EmbeddingModel embeddingModel;
private final ExecutorService executor;
public ParallelEmbeddingService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
// 根据 API 限流情况调整线程数
this.executor = Executors.newFixedThreadPool(4);
}
/**
* 并行批量 Embedding
*/
public List<EmbeddingResult> parallelBatchEmbed(List<String> texts, int batchSize) {
List<List<String>> batches = partition(texts, batchSize);
// 并行处理每个批次
List<CompletableFuture<List<EmbeddingResult>>> futures = batches.stream()
.map(batch -> CompletableFuture.supplyAsync(
() -> processBatch(batch),
executor
))
.toList();
// 合并结果
return futures.stream()
.map(CompletableFuture::join)
.flatMap(List::stream)
.toList();
}
private List<EmbeddingResult> processBatch(List<String> batch) {
EmbeddingRequest request = new EmbeddingRequest(batch, null);
EmbeddingResponse response = embeddingModel.call(request);
List<EmbeddingResult> results = new ArrayList<>();
for (int i = 0; i < response.getResults().size(); i++) {
results.add(new EmbeddingResult(
batch.get(i),
response.getResults().get(i).getOutput().getEmbedding()
));
}
return results;
}
private <T> List<List<T>> partition(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
partitions.add(list.subList(i, Math.min(i + size, list.size())));
}
return partitions;
}
public void shutdown() {
executor.shutdown();
}
}
7. Embedding 模型选择指南
7.1 主流模型对比
| 特性 | text-embedding-v3 | text-embedding-v2 | text-embedding-3-small | text-embedding-3-large |
|---|---|---|---|---|
| 提供商 | 阿里云 DashScope | 阿里云 DashScope | OpenAI | OpenAI |
| 默认维度 | 1024 | 1536 | 1536 | 3072 |
| 可调维度 | ✅ 256/512/1024 | ❌ | ✅ 256/1024/1536 | ✅ 256/1024/3072 |
| 多语言 | ✅ | ✅ 有限 | ✅ | ✅ |
| 中文效果 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 速度 | 快 | 快 | 快 | 中等 |
| 成本 | 低 | 低 | 低 | 较高 |
| 最大输入 | 8192 tokens | 2048 tokens | 8191 tokens | 8191 tokens |
7.2 选择建议
场景:中文为主的业务
推荐:text-embedding-v3
理由:中文语义理解最佳,维度可调,性价比高
场景:多语言/国际化
推荐:text-embedding-v3 或 text-embedding-3-small
理由:多语言支持好,生态广泛
场景:极致精度要求
推荐:text-embedding-3-large
理由:3072维,语义表达能力最强
场景:存储/带宽受限
推荐:text-embedding-v3 (256维) 或 text-embedding-3-small (256维)
理由:可调低维度,节省存储空间
7.3 动态切换模型
package com.example.embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.dashscope.DashScopeEmbeddingModel;
import org.springframework.ai.dashscope.api.DashScopeApi;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EmbeddingModelConfig {
/**
* 默认使用 v3 模型
*/
@Bean
@Qualifier("defaultEmbedding")
public EmbeddingModel defaultEmbedding(DashScopeApi dashScopeApi) {
return new DashScopeEmbeddingModel(
dashScopeApi,
MetadataMode.EMBED,
"text-embedding-v3"
);
}
/**
* v2 模型(兼容旧数据)
*/
@Bean
@Qualifier("v2Embedding")
public EmbeddingModel v2Embedding(DashScopeApi dashScopeApi) {
return new DashScopeEmbeddingModel(
dashScopeApi,
MetadataMode.EMBED,
"text-embedding-v2"
);
}
}
// 使用时注入指定的模型
@Service
public class DualModelService {
private final EmbeddingModel defaultEmbedding;
private final EmbeddingModel v2Embedding;
public DualModelService(
@Qualifier("defaultEmbedding") EmbeddingModel defaultEmbedding,
@Qualifier("v2Embedding") EmbeddingModel v2Embedding) {
this.defaultEmbedding = defaultEmbedding;
this.v2Embedding = v2Embedding;
}
public Embedding embedWithV3(String text) {
return defaultEmbedding.embed(text);
}
public Embedding embedWithV2(String text) {
return v2Embedding.embed(text);
}
}
8. Embedding 维度与性能调优
8.1 维度选择的影响
维度越高:
✅ 语义表达能力越强
✅ 区分度越高
❌ 存储空间越大
❌ 计算距离越慢
❌ 向量索引构建时间越长
维度越低:
✅ 存储紧凑
✅ 计算快速
❌ 语义表达能力有限
❌ 可能出现语义混淆
8.2 存储大小估算
假设 100 万条文档,每条一个 Embedding 向量:
3072维 (float32): 100万 × 3072 × 4 bytes ≈ 11.7 GB
1536维 (float32): 100万 × 1536 × 4 bytes ≈ 5.9 GB
1024维 (float32): 100万 × 1024 × 4 bytes ≈ 3.9 GB
512维 (float32): 100万 × 512 × 4 bytes ≈ 2.0 GB
256维 (float32): 100万 × 256 × 4 bytes ≈ 1.0 GB
如果使用 float16 或 quantization,可以再减半。
8.3 维度裁剪(Dimensionality Reduction)
如果现有模型不支持维度调节,可以通过矩阵投影来降低维度:
package com.example.embedding;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Component;
/**
* 维度裁剪工具 - 通过 PCA 或随机投影降低向量维度
*
* 注意:这只是近似方案,会损失一定精度。
* 优先使用模型原生支持的维度调节功能。
*/
@Component
public class DimensionReducer {
private final EmbeddingModel embeddingModel;
public DimensionReducer(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/**
* 简单截断(只适用于向量尾部信息量较少的情况)
* 注意:这不是推荐做法,仅作参考
*/
public List<Double> truncate(List<Double> vector, int targetDim) {
if (vector.size() <= targetDim) {
return vector;
}
return vector.subList(0, targetDim);
}
/**
* 平均池化降维
* 将向量分段后取平均值
*/
public List<Double> averagePool(List<Double> vector, int targetDim) {
int chunkSize = (int) Math.ceil((double) vector.size() / targetDim);
List<Double> result = new ArrayList<>();
for (int i = 0; i < targetDim; i++) {
int start = i * chunkSize;
int end = Math.min(start + chunkSize, vector.size());
double sum = 0;
for (int j = start; j < end; j++) {
sum += vector.get(j);
}
result.add(sum / (end - start));
}
// L2 归一化
return normalize(result);
}
private List<Double> normalize(List<Double> vector) {
double norm = Math.sqrt(
vector.stream().mapToDouble(v -> v * v).sum()
);
if (norm == 0) return vector;
return vector.stream()
.map(v -> v / norm)
.toList();
}
}
9. 常见陷阱与最佳实践
9.1 陷阱1:不同模型的向量不能混用
// ❌ 错误做法
Embedding vec1 = openaiEmbeddingModel.embed("你好"); // OpenAI 的向量空间
Embedding vec2 = dashScopeEmbeddingModel.embed("你好"); // DashScope 的向量空间
double similarity = cosineSimilarity(vec1, vec2); // 结果无意义!
// ✅ 正确做法:统一使用同一个模型
Embedding vec1 = embeddingModel.embed("你好");
Embedding vec2 = embeddingModel.embed("你好");
double similarity = cosineSimilarity(vec1, vec2); // 有意义
不同模型训练的向量空间完全不同,即使输入相同文本,生成的向量也完全不同。混用会导致相似度计算完全错误。
9.2 陷阱2:文本预处理不一致
// ❌ 错误:索引时和检索时预处理方式不同
// 索引时:去掉了标点符号
String indexedText = "红烧肉是一道经典的中国菜";
// 检索时:保留了标点符号
String queryText = "红烧肉怎么做?";
// ✅ 正确:保持一致的预处理
public String normalizeText(String text) {
return text.trim()
.replaceAll("\\s+", " ") // 合并多余空格
.toLowerCase(); // 统一小写(英文场景)
}
String indexedText = normalizeText("红烧肉是一道经典的中国菜");
String queryText = normalizeText("红烧肉怎么做?");
9.3 陷阱3:忽略 Token 长度限制
// ❌ 错误:超长文本直接传入
String longText = readFile("very_long_document.txt"); // 可能超过8192 tokens
Embedding embedding = embeddingModel.embed(longText); // 可能报错或截断
// ✅ 正确:先分块,再 Embedding
public List<Embedding> embedDocument(String document, int maxTokensPerChunk) {
List<String> chunks = splitIntoChunks(document, maxTokensPerChunk);
return chunks.stream()
.map(embeddingModel::embed)
.toList();
}
// 粗略估算:1个中文字 ≈ 1.5 tokens
// 8192 tokens ≈ 5500 个中文字
private List<String> splitIntoChunks(String text, int maxTokens) {
// 实现文本分块逻辑
// 注意:应该按语义边界分块(句子/段落),而不是简单按字符数
// 我们将在后续文章中详细讨论文本分块策略
return List.of(text); // 简化示例
}
9.4 陷阱4:Embedding 不是万能的
Embedding 擅长:
✅ 语义匹配("红烧肉" ≈ "五花肉烹饪")
✅ 主题相关(AI/机器学习/深度学习 相近)
✅ 意图理解("我想退款" ≈ "申请退货")
Embedding 不擅长:
❌ 精确匹配(身份证号、订单号、SKU)
❌ 数值比较(价格高低、日期先后)
❌ 逻辑推理(A>B, B>C => A>C)
❌ 专业术语精确匹配(药物分子式、法律条文编号)
最佳实践:Embedding + 关键词过滤 + 元数据过滤 = 最佳检索效果
9.5 最佳实践清单
1. ✅ 始终对 Embedding 向量做 L2 归一化(模型通常已内置)
2. ✅ 建立统一的文本预处理 pipeline
3. ✅ 同一应用内使用同一个 Embedding 模型
4. ✅ 监控 Embedding API 的延迟和错误率
5. ✅ 缓存常用文本的 Embedding 结果
6. ✅ 选择合适维度,平衡精度与性能
7. ✅ 定期评估 Embedding 质量(人工标注 + 自动评估)
8. ✅ 保留原始文本,方便调试和重新索引
9. ✅ 使用批量 API 而非逐个调用
10. ✅ 设计降级策略(Embedding 服务不可用时)
10. 完整实战项目:文档语义搜索
最后,我们构建一个完整的文档语义搜索服务,这是 RAG 系统的核心组件。
10.1 项目结构
document-search/
├── src/main/java/com/example/search/
│ ├── DocumentSearchApplication.java
│ ├── controller/SearchController.java
│ ├── service/DocumentService.java
│ ├── service/EmbeddingService.java
│ ├── service/SearchService.java
│ ├── model/Document.java
│ ├── model/SearchResult.java
│ └── config/EmbeddingConfig.java
└── src/main/resources/
└── application.yml
10.2 数据模型
package com.example.search.model;
import java.util.List;
/**
* 文档模型
*/
public record Document(
String id,
String title,
String content,
String category,
List<Double> embedding, // 文档的 Embedding 向量
long createdAt
) {}
/**
* 搜索结果
*/
public record SearchResult(
String id,
String title,
String content,
String category,
double score,
long createdAt
) {}
10.3 Embedding 服务
package com.example.search.service;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmbeddingService {
private final EmbeddingModel embeddingModel;
public EmbeddingService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/**
* 生成单条文本的 Embedding
*/
public List<Double> embed(String text) {
Embedding embedding = embeddingModel.embed(text);
return embedding.getEmbedding();
}
/**
* 批量生成 Embedding
*/
public List<List<Double>> embedBatch(List<String> texts) {
if (texts.isEmpty()) {
return List.of();
}
// DashScope 限制单次最多 25 条
int batchSize = 25;
List<List<Double>> allEmbeddings = new java.util.ArrayList<>();
for (int i = 0; i < texts.size(); i += batchSize) {
int end = Math.min(i + batchSize, texts.size());
List<String> batch = texts.subList(i, end);
EmbeddingRequest request = new EmbeddingRequest(batch, null);
EmbeddingResponse response = embeddingModel.call(request);
for (var result : response.getResults()) {
allEmbeddings.add(result.getOutput().getEmbedding());
}
}
return allEmbeddings;
}
}
10.4 文档服务(内存存储,实际项目应使用向量数据库)
package com.example.search.service;
import com.example.search.model.Document;
import com.example.search.model.SearchResult;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class DocumentService {
private final EmbeddingService embeddingService;
private final Map<String, Document> documentStore = new ConcurrentHashMap<>();
public DocumentService(EmbeddingService embeddingService) {
this.embeddingService = embeddingService;
}
/**
* 添加文档(自动生成 Embedding)
*/
public Document addDocument(String id, String title, String content, String category) {
// 合并标题和内容作为 Embedding 输入
String embedText = title + " " + content;
List<Double> embedding = embeddingService.embed(embedText);
Document doc = new Document(
id,
title,
content,
category,
embedding,
System.currentTimeMillis()
);
documentStore.put(id, doc);
return doc;
}
/**
* 批量添加文档
*/
public List<Document> addDocuments(List<DocumentInput> inputs) {
List<String> texts = inputs.stream()
.map(input -> input.title() + " " + input.content())
.toList();
List<List<Double>> embeddings = embeddingService.embedBatch(texts);
List<Document> docs = new ArrayList<>();
for (int i = 0; i < inputs.size(); i++) {
DocumentInput input = inputs.get(i);
Document doc = new Document(
input.id(),
input.title(),
input.content(),
input.category(),
embeddings.get(i),
System.currentTimeMillis()
);
documentStore.put(doc.id(), doc);
docs.add(doc);
}
return docs;
}
/**
* 获取所有文档
*/
public Collection<Document> getAllDocuments() {
return documentStore.values();
}
/**
* 删除文档
*/
public boolean removeDocument(String id) {
return documentStore.remove(id) != null;
}
/**
* 文档输入
*/
public record DocumentInput(String id, String title, String content, String category) {}
}
10.5 搜索服务
package com.example.search.service;
import com.example.search.model.Document;
import com.example.search.model.SearchResult;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class SearchService {
private final DocumentService documentService;
private final EmbeddingService embeddingService;
public SearchService(DocumentService documentService, EmbeddingService embeddingService) {
this.documentService = documentService;
this.embeddingService = embeddingService;
}
/**
* 语义搜索
*/
public List<SearchResult> semanticSearch(String query, int topK) {
// 1. 对查询生成 Embedding
List<Double> queryEmbedding = embeddingService.embed(query);
// 2. 计算与所有文档的相似度
List<SearchResult> results = documentService.getAllDocuments().stream()
.map(doc -> {
double score = cosineSimilarity(queryEmbedding, doc.embedding());
return new SearchResult(
doc.id(),
doc.title(),
truncateContent(doc.content(), 200),
doc.category(),
score,
doc.createdAt()
);
})
.filter(r -> r.score() > 0.3) // 过滤低相似度结果
.sorted(Comparator.comparingDouble(SearchResult::score).reversed())
.limit(topK)
.collect(Collectors.toList());
return results;
}
/**
* 带类别过滤的语义搜索
*/
public List<SearchResult> semanticSearchWithFilter(
String query,
String category,
int topK) {
List<Double> queryEmbedding = embeddingService.embed(query);
List<SearchResult> results = documentService.getAllDocuments().stream()
.filter(doc -> category == null || category.equals(doc.category()))
.map(doc -> {
double score = cosineSimilarity(queryEmbedding, doc.embedding());
return new SearchResult(
doc.id(),
doc.title(),
truncateContent(doc.content(), 200),
doc.category(),
score,
doc.createdAt()
);
})
.filter(r -> r.score() > 0.3)
.sorted(Comparator.comparingDouble(SearchResult::score).reversed())
.limit(topK)
.collect(Collectors.toList());
return results;
}
/**
* 余弦相似度计算
*/
private double cosineSimilarity(List<Double> vec1, List<Double> vec2) {
double dotProduct = 0;
double norm1 = 0;
double norm2 = 0;
for (int i = 0; i < vec1.size(); i++) {
double v1 = vec1.get(i);
double v2 = vec2.get(i);
dotProduct += v1 * v2;
norm1 += v1 * v1;
norm2 += v2 * v2;
}
double denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
return denominator == 0 ? 0 : dotProduct / denominator;
}
/**
* 截断内容用于展示
*/
private String truncateContent(String content, int maxLength) {
if (content == null || content.length() <= maxLength) {
return content;
}
return content.substring(0, maxLength) + "...";
}
}
10.6 控制器
package com.example.search.controller;
import com.example.search.model.Document;
import com.example.search.model.SearchResult;
import com.example.search.service.DocumentService;
import com.example.search.service.DocumentService.DocumentInput;
import com.example.search.service.SearchService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/search")
public class SearchController {
private final SearchService searchService;
private final DocumentService documentService;
public SearchController(SearchService searchService, DocumentService documentService) {
this.searchService = searchService;
this.documentService = documentService;
}
/**
* 语义搜索接口
*/
@GetMapping
public List<SearchResult> search(
@RequestParam String q,
@RequestParam(defaultValue = "5") int topK,
@RequestParam(required = false) String category) {
if (category != null) {
return searchService.semanticSearchWithFilter(q, category, topK);
}
return searchService.semanticSearch(q, topK);
}
/**
* 添加单个文档
*/
@PostMapping("/document")
public Document addDocument(@RequestBody DocumentInput input) {
return documentService.addDocument(
input.id(),
input.title(),
input.content(),
input.category()
);
}
/**
* 批量添加文档
*/
@PostMapping("/documents")
public List<Document> addDocuments(@RequestBody List<DocumentInput> inputs) {
return documentService.addDocuments(inputs);
}
/**
* 列出所有文档
*/
@GetMapping("/documents")
public Map<String, Object> listDocuments() {
var docs = documentService.getAllDocuments();
return Map.of(
"total", docs.size(),
"documents", docs.stream()
.map(d -> Map.of(
"id", d.id(),
"title", d.title(),
"category", d.category()
))
.toList()
);
}
/**
* 删除文档
*/
@DeleteMapping("/document/{id}")
public Map<String, Boolean> deleteDocument(@PathVariable String id) {
boolean deleted = documentService.removeDocument(id);
return Map.of("deleted", deleted);
}
}
10.7 初始化测试数据
package com.example.search;
import com.example.search.service.DocumentService;
import com.example.search.service.DocumentService.DocumentInput;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DataInitializer implements CommandLineRunner {
private final DocumentService documentService;
public DataInitializer(DocumentService documentService) {
this.documentService = documentService;
}
@Override
public void run(String... args) {
List<DocumentInput> sampleDocs = List.of(
new DocumentInput("doc1",
"Spring Boot 入门教程",
"Spring Boot 是一个用于创建独立的、生产级别的 Spring 应用的框架。它简化了 Spring 应用的初始搭建和开发过程。",
"Java"),
new DocumentInput("doc2",
"Python 数据分析指南",
"Python 是数据科学和机器学习领域最流行的编程语言之一。Pandas、NumPy、Matplotlib 等库提供了强大的数据处理和可视化能力。",
"Python"),
new DocumentInput("doc3",
"机器学习基础概念",
"机器学习是人工智能的一个重要分支,通过算法让计算机从数据中学习模式并做出预测。常见算法包括线性回归、决策树、神经网络等。",
"AI"),
new DocumentInput("doc4",
"深度学习与神经网络",
"深度学习是机器学习的子领域,使用多层神经网络来学习数据的层次化表示。CNN 用于图像处理,RNN 用于序列数据,Transformer 是当前最强大的架构。",
"AI"),
new DocumentInput("doc5",
"Java 并发编程实战",
"Java 提供了丰富的并发编程工具,包括 Thread、ExecutorService、CompletableFuture、synchronized 和 Lock 等。合理使用这些工具可以编写高效的并发程序。",
"Java"),
new DocumentInput("doc6",
"RESTful API 设计规范",
"RESTful API 设计遵循无状态、资源导向的原则。使用 HTTP 方法(GET/POST/PUT/DELETE)对应 CRUD 操作,URL 表示资源,状态码表示操作结果。",
"架构"),
new DocumentInput("doc7",
"Docker 容器化部署",
"Docker 是一种容器化技术,将应用及其依赖打包成镜像,实现环境一致性。相比虚拟机,容器更轻量、启动更快、资源利用更高效。",
"DevOps"),
new DocumentInput("doc8",
"MySQL 性能优化技巧",
"MySQL 性能优化涉及索引设计、查询优化、表结构设计和服务器配置等方面。合理使用 EXPLAIN 分析查询计划是优化的第一步。",
"数据库")
);
documentService.addDocuments(sampleDocs);
System.out.println("✅ 已初始化 " + sampleDocs.size() + " 条测试文档");
}
}
10.8 完整测试
# 启动应用
./mvnw spring-boot:run
# 1. 查看文档列表
curl http://localhost:8080/api/search/documents
# 2. 语义搜索:"如何学习编程"
curl "http://localhost:8080/api/search?q=如何学习编程&topK=3"
# 预期结果(按相似度排序):
# 1. Python 数据分析指南 (AI/编程相关)
# 2. Spring Boot 入门教程 (教程类)
# 3. 机器学习基础概念 (学习相关)
# 3. 语义搜索:"神经网络是什么"
curl "http://localhost:8080/api/search?q=神经网络是什么&topK=3"
# 预期结果:
# 1. 深度学习与神经网络 (直接匹配)
# 2. 机器学习基础概念 (相关概念)
# 3. Python 数据分析指南 (工具相关)
# 4. 跨语言搜索:"neural network"
curl "http://localhost:8080/api/search?q=neural+network&topK=3"
# 预期结果(v3模型支持中英文跨语言):
# 1. 深度学习与神经网络
# 2. 机器学习基础概念
# 5. 带类别过滤的搜索
curl "http://localhost:8080/api/search?q=教程&category=Java&topK=3"
# 预期结果:只在 Java 类别中搜索
# 1. Spring Boot 入门教程
# 2. Java 并发编程实战
总结
Embedding 是 AI 应用的基础设施,它将非结构化文本转化为机器可理解的向量表示。本文从原理到实践,完整介绍了:
- Embedding 基本概念:什么是向量嵌入、为什么需要它
- 工作原理:从 Token 到 Dense Vector 的完整流程
- Spring AI Alibaba 集成:DashScope Embedding 的配置和使用
- 相似度计算:余弦相似度的实现与应用
- 批量处理:分片、并行、性能优化
- 模型选择:不同 Embedding 模型的对比与选择策略
- 避坑指南:4大常见陷阱和10条最佳实践
- 完整实战:文档语义搜索系统的实现
掌握了 Embedding,你就为下一步的 Vector Store 集成 和 RAG 检索增强生成 打下了坚实基础。下一篇我们将深入 Vector Store,学习如何将 Embedding 向量存储到 Milvus、Redis 或 PostgreSQL 中,实现高效的向量检索。
下一篇预告:第10天 — Vector Store 集成(Milvus / Redis / PostgreSQL)
我们将学习多种向量数据库的集成方式,对比各自的优劣势,并构建生产级的向量检索服务。敬请期待!