一、先打个比方:给知识库里的每篇文章发“空间坐标”
想象你有一整个图书馆的书,现在要给每本书分配一个“空间坐标”。语义相近的书,坐标就挨在一起;语义无关的书,坐标就隔得很远。比如“养猫指南”和“养狗心得”的坐标几乎挨着,但它们离“汽车维修手册”就很远。
当读者问“怎么照顾宠物”时,系统先把这个问题也翻译成同一个空间里的坐标,然后直接找“离它最近”的那几本书——这就是向量检索的本质。向量转换负责“发坐标”,向量存储负责“记坐标”,向量检索负责“找邻居”。
二、三个核心角色: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 就是那个帮你“分餐”的管家:
你可以自定义这个策略:
@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);
}
}
这段代码背后发生了什么?
九、避坑指南:血泪教训
| 盲目设置 dimensions | 维度不匹配,写入直接报错 | 不确定时不要硬编码,让 Spring AI 从 EmbeddingModel 自动推断 |
| 多个 EmbeddingModel Bean | 自动注入冲突,启动报错 | 手动配置 PgVectorStore,明确指定用哪个 Model,并排除自动配置 |
| 读写使用不同 EmbeddingModel | 坐标系不一致,搜出无关结果 | 写入和检索必须是同一个模型实例 |
| 一次性嵌入过多文档 | 超过 token 上限,直接报错 | 使用 BatchingStrategy 自动分批 |
| 云服务 batch 限制 | 某些云厂商(如阿里云 DashScope)限制单次 batch 不超过 10 | 手动控制批次大小或 for 循环分批插入 |
向量转换和存储并不是什么黑科技。剥去所有术语的外壳,核心就是三句话:
- 向量转换 = 把文字翻译成高维坐标(float[]),语义相近的句子坐标点挨得近。
- 向量存储 = 把这些坐标连同原文和元数据一起存进数据库。
- 向量检索 = 把用户问题也翻译成坐标,在库里找距离最近的几个邻居。
Spring AI 的价值在于,它用 VectorStore 和 EmbeddingModel 两层抽象,把这套复杂流程封装成了几行代码。你只要选对嵌入模型、配好批处理策略、调好 SearchRequest 的四个参数,就能搭建出一个高效准确的 RAG 知识库。




