3. Embedding 与向量存储

4 min

项目骨架搭好了,ChatClientEmbeddingModel 也验证通过。接下来展开讲 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

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

输出类似:

text
模型向量维度: 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 系统中的文档块,它是向量存储的基本单元。

java
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 即可:

java
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

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、阈值和元数据过滤

接下来学习如何从真实文件中读取和切分文档,构建完整的文档摄入管道。