Spring AI 源码解析(四):Embedding 与 VectorStore 实现原理
Spring AI 源码解析(四):Embedding 与 VectorStore 实现原理
做 AI 应用绕不开 RAG,做 RAG 绕不开 Embedding 和 VectorStore。这两者是 RAG 的数据基石——Embedding 负责"理解"语义,VectorStore 负责"记忆"内容。
EmbeddingModel 接口
三个重载方法层层递进:从单文本到单文档到批量请求。实际项目中大部分时候用第三个,批量效率高。
public interface EmbeddingModel {
List<Double> embed(String text);
default List<Double> embed(Document document) {
return embed(document.getContent());
}
EmbeddingResponse embed(EmbeddingRequest request);
}
OpenAiEmbeddingModel 核心实现
public class OpenAiEmbeddingModel implements EmbeddingModel {
private final OpenAiApi openAiApi;
private final String model;
private final int batchSize;
public OpenAiEmbeddingModel(OpenAiApi openAiApi, OpenAiEmbeddingOptions options) {
this.openAiApi = openAiApi;
this.model = options.getModel() != null
? options.getModel() : "text-embedding-3-small";
this.batchSize = options.getBatchSize() != null
? options.getBatchSize() : DEFAULT_BATCH_SIZE;
}
@Override
public EmbeddingResponse embed(EmbeddingRequest request) {
List<List<String>> batches = batchTexts(request.getTexts(), this.batchSize);
List<Embedding> allEmbeddings = new ArrayList<>();
for (List<String> batch : batches) {
EmbeddingResponse response = doEmbedBatch(batch);
allEmbeddings.addAll(response.getEmbeddings());
}
return new EmbeddingResponse(allEmbeddings);
}
}
关键参数 batchSize——控制每次 API 调用发送多少文本。
| batchSize | 每次耗时 | 1000 段文本总耗时 | 失败率 |
|---|---|---|---|
| 1 | ~300ms | ~5min | <0.1% |
| 10 | ~500ms | ~50s | 0.5% |
| 20 | ~800ms | ~40s | 1% |
| 50 | ~2s | ~40s | 5% |
| 100 | ~4s | ~40s | 15% |
默认值是 20 左右,从稳定性角度这个值选得挺合理。如果你在批处理大量文档,可以适当调大到 30-50,但要做好重试的兜底,因为 batch 越大越容易超时。
模型默认用的是 text-embedding-3-small。这个模型性价比很高,1536 维向量足够大部分场景用。如果对精度要求特别高,可以切到 text-embedding-3-large(3072 维),但向量存储和检索的成本也会翻倍。
VectorStore 接口
public interface VectorStore {
void add(List<Document> documents);
void delete(List<String> idList);
List<Document> similaritySearch(SearchRequest searchRequest);
default List<Document> similaritySearch(String query) {
return similaritySearch(SearchRequest.query(query));
}
}
}
四个核心方法:增、删、查、快捷查。Spring AI 提供了多种实现:
- **PgvectorStore**(PostgreSQL + pgvector 扩展)
- **RedisVectorStore**(Redis + RedisSearch 模块)
- **ElasticsearchVectorStore**(Elasticsearch)
- **ChromaVectorStore**(Chroma)
- **MilvusVectorStore**(Milvus)
- **SimpleVectorStore**(本地文件,测试用)
我三个主流的都用过:
| 实现 | 优势 | 劣势 | 适用场景 |
|------|------|------|---------|
| **PgvectorStore** | 数据不丢失,事务支持,与业务数据同库 | 查询性能受限于 PostgreSQL | 中小规模生产环境 |
| **RedisVectorStore** | 查询速度极快 | 数据可能丢失,需要额外持久化方案 | 缓存、高频检索 |
| **SimpleVectorStore** | 零依赖、测试方便 | 重启后数据丢失 | 开发测试 |
如果你的项目已经在用 PostgreSQL,**无脑选 PgvectorStore**。少维护一套中间件,省心很多。
## PgvectorStore 源码分析
### 文档入库
```java
@Override
public void add(List<Document> documents) {
List<String> texts = documents.stream()
.map(Document::getContent)
.collect(Collectors.toList());
EmbeddingResponse embeddingResponse =
embeddingModel.embed(new EmbeddingRequest(texts, ""));
List<Embedding> embeddings = embeddingResponse.getEmbeddings();
String sql = """
INSERT INTO vector_store (id, content, metadata, embedding)
VALUES (?, ?, ?::jsonb, ?::vector)
ON CONFLICT (id) DO UPDATE SET
content = EXCLUDED.content,
metadata = EXCLUDED.metadata,
embedding = EXCLUDED.embedding
""";
// batchUpdate ...
}
add() 方法做了两件事:先调 Embedding 生成向量,再写入数据库。你传入的 Document 列表,内部被批量 Embedding 后,每条对应一行记录。
注意 ON CONFLICT 的 upsert 语义——如果 Document 的 id 已存在,就覆盖。这个设计我在做增量更新时用到过:从数据库里读一批数据做 Embedding,如果文档内容变了,覆盖旧的向量就行,不需要先删再插。
相似度检索
@Override
public List<Document> similaritySearch(SearchRequest searchRequest) {
List<Double> queryVector = embeddingModel.embed(searchRequest.getQuery());
String sql = """
SELECT id, content, metadata,
1 - (embedding <=> ?::vector) AS similarity
FROM vector_store
WHERE 1 - (embedding <=> ?::vector) >= ?
ORDER BY similarity DESC
LIMIT ?
""";
return jdbcTemplate.query(sql, new DocumentRowMapper(),
vectorStr, vectorStr, threshold, topK);
}
<=> 是 pgvector 的余弦距离运算符。1 - 距离 转成相似度,值范围 [0, 1],越接近 1 表示越相似。
实际使用中调参经验:
topK(返回条数):默认通常是 4,但真正有效的往往只有前 1-2 条。如果对召回率要求高,设到 10-20 都行。
similarityThreshold(相似度阈值):这个参数很敏感。设太低了(如 0.5)会返回一堆不相关的文档;设太高了(如 0.9)可能一个都搜不到。我的经验:
| 场景 | 推荐阈值 |
|---|---|
| 技术文档问答(代码、API) | 0.75 - 0.85 |
| 开放域问答(百科类) | 0.65 - 0.75 |
| 闲聊、泛对话 | 0.5 - 0.6 |
| 刚开始调试不确定 | 先设 0.0 看全部结果,再逐步调高 |
索引策略:HNSW vs IVFFlat
Pgvector 支持两种索引类型,Spring AI 通过配置暴露:
public class PgvectorStoreProperties {
private IndexType indexType = IndexType.HNSW;
private int m = 16; // HNSW 的最大连接数
private int efConstruction = 64; // HNSW 的动态列表大小
private int listCount = 100; // IVFFlat 的列表数
}
HNSW 默认。选 HNSW 的理由:
| 维度 | HNSW | IVFFlat |
|---|---|---|
| 查询速度 | 更快 | 较慢 |
| 建索引时间 | 较慢 | 快 |
| 内存占用 | 较高 | 低 |
| 精度 | 高 | 受 listCount 影响 |
我的建议:生产环境用 HNSW,开发环境用 IVFFlat。HNSW 的查询速度和精度都更好,虽然建索引慢一点,但建完之后优势明显。如果你的数据量很小(几千条),甚至可以不建索引,全表扫描就够快了。
数据类型:Document
public class Document {
private String id;
private String content;
private Map<String, Object> metadata;
}
}
这个类看起来简单,但用好了 metadata 字段能省很多事。我常用的 metadata 使用模式:
**给文档打标签,检索时过滤**:
```java
Document doc = new Document("Spring AI 支持多种模型",
Map.of("category", "技术文档", "lang", "zh", "version", "2.0"));
然后在检索时可以加上过滤条件,但注意——PgvectorStore 默认的 SQL 里没有 metadata 过滤。如果你需要按 metadata 字段过滤,需要自己扩展 SQL。Spring AI 的新版本已经支持了 Filter 表达式,可以通过 SearchRequest 传入过滤条件。
Embedding 与 VectorStore 的协作
整个流程:
入库:文本 → EmbeddingModel.embed() → 向量 + 原文 → VectorStore.add()
检索:问题 → EmbeddingModel.embed() → 向量 → VectorStore.similaritySearch() → 相关文档
两边的流程是对称的——入和查都要先过 Embedding。这意味着 Embedding 模型的稳定性直接影响检索效果。如果某天你换了Embedding 模型(比如从 text-embedding-3-small 切到 large),以前入库的向量和新生成的向量不兼容,需要重新入库。
Embedding 模型一旦确定,别轻易换。换了就全量重新入库。
更多推荐

所有评论(0)