如果本身项目是基于 SpringBoot 的,或者项目中已经引入了 ElasticSearch ,其实可以考虑直接使用 ES ,而不再引入向量数据库
ES!我的超人!
第一章|为什么 Spring 体系做 RAG,很多人第一个想到的就是向量数据库?
本章带你理解:向量数据库到底是什么,为什么大家都在谈它,以及为什么它不是唯一的选择。
假设你是一家在线教育公司的后端开发,某天产品经理跑来找你:"我们要做一个智能答疑助手,学生提问后,系统能从我们的题库和讲义里自动找到相关内容,然后用自然语言回答。"
你上网一搜------满屏都在说 RAG(检索增强生成) ,而 RAG 的标配似乎就是 向量数据库:Milvus、Qdrant、Pinecone、Weaviate......一个个名字扑面而来。
你的内心可能是这样的:
向量数据库到底干了什么?
要搞清楚这个问题,先得明白向量数据库在 RAG 里扮演的角色。
Embedding(向量化) 的本质是把一段文本映射成一个高维数字数组。比如"Spring Boot 的自动装配原理"这段话,经过 Embedding 模型处理后,可能变成一个 1024 维的浮点数数组:[0.023, -0.157, 0.891, ..., 0.044]。
语义相近的文本,在高维空间里的距离就近。这就是向量检索的基础。
语义是否相近常用的计算方法是这样计算的,基本原理是大家上学时学过的余弦相似度: <math xmlns="http://www.w3.org/1998/Math/MathML"> cosine_similarity ( A , B ) = A ⋅ B ∥ A ∥ × ∥ B ∥ = ∑ i = 1 n a i × b i ∑ i = 1 n a i 2 × ∑ i = 1 n b i 2 \text{cosine\similarity}(A, B) = \frac{A \cdot B}{\|A\| \times \|B\|} = \frac{\sum{i=1}^{n} a_i \times b_i}{\sqrt{\sum_{i=1}^{n} a_i^2} \times \sqrt{\sum_{i=1}^{n} b_i^2}} </math>cosine_similarity(A,B)=∥A∥×∥B∥A⋅B=∑i=1nai2 ×∑i=1nbi2 ∑i=1nai×bi 但也有一些其他的方法,只是余弦相似度是最常用的方法之一
其中 A 和 B 是两个向量,分子是点积,分母是两个向量的模长乘积。
简单来说:Embedding 模型把文字变成"语义指纹",向量数据库存储这些指纹并支持"找最相似的指纹"。
向量数据库 (如 Milvus、Qdrant)就是专门为这种"找最相似指纹"的场景设计的存储和检索引擎。它们的核心能力是 ANN(近似最近邻)搜索,在海量向量中快速找到与查询向量最接近的结果。
ANN 与 KNN 的区别:传统的 KNN(精确最近邻)需要遍历所有向量才能找到最相似的,计算量随数据量线性增长;
ANN 则通过预先建立索引结构(如 HNSW 图),用"跳着找"的方式近似地快速定位,在精度和速度之间做权衡。百万级数据下,ANN 可以把检索时间从 O(n) 降到 O(log n) 或更快。
主流 ANN 算法一览
| 算法 | 代表产品 | 核心思路 | 优势 | 劣势 |
|---|---|---|---|---|
| HNSW | ES、Milvus、Qdrant、Pinecone | 多层跳表图,从粗到细逐层搜索 | 速度快、精度高、参数少 | 内存占用较高 |
| IVF | FAISS、Milvus | 先聚类再搜索,只在目标簇内查找 | 内存友好、可压缩 | 需要预训练聚类中心 |
| PQ | FAISS | 向量分片后量化压缩 | 极致压缩、适合十亿级数据 | 精度损失明显 |
| HNSW + IVF | 混合方案 | 图索引 + 聚类双重加速 | 兼顾速度与内存 | 配置调优复杂 |
选型建议:中小规模(< 1000万)优先选 HNSW,简单高效;超大规模(> 1亿)可考虑 PQ 或 HNSW+IVF 混合方案。
但是------你可能不需要向量数据库
如果你的技术栈已经在用 Spring Boot + Elasticsearch,那么请先回答三个问题:
- 你的Agent需要检索的数据量是百万级以下吗?
- 你需要同时支持关键词检索和语义检索吗?
类型 说明 劣势 常用方式 关键词检索 直接精准匹配关键词,尤其是对确切的数字、术语效果很好 无法理解同义词/语义关联 BM25 语义检索 匹配"相关意思"的内容,本质是向量相似度 可能偏离字面(比如要拉格朗日中值定理,结果匹配到了柯西中值定理),计算量大 HNSW等
- 你团队的运维带宽有限吗?
如果这三个问题的答案都是"是",那么 ES 8.x 的 dense_vector 字段 + HNSW 算法,完全能胜任你的向量检索需求。你不需要额外引入一个向量数据库。
如果只有部分是"是",那你可以看完本文后进行取舍和抉择。
市场现状:根据 2024-2025 年的开发者调研和实践反馈,在中小规模 RAG 场景中(数据量 < 500 万条),ES 作为向量存储的方案已经被大量公司采用。Spring AI 官方也原生支持 Elasticsearch 作为 向量存储(VectorStore),这本身就是一种"官方背书"。
第二章|ES 做"向量数据库",为什么够用?
本章详细拆解 ES 8.x 的向量检索能力------从底层算法 HNSW,到 dense_vector 字段,再到它和专用向量数据库的对比
ES 8.x 的向量检索能力从哪来?
ES 从 7.8.0 版本开始支持 dense_vector(稠密向量,稠密指向量的大部分位置非0) 字段类型,8.x 版本更是引入了原生的 kNN 搜索 能力,底层使用的是 HNSW(Hierarchical Navigable Small World) 算法。
准确的说,引入的是 ANN 能力,并将其封装在 kNN 这个 API 中。 (注意k的大小写,KNN是算法,kNN是 API)
HNSW 是什么?
HNSW 是目前工业界最主流的 ANN 算法之一,Milvus、Qdrant 也都用它。核心思想是构建一个多层图结构:
搜索时从顶层入口 出发,逐层向下精搜,最终在底层找到最近的邻居。时间复杂度约为 O(log n) ,在百万级数据中,单次查询通常在 几十毫秒 级别。
类比理解:比如你想去一个叫"深圳"的地方旅游。你会先看中国地图(顶层,有各个省份)定位到广东省;然后看广东省地图(中层,有各个城市)定位到深圳市;最后看深圳地图(底层,精确搜索)走到具体景点,到了发现深圳的景点只有商场(bushi)。HNSW 就是这个思路。
ES 里的 dense_vector 字段
在 ES 中,你只需要在索引 mapping 里定义一个 dense_vector 字段,指定维度即可:
json
PUT /knowledge_base
{
"mappings": {
"properties": {
"textContent": { "type": "text" },
"vector": {
"type": "dense_vector",
"dims": 1024,
"index": true,
"similarity": "cosine"
}
}
}
}
dims:向量的维度,取决于你用的 Embedding 模型输出维度index: true:开启 HNSW 索引(8.x 默认开启)similarity:相似度算法,支持cosine(余弦)、l2_norm(欧氏距离)、dot_product(内积)
选哪种相似度? 文本语义检索场景最常用的是 cosine(余弦相似度) ,因为它对向量长度不敏感,只关注方向,更适合文本语义的度量。
l2_norm更适合图像向量检索,dot_product在模型训练时已归一化的情况下等价于 cosine 但更快。
ES vs 专用向量数据库
| 对比维度 | Elasticsearch 8.x | Milvus | Qdrant |
|---|---|---|---|
| 核心定位 | 全能搜索引擎(文本+向量) | 专用向量数据库 | 轻量级向量数据库 |
| 文本检索 | ✅ BM25 原生支持 | ❌ 需外部组件 | ❌ 需外部组件 |
| 向量检索 | ✅ HNSW | ✅ HNSW/IVF/多种 | ✅ HNSW |
| 混合检索 | ✅ 一套索引内完成 | ❌ 需两套系统拼接 | ⚠️ 支持 Dense+Sparse |
| 数据量上限 | 百万~千万级 | 百亿级 | 亿级 |
| 运维复杂度 | 低(大部分团队已有ES) | 高(独立集群+依赖) | 中(单进程/Docker) |
| Spring AI 支持 | ✅ 原生 VectorStore | ✅ 原生 VectorStore | ✅ 原生 VectorStore |
| 学习成本 | 低(沿用ES知识) | 高(新概念+新API) | 中 |
专用向量数据库可能需要两套系统拼接,这也是选型的重要因考虑因素一。像 Milvus 这种向量数据库不支持关键字检索,就需要拼接,如果你本就不熟悉向量数据库,还要在这基础上进行拼接,更是难上加难。
关键结论 :如果你的数据量在百万~千万级,且需要混合检索,ES 是最优选。只有在数据量超过亿级,或者向量检索是系统唯一核心功能时,才需要考虑 Milvus 这类专业向量数据库。
第三章|ES 在 RAG 的角色
本章从"为什么大模型需要外挂知识库"讲起,完整梳理 RAG 的工作流程,并标出每一步 ES 能做什么。
为什么大模型"知识不够用"?
大模型(如 GPT、DeepSeek、千问)有两个硬伤:
- 训练数据有截止日期------它不知道昨天发生的事
- 它不知道你公司的内部知识------你的业务文档、产品手册、历史工单,模型从未见过
💬 举个不恰当但直观的例子:大模型就像一个读了万卷书但从未出过门的学者。你问他"唐朝的税制",他能滔滔不绝;但你问他"我们公司上个月的退款政策改了什么",他就一脸懵。
RAG(Retrieval Augmented Generation,检索增强生成) 就是解决这个问题的方案:先检索,再生成。把相关知识从外部知识库中找出来,喂给大模型,让它基于真实资料回答。
RAG 的完整工作流
在这个流程中,ES 可以覆盖以下环节:
| RAG 环节 | ES 能做什么 |
|---|---|
| 文档存储 | 原文 + 元数据一起存 |
| 向量索引 | dense_vector 字段 + HNSW |
| 向量检索 | _knn_search 原生接口 |
| 关键词检索 | BM25,ES 的看家本领 |
| 混合检索 | KNN + BM25 rescore,一套查询搞定 |
| 权限过滤 | bool + filter,按用户/组织隔离数据 |
🏆 业界最佳实践 :目前大量企业级 RAG 方案(包括阿里云的企业知识库、字节内部的智能客服等)都采用了 "ES 作为统一存储 + 混合检索" 的架构,而不是"Milvus 做向量 + ES 做文本"的双系统方案。双系统的痛点在于数据同步------写入时要同时写两个库,更新时双删双改,运维成本直接翻倍。
第四章|ES 混合检索:BM25 + KNN,这才是正确打开方式
本章是全文的核心------详细讲解为什么纯向量检索不够好,以及如何用 ES 的 rescore 机制实现"KNN 粗召回 + BM25 精排"的混合检索策略。
纯向量检索的"翻车现场"
纯 KNN 向量检索有一个典型问题:语义相近但答案错位。
假设你的知识库里有这么一段话
竖屏模式采用上下分栏布局(题干-答案-草稿纵向排列),横屏模式采用左右分栏布局(题干+答案 | 草稿左右排列)。经典模式布局比例:题干50% + 答案30% + 草稿20%。
用户问:"横屏模式的布局比例是多少?"
纯 KNN 检索的结果可能是这样的:
| 排名 | 召回内容 | 问题 |
|---|---|---|
| 1 | "竖屏模式采用上下分栏布局,横屏模式采用左右分栏布局" | 说了布局方向,但没说比例 |
| 2 | "布局设置支持横屏和竖屏分别保存配置" | 泛泛而谈 |
| 3 | "经典模式布局比例:题干50% + 答案30% + 草稿20%" | ✅ 这才是正确答案 |
看到问题了吗?KNN 返回的内容在语义上 确实都和"横屏布局"相关,但用户要的是具体比例数字,而 KNN 对数字这类"硬信息"的区分能力不足。
BM25:关键词检索的经典算法
BM25(Best Matching 25) 是信息检索领域的经典算法,ES 默认的文本相关性打分就是它。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> B M 25 ( D , Q ) = ∑ i = 1 n I D F ( q i ) ⋅ f ( q i , D ) ⋅ ( k 1 + 1 ) f ( q i , D ) + k 1 ⋅ ( 1 − b + b ⋅ ∣ D ∣ a v g d l ) BM25(D, Q) = \sum_{i=1}^{n} IDF(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{avgdl}\right)} </math>BM25(D,Q)=i=1∑nIDF(qi)⋅f(qi,D)+k1⋅(1−b+b⋅avgdl∣D∣)f(qi,D)⋅(k1+1)
参数解释(不重要):
- <math xmlns="http://www.w3.org/1998/Math/MathML"> D D </math>D 为目标文档, <math xmlns="http://www.w3.org/1998/Math/MathML"> Q = { q 1 , q 2 , ... , q n } Q = \{q_1, q_2, \ldots, q_n\} </math>Q={q1,q2,...,qn} 为查询词集合
- <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( q i , D ) f(q_i, D) </math>f(qi,D) 为词 <math xmlns="http://www.w3.org/1998/Math/MathML"> q i q_i </math>qi 在文档 <math xmlns="http://www.w3.org/1998/Math/MathML"> D D </math>D 中的词频(TF)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> ∣ D ∣ |D| </math>∣D∣ 为文档 <math xmlns="http://www.w3.org/1998/Math/MathML"> D D </math>D 的长度(词数), <math xmlns="http://www.w3.org/1998/Math/MathML"> a v g d l avgdl </math>avgdl 为语料库平均文档长度
- <math xmlns="http://www.w3.org/1998/Math/MathML"> k 1 k_1 </math>k1(默认 1.2)和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b(默认 0.75)为调参常数
- <math xmlns="http://www.w3.org/1998/Math/MathML"> I D F ( q i ) = log N − n ( q i ) + 0.5 n ( q i ) + 0.5 IDF(q_i) = \log\frac{N - n(q_i) + 0.5}{n(q_i) + 0.5} </math>IDF(qi)=logn(qi)+0.5N−n(qi)+0.5, <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 为总文档数, <math xmlns="http://www.w3.org/1998/Math/MathML"> n ( q i ) n(q_i) </math>n(qi) 为包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> q i q_i </math>qi 的文档数
BM25 它的核心思想有三点:
- 词频(TF):一个词在文档中出现越多,这个词对该文档越重要------但有上限(饱和曲线)
- 逆文档频率(IDF):一个词在所有文档中出现越普遍(如"的"、"是"),它的区分度越低,权重越低
- 文档长度归一化:长文档天然占优势,需要适当降低权重
BM25 vs TF-IDF 的关键差异 :早期常用的 TF-IDF 中词频是线性增长 的------一个词出现 100 次的文档得分是出现 10 次的 10 倍,这显然不合理。 BM25 引入了 词频饱和 机制,公式是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( k 1 + 1 ) ⋅ t f k 1 + t f \frac{(k_1+1) \cdot tf}{k_1 + tf} </math>k1+tf(k1+1)⋅tf,让词频贡献有一个天花板,更符合直觉。
ES 中调用 BM25 时有两个关键参数,其实就是 BM25 公式中的参数:
| 参数 | 默认值 | 作用 |
|---|---|---|
k1 |
1.2 | 控制词频饱和速度。值越大,词频对分数的影响越大 |
b |
0.75 | 控制长度归一化强度。0 = 忽略长度,1 = 完全归一化 |
实战建议 :大多数场景下,默认值就够用了。只有在你发现短文档总是排不上、或者长文档霸榜时,才需要微调这两个参数。
混合检索:两阶段策略
核心思路是 "KNN 提供候选池,BM25 做最终判定":
topK × 30 条"] C --> D["BM25 Rescore
在窗口内重排序"] D --> E["minScore 过滤
低于阈值丢弃"] E --> F["返回 topK 结果"] style C fill:#2E86AB,color:#fff style D fill:#E84855,color:#fff style E fill:#F9A825,color:#333
第一阶段:kNN 粗召回
用 ES 的 _knn_search 接口做向量近邻搜索,召回窗口设为 topK 的 30 倍。比如最终要返回 10 条结果,KNN 先粗召回 300 条作为候选池。
json
{
"knn": {
"field": "vector",
"query_vector": [0.023, -0.157, ...],
"k": 300,
"num_candidates": 300
}
}
为什么是 30 倍? 这是很多博主推荐的值,经过我的实验30倍的效果也确实不错。20 倍召回不够充分,容易漏掉关键结果;50 倍召回质量没有明显提升,但耗时显著增加。30 倍是召回质量和性能的平衡点。
第二阶段:BM25 重排序
在 kNN 召回的候选窗口(就是上一步 kNN 匹配到出来的)内,用 BM25 重新打分。这一步使用 ES 的 rescore 机制:
json
{
"rescore": {
"window_size": 300,
"query": {
"query_weight": 0.2,
"rescore_query_weight": 1.0,
"rescore_query": {
"match": {
"textContent": {
"query": "分片大小是多少",
"operator": "and"
}
}
}
}
}
}
权重设计:
- KNN 权重 0.2:提供语义相关的候选池
- BM25 权重 1.0:主导最终排序,让关键词匹配度高的内容排上来
为什么不反过来,让 KNN 主导? 因为在中文技术文档场景中,用户的问题往往包含专业术语和精确信息(如版本号、配置值、类名),BM25 对这类"硬词"的命中率远高于语义检索。让 BM25 做最终判定,能显著提升回答的准确性。
安全网:minScore 过滤
低于一定分数的结果,即使排在 topK 里也不应该返回。实践中设 min_score = 0.3,过滤掉语义和关键词都不匹配的噪音结果。
市面上的其他做法
混合检索不止 ES rescore 这一种实现,业界还有几种常见方案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| ES Rescore | KNN 召回 + BM25 重排序(本章方案) | 已有 ES、中小规模数据,需要自由调整向量查找和文本查找的权重 |
| RRF(倒数秩融合) | 将 KNN 和 BM25 的排名取倒数后加权融合 | 两路检索结果需要公平融合 |
| Ensemble Retriever | LangChain 提供的混合检索器,支持 BM25 + VectorStore | Python/LangChain 技术栈 |
| Cross-Encoder 重排 | 用交叉编码器对候选结果精细打分 | 对精度要求极高、可接受额外延迟 |
对于最常见的 ES Rescore 和 RRF,虽然都加权,但在核心思想上和适用场景上还是有所区别的。
| 维度 | RRF (倒数排名融合) | Rescore (重评分) |
|---|---|---|
| 核心原理 | 基于排名。忽略原始分数,只看文档在各自列表中的位置(第几名)。 | 基于分数。利用原始分数或新的查询分数进行加权计算。 |
| 解决痛点 | 分数不可通约。解决 BM25(关键词)和向量检索(语义)分数维度不同、无法直接相加的问题。 | 性能与精度的平衡。避免对全量数据进行昂贵计算(如复杂脚本、短语匹配),只对头部结果精排。 |
| 加权对象 | 加权的是"检索器"(例如:关键词检索权重 0.8,向量检索权重 0.2)。 | 加权的是"分数"(例如:原始分 0.7 + 重排分 1.2)。 |
| 计算范围 | 通常在协调节点进行,融合多个检索结果集(Window Size 内)。 | 在每个分片上,对Top-N (Window Size) 文档进行二次计算。 |
| 典型场景 | 混合检索。必须同时使用关键词和向量搜索时。 | 精细化排序。比如先搜"手机",再对前 50 个结果计算"销量*价格"或进行"短语匹配"。 |
目前最主流的做法 :在企业级 Java/Spring 技术栈中,ES Rescore 方案 是采用最广泛的,因为它在同一个引擎内完成所有操作,无需额外的数据同步和系统拼接。
第五章|总结------什么时候该用 ES,什么时候该上向量数据库?
本章帮你做最终的技术选型决策,给出清晰的判断标准和行动建议。
决策树
KNN + BM25 混合检索"] C -->|否| F{"向量检索是核心业务吗?"} F -->|否| E F -->|是| G["🔶 考虑 ES + Milvus
ES 做文本,Milvus 做向量"] D -->|否| H["先用 ES 起步
Spring AI 抽象层让切换成本极低"] D -->|是| I{"数据量级?"} I -->|百万级以下| J["Qdrant(轻量好上手)"] I -->|亿级以上| K["Milvus(分布式强扩展)"] style E fill:#27AE60,color:#fff style G fill:#F39C12,color:#fff style H fill:#27AE60,color:#fff style J fill:#2E86AB,color:#fff style K fill:#2E86AB,color:#fff
总结
-
大多数 Spring 项目做 RAG,ES 8.x 完全够用------一套引擎同时搞定文本检索和向量检索,运维成本最低,Spring AI 原生支持。
-
RAG 效果好不好,七成在数据,两成在召回,一成在模型------别上来就死磕模型和 prompt,先把分片策略和混合检索做好,效果提升立竿见影。
-
先用 ES 起步,未来切 Milvus 只需换配置------Spring AI 的 VectorStore 抽象层让你今天的选型不会被明天"锁死",拥抱变化,低成本试错。
第六章|Spring AI + ES:从配置到跑通
这章就是实战了,只学理论的可以收手了。本章带你用 Spring AI 框架,以 ES 作为 VectorStore,搭建一个最小可用的 RAG 应用。
环境准备
| 组件 | 最低版本 | 推荐 |
|---|---|---|
| Spring Boot | 3.3.x | 3.5.x |
| Elasticsearch | 8.13.x | 8.14.x+ |
| JDK | 17 | 21 |
Docker 启动 ES
bash
docker run -d \
--name elasticsearch \
-p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "xpack.security.http.ssl.enabled=false" \
elasticsearch:8.14.3
访问 http://localhost:9200,看到 JSON 返回即启动成功。
本地部署 Embedding 模型(Ollama)
bash
# 拉取 Embedding 模型
ollama pull mxbai-embed-large:latest
# 如果还需要对话模型
ollama pull qwen2.5:7b
项目搭建
1. 引入依赖
xml
<dependencies>
<!-- Spring AI 核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI Elasticsearch 向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-elasticsearch-store-spring-boot-starter</artifactId>
</dependency>
<!-- Ollama(如用本地模型) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Data Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
2. 配置文件
yaml
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: qwen2.5:7b
embedding:
model: mxbai-embed-large
elasticsearch:
uris: http://localhost:9200
3. 配置 VectorStore
java
@Configuration
public class EsRagConfig {
@Bean
public VectorStore vectorStore(RestClient restClient, EmbeddingModel embeddingModel) {
ElasticsearchVectorStoreOptions options = new ElasticsearchVectorStoreOptions();
options.setIndexName("knowledge_base"); // 索引名
options.setSimilarity(SimilarityFunction.cosine); // 余弦相似度
options.setDimensions(1024); // 向量维度
return ElasticsearchVectorStore.builder(restClient, embeddingModel)
.options(options)
.initializeSchema(true) // 自动创建索引
.batchingStrategy(new TokenCountBatchingStrategy())
.build();
}
}
initializeSchema(true)` 会在项目启动时自动创建 ES 索引和 mapping,开发阶段非常方便。生产环境建议关闭,改为手动管理索引。
4. 最小 RAG Controller
java
@RestController
@RequestMapping("/rag")
public class RagController implements CommandLineRunner {
@Resource
private ChatClient chatClient;
@Resource
private VectorStore vectorStore;
// 启动时把文档写入向量存储
@Override
public void run(String... args) {
List<Document> documents = List.of(
new Document("Spring Boot 的自动装配通过 @EnableAutoConfiguration 注解触发,"
+ "会扫描 classpath 下的 META-INF/spring.factories 文件来加载配置类。"),
new Document("Spring AI 是 Spring 官方推出的 AI 开发框架,"
+ "支持多种大模型和向量数据库的集成,提供统一的抽象接口。"),
new Document("Elasticsearch 8.x 原生支持向量检索,"
+ "底层使用 HNSW 算法,可以在同一索引中同时进行文本检索和向量检索。")
);
vectorStore.add(documents);
System.out.println("文档写入 ES 成功!");
}
// RAG 问答接口
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.advisors(new QuestionAnswerAdvisor(vectorStore))
.call()
.content();
}
}
5. 测试
bash
curl "http://localhost:8888/rag/ask?question=Spring+Boot的自动装配原理是什么"
模型会先从 ES 中检索出最相关的文档片段,然后基于这些片段生成回答。
Spring AI 的 VectorStore 抽象
Spring AI 最大的价值在于 VectorStore 接口的统一抽象。你只需要换一个实现类,就能从 ES 切换到 Milvus、Qdrant、Redis 等任何向量存储:
add() / similaritySearch()"] B --> C["ElasticsearchVectorStore"] B --> D["MilvusVectorStore"] B --> E["QdrantVectorStore"] B --> F["RedisVectorStore"] style C fill:#2E86AB,color:#fff style B fill:#F9A825,color:#333
这意味着 :假设今天你用 ES 跑通了,明天业务增长需要切 Milvus,业务代码一行不用改,只需要换依赖和配置。这就是 Spring 体系"约定优于配置"的威力。