欢迎光临
我们一直在努力

Spring AI RAG 核心机制: 向量转换与存储【大白话详解】

一、先打个比方:给知识库里的每篇文章发“空间坐标”

想象你有一整个图书馆的书,现在要给每本书分配一个“空间坐标”。语义相近的书,坐标就挨在一起;语义无关的书,坐标就隔得很远。比如“养猫指南”和“养狗心得”的坐标几乎挨着,但它们离“汽车维修手册”就很远。

当读者问“怎么照顾宠物”时,系统先把这个问题也翻译成同一个空间里的坐标,然后直接找“离它最近”的那几本书——这就是向量检索的本质。向量转换负责“发坐标”,向量存储负责“记坐标”,向量检索负责“找邻居”。


二、三个核心角色:Document、EmbeddingModel、VectorStore

Spring AI 把整个流程抽象成了三个角色,你只要搞懂它们,RAG 的骨架就立住了。

2.1 Document:带档案袋的包裹

Document 不是简单的字符串,而是一个带标签的包裹。它里面有两样东西:

  • content:正文,后续会被转成向量。
  • metadata:键值对形式的元数据。除了做备注,它更重要的作用是检索时的过滤条件——比如只查 category == 'tech' AND year >= 2025 的文档,先把无关内容筛掉。

Document doc = new Document(
"Spring AI 是一个简化 AI 应用开发的框架…",
Map.of("source", "官方文档", "category", "tech", "year", "2025")
);

2.2 EmbeddingModel:从“人话”到“机语”的翻译官

计算机看不懂“什么是 Spring AI”,它只认数字。EmbeddingModel 就是翻译官,把人类语言翻译成高维空间里的一个坐标点(一个 float[] 数组)。

  • “猫” → [0.12, 0.88, 0.03, …]
  • “狗” → [0.11, 0.91, 0.04, …](跟猫挨得很近)
  • “汽车” → [0.95, 0.02, 0.78, …](离猫狗很远)

这里有一个铁律:写入文档和查询问题时,必须使用同一个 EmbeddingModel!如果写入时用 OpenAI 的模型,查询时换了个国产模型,两者的坐标系对不上,搜出来的结果就全废了。
在这里插入图片描述

2.3 VectorStore:统一的操作入口

Spring AI 的设计哲学是“屏蔽底层差异”。不管你背后用 PGvector、Redis、Milvus 还是 Pinecone,代码写法都一样,都通过 VectorStore 接口操作:

/**
* 向量存储的统一操作入口,屏蔽底层向量数据库(PGvector、Redis、Milvus、Pinecone 等)的差异。
* 同时继承 {@link DocumentWriter}(可写)和 {@link VectorStoreRetriever}(可读),
* 提供完整的增删查能力。
*/

public interface VectorStore extends DocumentWriter, VectorStoreRetriever {

/**
* 获取当前向量存储实例的名称,默认返回实现类的简单类名。
* 可用于日志输出、多数据源场景下区分不同存储实例。
*
* @return 向量存储名称
*/

default String getName() {
return this.getClass().getSimpleName();
}

/**
* 批量添加文档到向量存储。
* 内部会自动完成:分批(BatchingStrategy) → 向量转换(EmbeddingModel) → 持久化写入。
*
* @param documents 待写入的文档列表,每个文档包含正文(content)和元数据(metadata)
*/

void add(List<Document> documents);

/**
* 根据文档 ID 列表批量删除文档。
*
* @param idList 待删除的文档 ID 集合
*/

void delete(List<String> idList);

/**
* 根据过滤条件删除匹配的文档。
* 过滤表达式会先做元数据层面的结构化筛选,再对命中的文档执行删除。
*
* @param filterExpression 过滤条件表达式,例如 {@code "category == 'tech' AND year < 2025"}
*/

void delete(Filter.Expression filterExpression);

/**
* 根据过滤条件字符串删除匹配的文档(便捷重载)。
* 语法类似 SQL 的 WHERE,但只作用于 metadata 字段。
*
* @param filterExpression 过滤条件字符串,例如 {@code "category == 'tech' AND year < 2025"}
*/

default void delete(String filterExpression) {
// … 内部将字符串解析为 Filter.Expression 后调用 delete(Filter.Expression)
}

/**
* 获取底层向量数据库的原生客户端(如 PGvector 的 JdbcTemplate、Redis 的 Redisson 等)。
* 当你需要绕过 Spring AI 抽象直接操作底层数据库时使用,绝大部分场景不需要调用此方法。
*
* @param <T> 原生客户端类型
* @return 原生客户端的 Optional 包装,若实现类不支持则返回 {@code Optional.empty()}
*/

default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
}

另外还有一个 VectorStoreRetriever 只读接口,只提供 similaritySearch。如果你的服务只负责查资料,不需要给它“删库”的权限,注入只读接口即可——这就是最小权限原则。

@FunctionalInterface
public interface VectorStoreRetriever {

List<Document> similaritySearch(SearchRequest request);

default List<Document> similaritySearch(String query) {
return this.similaritySearch(SearchRequest.builder().query(query).build());
}
}


三、写入流程:一段文字是怎么变成库里的一行记录的?

当你调用 vectorStore.add(documents) 时,Spring AI 内部默默完成了一整套流水线:

#mermaid-svg-rhGgyo4i2e2zM7vd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rhGgyo4i2e2zM7vd .error-icon{fill:#552222;}#mermaid-svg-rhGgyo4i2e2zM7vd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rhGgyo4i2e2zM7vd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rhGgyo4i2e2zM7vd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rhGgyo4i2e2zM7vd .marker.cross{stroke:#333333;}#mermaid-svg-rhGgyo4i2e2zM7vd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rhGgyo4i2e2zM7vd p{margin:0;}#mermaid-svg-rhGgyo4i2e2zM7vd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd .cluster-label text{fill:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd .cluster-label span{color:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd .cluster-label span p{background-color:transparent;}#mermaid-svg-rhGgyo4i2e2zM7vd .label text,#mermaid-svg-rhGgyo4i2e2zM7vd span{fill:#333;color:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd .node rect,#mermaid-svg-rhGgyo4i2e2zM7vd .node circle,#mermaid-svg-rhGgyo4i2e2zM7vd .node ellipse,#mermaid-svg-rhGgyo4i2e2zM7vd .node polygon,#mermaid-svg-rhGgyo4i2e2zM7vd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rhGgyo4i2e2zM7vd .rough-node .label text,#mermaid-svg-rhGgyo4i2e2zM7vd .node .label text,#mermaid-svg-rhGgyo4i2e2zM7vd .image-shape .label,#mermaid-svg-rhGgyo4i2e2zM7vd .icon-shape .label{text-anchor:middle;}#mermaid-svg-rhGgyo4i2e2zM7vd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rhGgyo4i2e2zM7vd .rough-node .label,#mermaid-svg-rhGgyo4i2e2zM7vd .node .label,#mermaid-svg-rhGgyo4i2e2zM7vd .image-shape .label,#mermaid-svg-rhGgyo4i2e2zM7vd .icon-shape .label{text-align:center;}#mermaid-svg-rhGgyo4i2e2zM7vd .node.clickable{cursor:pointer;}#mermaid-svg-rhGgyo4i2e2zM7vd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rhGgyo4i2e2zM7vd .arrowheadPath{fill:#333333;}#mermaid-svg-rhGgyo4i2e2zM7vd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rhGgyo4i2e2zM7vd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rhGgyo4i2e2zM7vd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rhGgyo4i2e2zM7vd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rhGgyo4i2e2zM7vd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rhGgyo4i2e2zM7vd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rhGgyo4i2e2zM7vd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rhGgyo4i2e2zM7vd .cluster text{fill:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd .cluster span{color:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rhGgyo4i2e2zM7vd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rhGgyo4i2e2zM7vd rect.text{fill:none;stroke-width:0;}#mermaid-svg-rhGgyo4i2e2zM7vd .icon-shape,#mermaid-svg-rhGgyo4i2e2zM7vd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rhGgyo4i2e2zM7vd .icon-shape p,#mermaid-svg-rhGgyo4i2e2zM7vd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rhGgyo4i2e2zM7vd .icon-shape .label rect,#mermaid-svg-rhGgyo4i2e2zM7vd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rhGgyo4i2e2zM7vd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rhGgyo4i2e2zM7vd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rhGgyo4i2e2zM7vd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

第 1 批

第 2 批

第 N 批

原始文档(PDF / Markdown / 文本)

Document 对象(content + metadata)

BatchingStrategyTokenCountBatchingStrategy按 token 数拆分批次

EmbeddingModel调用嵌入 API文本 → float[] 向量

VectorStore.add()<写入向量数据库

数据库表id | content | metadata | embedding(PGvector / Redis / Milvus)

3.1 第一步:分批——不让嵌入模型“吃撑”

嵌入模型一次能处理的文本量有上限(比如 OpenAI 的嵌入模型默认上限 8191 token)。如果你一次性塞给它 100 篇长文,它会直接报错,就像你一次吞不下整桌菜。

Spring AI 的 TokenCountBatchingStrategy 就是那个帮你“分餐”的管家:

  • 用 JTokkitTokenCountEstimator 估算每篇文档的 token 数。
  • 像装箱子一样,把文档依次放进批次篮子,装满了就换个新的。
  • 默认留 10% 的缓冲空间:实际上限 ≈ 8191 × 0.9 ≈ 7371 token。
  • 如果单个文档就超过了上限,直接抛出异常,因为它拆无可拆。
  • 你可以自定义这个策略:

    @Bean
    public BatchingStrategy batchingStrategy() {
    return new TokenCountBatchingStrategy(
    EncodingType.CL100K_BASE, // 编码方式,用于准确估算 token
    8000, // 最大 token 上限
    0.1 // 保留 10% 缓冲
    );
    }

    3.2 第二步:向量转换——真正的“翻译”

    每个批次被送到 EmbeddingModel,模型调用远程 API(如 OpenAI 的 text-embedding-ada-002),把每段文本转成一个 float[] 数组。这个数组就是文档的“数字指纹”。例如 text-embedding-ada-002 输出的向量维度是 1536,意味着每段文字被映射到了 1536 维空间中的一个点。

    3.3 第三步:存储落地——一切入库

    向量、原文、元数据被打包存入具体数据库。以 PGvector 为例,建表 SQL 大概长这样:

    CREATE TABLE IF NOT EXISTS vector_store (
    id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
    content text, — 原文
    metadata json, — 元数据(JSON 格式)
    embedding vector(1536) — 向量,1536 维
    );

    CREATE INDEX ON vector_store
    USING HNSW (embedding vector_cosine_ops);

    这里有三个关键点:

    关键点说明
    vector(1536) PGvector 专有的数据类型,维度必须和 EmbeddingModel 的输出严格一致。
    HNSW 索引 全称 Hierarchical Navigable Small World,一种近似最近邻索引。它构建多层图结构,查询时像“走迷宫找出口”一样沿图跳转,不用穷举所有向量,速度极快。
    vector_cosine_ops 指定用余弦距离作为相似度度量方式。

    四、检索流程:用户提问后,系统内部发生了什么?

    当用户在聊天框里输入“Spring AI 怎么接入 RAG”后,背后的检索链路是这样的:

    #mermaid-svg-GMfY4CJEvUAqfK49{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GMfY4CJEvUAqfK49 .error-icon{fill:#552222;}#mermaid-svg-GMfY4CJEvUAqfK49 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GMfY4CJEvUAqfK49 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GMfY4CJEvUAqfK49 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GMfY4CJEvUAqfK49 .marker.cross{stroke:#333333;}#mermaid-svg-GMfY4CJEvUAqfK49 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GMfY4CJEvUAqfK49 p{margin:0;}#mermaid-svg-GMfY4CJEvUAqfK49 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 .cluster-label text{fill:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 .cluster-label span{color:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 .cluster-label span p{background-color:transparent;}#mermaid-svg-GMfY4CJEvUAqfK49 .label text,#mermaid-svg-GMfY4CJEvUAqfK49 span{fill:#333;color:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 .node rect,#mermaid-svg-GMfY4CJEvUAqfK49 .node circle,#mermaid-svg-GMfY4CJEvUAqfK49 .node ellipse,#mermaid-svg-GMfY4CJEvUAqfK49 .node polygon,#mermaid-svg-GMfY4CJEvUAqfK49 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GMfY4CJEvUAqfK49 .rough-node .label text,#mermaid-svg-GMfY4CJEvUAqfK49 .node .label text,#mermaid-svg-GMfY4CJEvUAqfK49 .image-shape .label,#mermaid-svg-GMfY4CJEvUAqfK49 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GMfY4CJEvUAqfK49 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GMfY4CJEvUAqfK49 .rough-node .label,#mermaid-svg-GMfY4CJEvUAqfK49 .node .label,#mermaid-svg-GMfY4CJEvUAqfK49 .image-shape .label,#mermaid-svg-GMfY4CJEvUAqfK49 .icon-shape .label{text-align:center;}#mermaid-svg-GMfY4CJEvUAqfK49 .node.clickable{cursor:pointer;}#mermaid-svg-GMfY4CJEvUAqfK49 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GMfY4CJEvUAqfK49 .arrowheadPath{fill:#333333;}#mermaid-svg-GMfY4CJEvUAqfK49 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GMfY4CJEvUAqfK49 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GMfY4CJEvUAqfK49 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GMfY4CJEvUAqfK49 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GMfY4CJEvUAqfK49 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GMfY4CJEvUAqfK49 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GMfY4CJEvUAqfK49 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GMfY4CJEvUAqfK49 .cluster text{fill:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 .cluster span{color:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GMfY4CJEvUAqfK49 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GMfY4CJEvUAqfK49 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GMfY4CJEvUAqfK49 .icon-shape,#mermaid-svg-GMfY4CJEvUAqfK49 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GMfY4CJEvUAqfK49 .icon-shape p,#mermaid-svg-GMfY4CJEvUAqfK49 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GMfY4CJEvUAqfK49 .icon-shape .label rect,#mermaid-svg-GMfY4CJEvUAqfK49 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GMfY4CJEvUAqfK49 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GMfY4CJEvUAqfK49 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GMfY4CJEvUAqfK49 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    用户提问'Spring AI 怎么接入 RAG?'

    EmbeddingModel将查询文本向量化→ float[] 查询向量

    SearchRequest构建检索条件query + topK + threshold + filter

    VectorStore.similaritySearch()

    元数据过滤Filter Expression先筛 category/时间/来源

    相似度计算余弦相似度 / 欧氏距离 / 点积查询向量 vs 候选向量

    阈值过滤 + TopK 排序similarityThreshold 砍掉低分按相似度降序取前 N 个

    返回 Document 列表(content + metadata + similarity score)

    拼接为上下文发给大模型生成回答

    4.1 查询向量化——跟写入时用同一把“尺子”

    用户的问题不是直接去数据库里做模糊查询,而是先经过同一个 EmbeddingModel 转成向量。这样才能保证查询向量和文档向量处于同一个坐标系里。

    4.2 相似度计算——三种“量尺”

    向量数据库通常支持三种相似度度量方式:

    方法大白话解释数值范围
    余弦相似度 看两个箭头指的方向有多接近,不看长度。夹角越小越相似。 -1 到 1(1 表示完全同向)
    欧氏距离 两点之间的直线距离,距离越短越相似。 0 到无穷
    点积(内积) 两个向量对应位置相乘再累加,值越大越相似。 取决于向量长度

    PGvector 默认使用 COSINE_DISTANCE(余弦距离)。如果你的向量已经归一化到长度 1,用内积性能最佳。

    4.3 过滤 + 排序——先缩小范围,再挑最好的

    检索时,系统通常会先在元数据层面做结构化筛选,再对筛选后的子集做向量相似度计算。比如先过滤出 category == 'tech' AND year >= 2025 的文档,再在这几百篇里找语义最接近的。这样既省计算又提升精度,是 RAG 优化的关键手段。


    五、SearchRequest:控制检索质量的四个旋钮

    similaritySearch 不是简单传个字符串,而是传一个 SearchRequest 对象。它的四个参数直接决定了召回质量:

    SearchRequest request = SearchRequest.builder()
    .query("Spring AI 怎么接入 RAG") // 查询文本
    .topK(5) // 最多返回 5 个(默认 4)
    .similarityThreshold(0.75) // 低于 0.75 的直接丢弃
    .filterExpression("category == 'tech' AND year >= 2025") // 只查符合条件的
    .build();

    List<Document> results = vectorStore.similaritySearch(request);

    参数默认值怎么调
    query "" 用户问题原文
    topK 4 想要更多上下文就调大(如 10),想要更精准就调小(如 3)
    similarityThreshold 0.0(不过滤) 召回太少、太严格 → 降低到 0.5;召回太多、混入无关内容 → 提高到 0.8
    filterExpression null 语法类似 SQL 的 WHERE,但只过滤 metadata 字段。例如 "author in ['john', 'jill'] && article_type == 'blog'"

    六、元数据过滤:在向量搜索之前“先筛一遍”

    元数据过滤支持两种写法。字符串表达式(像 SQL):

    .filterExpression("author in ['john', 'jill'] && article_type == 'blog'")

    DSL 构建器(类型安全,推荐):

    FilterExpressionBuilder b = new FilterExpressionBuilder();
    Filter.Expression exp = b.and(
    b.in("author", "john", "jill"),
    b.eq("article_type", "blog")
    ).build();

    在 PGvector 中,这些过滤表达式会被转成 PostgreSQL 的 JSON path 查询,在数据库层面直接执行,效率很高。


    七、PGvector 配置避坑:dimensions 不能乱写

    PGvector 是企业场景下最受欢迎的方案之一——大多数公司本来就有 PostgreSQL,装个扩展就能支持向量检索,零额外运维成本。

    但在配置时有一个最常见的坑:

    ⚠️ 在不确定向量维度的情况下,一定不要指定 dimensions 配置!

    如果你用的嵌入模型输出 1536 维,但你配置里写死了 dimensions: 768,建表时 embedding 列就会被设为 vector(768)。当模型返回 1536 维的向量时——直接报错。更糟糕的是,改 dimensions 需要重建整个表。

    正确做法:不指定 dimensions,让 PgVectorStore 从 EmbeddingModel 自动推断维度。

    手动配置示例:

    @Bean
    public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
    return PgVectorStore.builder(jdbcTemplate, embeddingModel)
    .distanceType(COSINE_DISTANCE) // 余弦距离
    .indexType(HNSW) // HNSW 索引
    .initializeSchema(true) // 自动建表
    .schemaName("public")
    .vectorTableName("vector_store")
    .maxDocumentBatchSize(10000)
    .build();
    }


    八、完整实战:从写入到检索的串联代码

    最后,把所有环节串起来,一段完整可运行的示例:

    @Configuration
    public class RagVectorConfig {

    // 1. 批处理策略:防止超过嵌入模型 token 上限
    @Bean
    public BatchingStrategy batchingStrategy() {
    return new TokenCountBatchingStrategy(
    EncodingType.CL100K_BASE,
    8000,
    0.1
    );
    }

    // 2. 手动配置 PGvector(避免自动注入冲突)
    @Bean
    public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
    return PgVectorStore.builder(jdbcTemplate, embeddingModel)
    .distanceType(COSINE_DISTANCE)
    .indexType(HNSW)
    .initializeSchema(true)
    .maxDocumentBatchSize(10000)
    .build();
    }

    // 3. 写入文档(内部自动完成分批 → 嵌入 → 存储)
    public void indexDocuments(VectorStore vectorStore) {
    List<Document> docs = List.of(
    new Document("Spring AI 支持 RAG 架构,通过 VectorStore 接口…",
    Map.of("category", "tech", "year", "2025")),
    new Document("Spring Boot 3.0 引入了 AOT 编译支持…",
    Map.of("category", "tech", "year", "2024"))
    );
    vectorStore.add(docs);
    }

    // 4. 检索文档
    public List<Document> search(VectorStore vectorStore, String question) {
    SearchRequest request = SearchRequest.builder()
    .query(question)
    .topK(5)
    .similarityThreshold(0.7)
    .filterExpression("category == 'tech' AND year >= 2025")
    .build();
    return vectorStore.similaritySearch(request);
    }
    }

    这段代码背后发生了什么?

  • add(docs) → BatchingStrategy 先按 token 数分批。
  • 每批交给 EmbeddingModel 转成 float[] 向量数组。
  • PgVectorStore 把向量、原文、元数据写入 PostgreSQL 的 vector_store 表。
  • similaritySearch → 用户问题先向量化 → 数据库用 HNSW 索引做近似最近邻搜索 → 同时按 category 和 year 过滤 → 返回相似度最高的 5 个文档。

  • 九、避坑指南:血泪教训

    坑后果解决方案
    盲目设置 dimensions 维度不匹配,写入直接报错 不确定时不要硬编码,让 Spring AI 从 EmbeddingModel 自动推断
    多个 EmbeddingModel Bean 自动注入冲突,启动报错 手动配置 PgVectorStore,明确指定用哪个 Model,并排除自动配置
    读写使用不同 EmbeddingModel 坐标系不一致,搜出无关结果 写入和检索必须是同一个模型实例
    一次性嵌入过多文档 超过 token 上限,直接报错 使用 BatchingStrategy 自动分批
    云服务 batch 限制 某些云厂商(如阿里云 DashScope)限制单次 batch 不超过 10 手动控制批次大小或 for 循环分批插入

    向量转换和存储并不是什么黑科技。剥去所有术语的外壳,核心就是三句话:

    • 向量转换 = 把文字翻译成高维坐标(float[]),语义相近的句子坐标点挨得近。
    • 向量存储 = 把这些坐标连同原文和元数据一起存进数据库。
    • 向量检索 = 把用户问题也翻译成坐标,在库里找距离最近的几个邻居。

    Spring AI 的价值在于,它用 VectorStore 和 EmbeddingModel 两层抽象,把这套复杂流程封装成了几行代码。你只要选对嵌入模型、配好批处理策略、调好 SearchRequest 的四个参数,就能搭建出一个高效准确的 RAG 知识库。
    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Spring AI RAG 核心机制: 向量转换与存储【大白话详解】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址