项目骨架搭好了,ChatClient 和 EmbeddingModel 也验证通过。接下来展开讲 Embedding 和向量存储,这些是 RAG 系统的数据基础,理解它们才能搞懂后续的检索流程。
1. 什么是 Embedding ?
Embedding(嵌入)是一种将文本转换为固定长度浮点数向量的技术。每个向量可以理解为文本在 "语义空间" 中的 "坐标",语义相近的文本,向量坐标也相近。
核心要点:
- 输入:任意长度的文本字符串
- 输出:固定长度的浮点数数组(如 2048 维、4096 维)
- 特性:
语义相似的文本 => 向量距离近;语义不同的文本 => 向量距离远 - 用途:RAG 中的相似度检索、文本分类、聚类分析等
2. EmbeddingModel API
Spring AI 通过 EmbeddingModel 接口抽象了所有 Embedding 模型的调用。前面已经验证过它能正常连接,这里先看完整 API,再用一个测试类逐一验证。
| 方法 | 参数 | 返回类型 | 说明 |
|---|---|---|---|
embed(String) |
文本字符串 | float[] |
嵌入单个文本,最常用 |
embed(Document) |
Document 对象 | float[] |
嵌入文档的内容字段 |
call(EmbeddingRequest) |
请求对象(含文本列表) | EmbeddingResponse |
批量嵌入,支持选项配置 |
dimensions() |
无 | int |
返回模型的向量维度 |
下面用一个测试类把这些 API 全部跑一遍,同时验证语义相似度的效果:
src/test/java/com/albertstack/rag/EmbeddingModelTest.java
package com.albertstack.rag;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
class EmbeddingModelTest {
@Autowired
EmbeddingModel embeddingModel;
@Test
void allEmbeddingApis() {
// ========== 1. dimensions() — 获取模型向量维度 ==========
int dims = embeddingModel.dimensions();
log.info("模型向量维度: {}", dims);
assertThat(dims).isEqualTo(4096); // qwen3-embedding:8b 输出 4096 维
// ========== 2. embed(String) — 嵌入单个文本 ==========
float[] vector = embeddingModel.embed("Spring AI 是什么?");
log.info("embed(String) 返回维度: {}", vector.length);
assertThat(vector).hasSize(dims);
// ========== 3. embed(Document) — 嵌入 Document 对象 ==========
var doc = new Document("RAG 通过检索外部文档增强 LLM 的回答",
Map.of("source", "tutorial"));
float[] docVector = embeddingModel.embed(doc);
log.info("embed(Document) 返回维度: {}", docVector.length);
assertThat(docVector).hasSize(dims);
// ========== 4. call(EmbeddingRequest) — 批量嵌入 ==========
EmbeddingResponse response = embeddingModel.call(
new EmbeddingRequest(
List.of(
"Spring AI 是 Spring 官方的 AI 集成框架",
"RAG 增强了大语言模型的回答能力",
"向量数据库用于存储和检索高维向量"
),
null // 使用默认选项
)
);
var embeddings = response.getResults();
log.info("call() 批量嵌入数量: {}", embeddings.size());
assertThat(embeddings).hasSize(3);
embeddings.forEach(e -> assertThat(e.getOutput()).hasSize(dims));
// ========== 5. 语义相似度验证 ==========
// 语义相近的两句话
float[] v1 = embeddingModel.embed("Java 是一种面向对象的编程语言");
float[] v2 = embeddingModel.embed("Java 是一门 OOP 编程语言");
// 语义不同的一句话
float[] v3 = embeddingModel.embed("今天的天气非常晴朗");
double simSimilar = cosineSimilarity(v1, v2);
double simDifferent = cosineSimilarity(v1, v3);
log.info("相近文本相似度: {}", String.format("%.4f", simSimilar));
log.info("不同文本相似度: {}", String.format("%.4f", simDifferent));
// 语义相近的文本,相似度应该更高
assertThat(simSimilar).isGreaterThan(simDifferent);
assertThat(simSimilar).isGreaterThan(0.8);
assertThat(simDifferent).isLessThan(0.6);
}
/**
* 计算两个向量的余弦相似度
*/
private double cosineSimilarity(float[] a, float[] b) {
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
}输出类似:
模型向量维度: 4096
embed(String) 返回维度: 4096
embed(Document) 返回维度: 4096
call() 批量嵌入数量: 3
相近文本相似度: 0.9078
不同文本相似度: 0.3968核心验证点:
dimensions():确认 qwen3-embedding:8b 输出 4096 维向量embed(String):最常用的方式,传入文本直接返回向量embed(Document):传入 Document 对象,自动提取text字段嵌入call(EmbeddingRequest):批量嵌入多条文本,减少网络往返- 语义相似度:语义相近的两句话相似度高达 0.90,主题无关的文本只有 0.39,这就是 RAG 检索的底层原理:用向量相似度找到最相关的文档
3. Document 对象
Spring AI 用 Document 类表示 RAG 系统中的文档块,它是向量存储的基本单元。
import org.springframework.ai.document.Document;
// 最简单的创建方式:只有内容
var doc1 = new Document("Spring AI 支持多种 AI 模型提供商");
// 带元数据的创建方式
var doc2 = new Document(
"RAG 通过检索外部文档增强 LLM 的回答",
Map.of(
"source", "rag-tutorial.md",
"category", "concept",
"author", "Albert"
)
);Document 的核心属性:
| 属性 | 类型 | 说明 |
|---|---|---|
id |
String |
唯一标识,自动生成 UUID |
text |
String |
文本内容,Embedding 和检索都基于此 |
metadata |
Map<String, Object> |
元数据,可用于过滤检索结果 |
score |
Double |
相似度得分,仅在检索结果中有值 |
3.1 元数据(Metadata)
metadata 是 Document 的重要扩展能力。检索时可以用 filterExpression 按元数据字段过滤,缩小搜索范围。metadata 有两个来源:
Reader 自动填充的字段:
| 字段 | 来源 | 说明 |
|---|---|---|
source |
TextReader | 文件名 |
charset |
TextReader | 字符编码,如 UTF-8 |
file_name |
PagePdfDocumentReader | PDF 文件名 |
page_number |
PagePdfDocumentReader | 页码(从 0 开始) |
title |
MarkdownDocumentReader | Markdown 标题层级 |
手动添加的业务字段(根据项目需要自行定义):
| 字段 | 示例值 | 用途 |
|---|---|---|
category |
"framework", "api" |
按文档分类过滤 |
author |
"Albert" |
按作者过滤 |
ingested_at |
"2026-04-07T10:00:00Z" |
记录摄入时间,排查数据新鲜度 |
4. SimpleVectorStore
SimpleVectorStore 是 Spring AI 内置的内存向量存储实现。它把所有文档和向量保存在 JVM 内存中,适合开发和测试。
创建方式很简单,传入 EmbeddingModel 即可:
var vectorStore = SimpleVectorStore.builder(embeddingModel).build();5. SearchRequest 参数
SearchRequest 控制搜索行为,以下是常用参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
query |
String |
— | 搜索文本,必填 |
topK |
int |
4 |
返回最相似的 K 个结果 |
similarityThreshold |
double |
0.0 |
最低相似度阈值,低于此值的结果会被过滤 |
filterExpression |
String |
null |
基于元数据的过滤表达式 |
过滤表达式支持的操作符:
| 操作符 | 示例 | 说明 |
|---|---|---|
== |
source == 'docs' |
等于 |
!= |
source != 'tutorial' |
不等于 |
in |
source in ['docs', 'api'] |
包含在列表中 |
nin |
source nin ['draft'] |
不在列表中 |
&& |
source == 'docs' && chapter == '01' |
逻辑与 |
|| |
source == 'docs' || source == 'api' |
逻辑或 |
6. 向量存储测试
下面用测试用例演示 SimpleVectorStore 的完整用法,覆盖添加、搜索、过滤和持久化。
src/test/java/com/albertstack/rag/VectorStoreTest.java
package com.albertstack.rag;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
class VectorStoreTest {
@Autowired
EmbeddingModel embeddingModel;
@TempDir
Path tempDir;
SimpleVectorStore vectorStore;
@BeforeEach
void setUp() {
// 创建向量存储,传入 EmbeddingModel 用于自动生成向量
vectorStore = SimpleVectorStore.builder(embeddingModel).build();
// 准备测试文档,每个文档携带元数据用于后续过滤
var docs = List.of(
new Document(
"Spring AI 是 Spring 官方推出的 AI 集成框架,提供统一 API 对接各种 AI 模型",
Map.of("source", "spring-ai-docs", "type", "introduction")
),
new Document(
"RAG 全称 Retrieval-Augmented Generation,通过检索外部知识增强大模型的回答质量",
Map.of("source", "rag-tutorial", "type", "concept")
),
new Document(
"SimpleVectorStore 是 Spring AI 内置的内存向量存储,适合开发和测试使用",
Map.of("source", "spring-ai-docs", "type", "component")
),
new Document(
"PGVector 是 PostgreSQL 的向量扩展,适合生产环境的大规模向量检索",
Map.of("source", "pgvector-docs", "type", "component")
),
new Document(
"Ollama 可以在本地运行大语言模型,支持 Llama、Qwen 等开源模型",
Map.of("source", "ollama-docs", "type", "tool")
)
);
// add() 内部自动调用 embeddingModel.embed() 生成向量,然后保存到内存
vectorStore.add(docs);
}
/**
* 基础相似度搜索:topK 控制返回数量
*/
@Test
void similaritySearch() {
// topK=3 表示返回最相似的 3 个结果
var results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("什么是 RAG?")
.topK(3)
.build()
);
// 内部流程:query 先被 embed 为向量,再与所有文档向量计算余弦相似度,按得分降序返回
for (Document doc : results) {
log.info("相似度: {} | 来源: {} | 内容: {}",
String.format("%.4f", doc.getScore()),
doc.getMetadata().get("source"),
doc.getText().substring(0, Math.min(50, doc.getText().length()))
);
}
// 关于 RAG 的文档应该排在最前面
assertThat(results).isNotEmpty();
assertThat(results.getFirst().getText()).contains("RAG");
}
/**
* similarityThreshold:过滤低相关结果
*/
@Test
void searchWithThreshold() {
// 设置 similarityThreshold=0.6,低于此相似度的结果直接丢弃
var results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("什么是 RAG?")
.topK(5)
.similarityThreshold(0.6)
.build()
);
log.info("高相似度结果数: {}", results.size());
// 所有返回结果的 score 都应 >= 0.6
results.forEach(doc ->
assertThat(doc.getScore()).isGreaterThanOrEqualTo(0.6)
);
}
/**
* filterExpression:基于元数据过滤,缩小检索范围
*/
@Test
void searchWithFilter() {
// 只在 source="spring-ai-docs" 的文档中搜索
var filtered = vectorStore.similaritySearch(
SearchRequest.builder()
.query("Spring AI")
.topK(3)
.filterExpression("source == 'spring-ai-docs'")
.build()
);
// 返回结果的 source 都是 spring-ai-docs
filtered.forEach(doc ->
assertThat(doc.getMetadata().get("source")).isEqualTo("spring-ai-docs")
);
}
/**
* 持久化:save() 保存到 JSON 文件,load() 恢复到新实例
*/
@Test
void saveAndLoad() {
File saveFile = tempDir.resolve("vector-store.json").toFile();
// 保存:将所有文档及其向量序列化为 JSON
vectorStore.save(saveFile);
assertThat(saveFile).exists();
log.info("文件大小: {} bytes", saveFile.length());
// 加载:创建新实例,从文件恢复数据
var restoredStore = SimpleVectorStore.builder(embeddingModel).build();
restoredStore.load(saveFile);
// 验证加载后的搜索结果与原始一致
var original = vectorStore.similaritySearch(
SearchRequest.builder().query("什么是 RAG?").topK(3).build()
);
var restored = restoredStore.similaritySearch(
SearchRequest.builder().query("什么是 RAG?").topK(3).build()
);
assertThat(restored).hasSameSizeAs(original);
assertThat(restored.getFirst().getText()).isEqualTo(original.getFirst().getText());
}
}7. 本章小结
掌握了 RAG 系统的数据基础,从文本到向量的转换,以及向量的存储与检索。
| 知识点 | 说明 |
|---|---|
| Embedding | 将文本转为向量,语义相近的文本向量距离也近 |
| EmbeddingModel | Spring AI 的嵌入模型抽象,支持单条和批量嵌入 |
| Document | Spring AI 的文档对象,包含内容、元数据和向量 |
| SimpleVectorStore | 内存向量存储,支持添加、搜索和持久化 |
| SearchRequest | 控制搜索行为的参数对象,支持 topK、阈值和元数据过滤 |
接下来学习如何从真实文件中读取和切分文档,构建完整的文档摄入管道。