news 2026/9/8 16:51:39

Spring Boot集成向量数据库与RAG:从语义搜索到智能问答

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot集成向量数据库与RAG:从语义搜索到智能问答

做“Spring with AI ()”这个系列,初衷很简单:很多团队技术栈已经稳定在Spring Boot上,现在又想把AI能力接进来,但不想为了一个“智能搜索”就引入一整套新语言新框架。这篇文章对应系列里的“搜索扩展”主题,核心是把向量数据库和RAG的底层逻辑讲清楚,同时给一套能在Spring Boot工程里直接跑的Java实现。标题里的“(上)”也不是随便写的,我打算先用这一篇把“向量化+检索”这条链路打通,后面再聊“把检索出来的片段拼进Prompt、让大模型基于私有知识生成答案”的完整RAG闭环。如果你正在做知识库问答、企业资料语义检索,或是已经被“用户搜了关键词但文档里根本没有这个字”折磨过,这篇应该对你有用。

我把话先说在前面:向量检索不是银弹,RAG也不是把文档扔进去就能得到一个好用问答机器人。搜索这块的升级,真正难的不是调用一个API,而是理解“为什么从倒排索引换成了向量的最近邻”,以及“怎么让检索单元、向量模型、相似度阈值互相配合”。这决定了你后面做出来的功能到底是玩具还是能上生产的东西。

1. 先弄清楚搜索到底要扩展什么

1.1 关键词搜索的边界在哪

传统搜索的实现逻辑,最容易理解的是倒排索引:把文档拆成词,建立一个“词 -> 文档”的映射表。用户输入查询词,系统去索引里找哪些文档包含这个词,再按词频、位置、权重之类的因素排序。这种模式下,搜索系统本质上是在做“字面匹配”。字面匹配稳,绝大多数场景也没问题,但它有一个先天性缺陷:用户想的词,和文档里写的词,往往对不上

举个例子,用户搜“苹果手机电池鼓包要不要换”,你的知识库文档标题写得是“iPhone电池膨胀处理建议”,这就出现了严重的主语不一致。传统搜索要做扩展,常见办法是做同义词表、拼音纠错、相关搜索词推荐,但这些规则要靠人慢慢维护,覆盖面有限,而且只能解决“已知的同义关系”。真正深层的问题在于:语言表达是无限多样的,“同一个意思”可以用几十种完全不同表面的句子来描述,靠有限规则很难穷举。

这也是为什么“搜索扩展”这件事,后来发展出了更自动化的路线。与其让机器去硬凑字面,不如让机器理解这句话背后的语义,再拿着查询语义去匹配文档语义。

1.2 向量检索的直觉理解

向量检索解决这个问题的思路是:用深度学习模型把一句话或一段文档映射成一组浮点数,也就是Embedding向量。假设这段向量是128维,你可以把它理解成这句话在一个“语义空间”里的坐标。同一个意思的表达,不管用词差多远,经过模型映射之后,它们在这个空间里的位置会比较接近。

可以把向量空间想象成一张地图:所有文档都是地图上的点,用户输入的查询也被映射成另一个点。向量数据库要做的,不是遍历所有点算距离(那样性能太差),而是用一种叫近似最近邻的算法,快速找到离查询点最近的一批文档点。这些“离得近”的文档,在语义上大概率也是和查询相关的。

比较两个向量相似度最常用的是余弦相似度,计算公式形式上是这样:

cos(A, B) = (A·B) / (|A| × |B|)

这个值落在-1到1之间,数值越大,说明两个向量方向越一致,语义越接近。做RAG检索时,我们通常使用阈值在0到1之间来判断检索相关性(取决于向量库的具体距离算法)。这里有一个很容易误伤的细节:不同向量模型的分数分布差异可能很大,有的模型算出来的相似度普遍偏高,有的偏低,所以0.7这个阈值不是万能标准,需要基于实际数据去调。

1.3 RAG为什么需要一个检索步骤

大模型虽然知识面广,但它对你公司内部的制度文档、产品手册、售后话术并不了解。RAG即检索增强生成,它的思路是分两步:先检索出和问题相关的内容片段,再把这些片段作为上下文和用户问题一起交给大模型,让它基于给定材料来回答。

这里的核心矛盾在于:大模型能接受的输入Token是有限的,你不能把几十万字的文档全部塞进Prompt,也无法保证模型能从海量资料中准确找到那一小段关键信息。检索这一步的价值,就是把“最可能跟当前问题相关”的内容片段从大资料库中挑出来。很多RAG项目效果不佳,问题并不出在大模型本身,而是出在检索这一步——根本没把相关片段捞出来,或者捞出来的片段是残缺的、噪音很多的。所以我在这个系列里,特意把检索部分作为单独一篇来写,值得多花时间。

2. 项目整体设计与选型思路

2.1 离线索引与在线查询两条链路

大多数RAG项目可以拆成两条链路。一条是离线索引链路:把原始文档读进来,做清洗、切分、向量化,最后写入向量数据库。另一条是在线查询链路:用户输入问题,系统把问题向量化,在向量数据库中检索相似片段,再把结果返回给上层应用。

这个设计在企业项目里有实际意义:离线索引往往比较耗时,一次要处理几百个文档,向量化也非常吃接口配额或GPU资源,所以应该做成后台任务,而不是每次用户搜索都重新处理一遍原始文档。在线查询链路则要求低延迟,通常在几百毫秒内返回,但这里的耗时大头往往不是向量数据库检索,而是向量化模型调用和后续大模型生成。如果你发现查询很慢,先看清楚慢在哪一段再优化。

2.2 向量数据库怎么选

在Spring AI里,向量存储被抽象成了一个叫VectorStore的接口,底层实现可以切换成不同的数据库。这种抽象带来的好处是:代码的业务逻辑不需要跟着底层数据库变,选型时也不用把路堵死。

我用一张表整理一下实际选型时容易遇到的场景:

向量存储方案适合场景注意点
SimpleVectorStore本地跑通流程、单元测试、演示Demo数据存在内存,JVM重启就没了
Chroma小规模知识库、快速原型、想保留数据持久化部署简单,但大规模并发能力一般
Redis / Elasticsearch向量能力原有基础设施已经有Redis或ES不用额外引入系统,但要评估版本能力
PostgreSQL pgvector业务数据本身在PostgreSQL里可以和事务数据放一起,但索引构建要关注
Milvus / Qdrant海量数据、高并发生产环境需要独立部署,运维成本更高

对我来说,判断标准很朴素:如果只是做技术验证,用SimpleVectorStore完全没问题;如果你已经确定要长期在项目里用向量检索,但团队不想维护额外数据库,PostgreSQL的pgvector或者已有Redis的向量模块挺合适。如果数据量到了千万级,或者查询QPS很高,这时候再认真评估独立的专业向量数据库。

这次示例我选择用Chroma来演示,原因很简单:它是独立数据库里部署成本最低的之一,基本可以当本地服务跑,而且Spring AI对它的集成比较成熟。Chroma里的Collection概念可以粗略理解成关系数据库的“表”,它内部实际做的事情是:把向量数据、文档原文、元数据一起存下来,统一管理。

2.3 Embedding模型决定语义质量上限

向量检索的上限不是向量数据库决定的,而是Embedding模型决定的。你自己可以做个实验:同一个文档,用两个不同Embedding模型生成向量,检索结果可能差异很明显。汉语里这种例子尤其普遍,比如“苹果”到底是水果还是手机品牌,模型训练语料不同,对上下文的捕捉能力也会有差别。

我在这类项目里已经踩过几次模型选型的坑,总结出的经验是:

  • 如果业务语料以中文为主,优先选择在中文语料上训练较好的模型,比如bge系列,或者业务偏垂直场景的话自己用领域数据微调Embedding模型。
  • 向量维度会影响存储开销和检索性能。OpenAI的text-embedding-3-small输出1536维,很多开源模型是1024维或768维。维度越高理论上表达能力越强,但存储和计算成本也在涨,没必要盲目追高。
  • 向量化要统一用同一个模型。索引时用A模型,查询时换了B模型,生成的向量空间都不一样,检索结果会完全失去意义。这个问题在团队协作时尤其容易发生。

3. 用Spring AI写一个最小可运行的向量检索

3.1 Spring Boot工程与依赖

我当前示例基于Spring Boot 3.4.x和Spring AI 1.0.0版本。因为Spring AI的版本更新确实比较快,并且很多接口在正式版前后有调整,我这里会使用正式版的写法和大家演示,如果看文章时版本已经又迭代了,文中提到的关键接口仍然可以作为参照。

先建立一个Spring Boot工程,Java版本至少选17,然后加入以下依赖:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-chroma-store</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

因为Spring AI已经提供了基于Spring Boot自动配置的Starter,只要在application.yml中配置好API Key和向量库连接信息,Spring容器里会自动出现EmbeddingModel和对应的VectorStore实现Bean,省去手动new一堆对象的麻烦。

如果是要跑通流程但不想在本地开Chroma服务,可以暂时不用引入spring-ai-chroma-store,直接用Spring AI提供的内存向量存储SimpleVectorStore。我自己在开发阶段经常是这样,先把RAG流程的功能逻辑验证好,再切到真实向量库。

3.2 配置Embedding模型与Chroma连接

Chroma需要先启动一个服务。最简单的跑法是用Docker容器,本地起一个映射到8000端口的实例。Spring AI连接Chroma后,可以自动生成所需的Collection。

spring: ai: openai: base-url: ${OPENAI_BASE_URL:https://api.openai.com} api-key: ${OPENAI_API_KEY:} embedding: options: model: text-embedding-3-small vectorstore: chroma: client: host: http://localhost:8000 initialize-schema: true collection-name: knowledge_chunk

关于collection-name这个配置我要多说一句:它相当于给这批向量数据起了一个逻辑命名空间,后续查询和写入都指向这个Collection。想重建索引时,通常不是直接删除Chroma里的数据目录,而是在代码或配置里换一个新的collection-name,这样最安全,可以避免老数据还没清干净就写新数据造成混淆。

3.3 把文档切分成合适的检索片段

在索引流程里最容易被忽视的是文档切分。很多人拿到一个PDF,直接把整页文字塞给模型向量化然后入库,结果检索质量很差。原因并不神秘:当你把一篇5000字的文档压缩成一个向量时,Embedding模型只能提取整体的主题,无法保留每个局部细节的精确位置。检索时,用户关心的是那5000字里某一个具体问题,你和整篇文档的距离自然不会近。

合理的做法是把文档切分成若干个较小的片段,再分别向量化。切分参数通常涉及两个:chunkSize(片段长度)和overlap(相邻片段重叠长度)。片段太长,语义被稀释;片段太短,单个片段信息量不足,回答问题时缺少上下文。我给一个适合起步的经验值:中文文档大约切分500到800个Token,重叠50到100个Token。这不是绝对的,要根据文档类型调整。

Spring AI里提供了一些现成的文本切分器,比如TokenTextSplitter就是基于Token数量来切,比按字符数切更接近模型实际看到的长度:

TextSplitter textSplitter = new TokenTextSplitter(600, 100);

如果把文档切得太碎,还有一个隐患:检索出来的一两个片段可能没头没尾,需要把相邻的前后片段也一起返回,才能给大模型充足的上下文。这种情况下,重叠参数的设置就能起到一定效果,至少能减少核心内容恰好被拦腰截断的几率。

3.4 解析文档并写入向量库

先用一个DocumentIndexService做索引写入。实际项目中,原始数据可能来自数据库、PDF、Word、网页,但这并不影响向量库写入层的设计。我们只需要把文本内容取出来,加上必要的元数据,构造出Document对象交给VectorStore。

@Service public class DocumentIndexService { private static final Logger log = LoggerFactory.getLogger(DocumentIndexService.class); private final VectorStore vectorStore; private final TextSplitter textSplitter = new TokenTextSplitter(600, 100); public DocumentIndexService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public int indexPlainText(String content, String source) { Document doc = Document.builder() .text(content) .metadata("source", source) .build(); List<Document> chunks = textSplitter.split(doc); vectorStore.add(chunks); log.info("索引完成,文档来源={},切分片段数={}", source, chunks.size()); return chunks.size(); } }

这里有一个经验:元数据一定要提前想清楚。source表示来源,是排查问题的重要线索,强烈建议至少把文档名、页面号或业务主键放进metadata里。将来召回一个片段,如果你都不知道它来自哪份文件,那这条检索结果对用户来说也没有多大意义。

3.5 查询与召回相似片段

查询侧的核心是把用户的文本查询转换为向量,然后在Collection中执行近邻搜索。Spring AI的VectorStore接口已经封装了完整流程:

@Service public class VectorSearchService { private final VectorStore vectorStore; public VectorSearchService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public List<Document> search(String query, int topK, double similarityThreshold) { SearchRequest request = SearchRequest.builder() .query(query) .topK(topK) .similarityThreshold(similarityThreshold) .build(); return vectorStore.similaritySearch(request); } }

similarityThreshold的作用是过滤掉相似度过低的结果,topK则决定最多返回多少条。给新手一个比较稳妥的建议:一开始可以把topK调大一点,比如返回10条,先肉眼看一下召回结果里到底有几条是相关的,然后再根据情况收紧。不要一上来就设个很低的阈值,看见结果里全是无关内容,会很打击信心。

3.6 最小RAG闭环:检索结果拼接上下文

既然标题提到了RAG,这里我先把最小闭环演示完:用户提问 -> 向量检索 -> 把命中文档内容拼接成上下文 -> 调用大模型生成回答。后续整个RAG问答的骨架,基本都是这样构建的。

@Service public class SimpleRagService { private final VectorSearchService searchService; private final ChatClient chatClient; public SimpleRagService(VectorSearchService searchService, ChatClient.Builder chatClientBuilder) { this.searchService = searchService; this.chatClient = chatClientBuilder.build(); } public String answer(String question) { List<Document> docs = searchService.search(question, 5, 0.5); String context = docs.stream() .map(Document::getText) .collect(Collectors.joining("\n---\n")); return chatClient.prompt() .system("你是一个知识库问答助手。请优先根据用户提供的资料回答问题。" + "如果资料中找不到答案,请明确说明资料中没有相关内容。") .user("参考资料如下:\n" + context + "\n问题:" + question) .call() .content(); } }

这个简单版本能跑通,但后续可以优化的点很多:比如只把检索结果的正文拼接起来,容易超Token限制;比如检索到相似但完全不相关的片段,也会混进上下文。要在大规模应用里做好RAG,还得在检索后增加重排序、过滤等步骤。这就留到系列后面的部分继续展开了。

4. 检索结果怎么看:相似度阈值和质量评估

4.1 相似度分数只是参考,排序才是关键

很多人第一次跑通向量检索,会盯着返回的score发呆:到底多少分算相关?我的回答是:不要过度解读绝对分数,先看排序。

向量检索召回的分数,其实是一个归一化后的相似度值。不同模型、不同向量数据库的分数分布差别很大。如果你拿一个0.7的阈值去过滤一个习惯于打出0.8、0.9分的模型,可能把不少有效结果都过滤掉了;但如果换成另外一个打分偏保守的模型,0.7可能又过滤得太少。所以更好的做法是先看量化排序,再结合具体场景设定阈值。

Spring AI官方建议是用类似similarityThreshold的参数控制兜底,而不是作为严格的分类器。我一般会把阈值设得偏低一些,比如0.5左右,然后用topK限制数量,最后在代码里做进一步过滤或重排。

4.2 通过元数据过滤来提升准确度

向量相似度只是“宽泛相关”,如果你的文档来源多样,用户可能只想在特定范围内搜索。比如公司有产品手册、内部制度、售后记录几类文档,用户问“请假流程”时,如果内部制度文档里恰好包含“请假”二字,可能就被召回了。最好在查询时加上元数据过滤条件。

Spring AI的SearchRequest支持传入过滤表达式。例如只想在来源为internal_policy.md的文档里搜索:

SearchRequest request = SearchRequest.builder() .query(question) .topK(5) .filterExpression("source == 'internal_policy.md'") .build();

加了过滤后,相当于在关系数据库里先加了WHERE条件再做向量检索,能够有效减少无关干扰。元数据设计越合理,后面能做的细粒度控制越多。

4.3 为什么需要关注“混合检索”

纯向量检索是稠密检索,它擅长语义泛化,但有一个明显不足:对精确词、产品型号、专有名词不够敏感。用户搜“AW-320型号说明书”的时候,如果Embedding模型没有把“AW-320”这个型号和文档中文案的关联学得足够好,检索效果可能不如直接关键词搜索。

所以在真实生产系统里,越来越多人会做混合检索:同时执行关键词检索(通常称为稀疏检索)和向量检索,再把两者结果做融合排序。这正好呼应了标题里的“搜索扩展”——不是用向量搜索完全替代传统搜索,而是在传统关键词搜索的基础之上,增加一个语义维度。混合检索后续我会在系列里具体展开,这里想表达的是:向量检索和关键词检索不是对立关系,它们是互补关系。

5. 我在接入过程中踩过的坑

5.1 Chroma连接和Schema自动初始化

本地用Chroma时,第一次启动后如果Spring应用连接不上,十有八九是网络或配置问题。得先在浏览器访问http://localhost:8000/api/v1看一下服务是否正常返回。如果你用Docker启动,要注意把端口映射出来,并且保证Spring配置里的host端口指向一致。

initialize-schema: true这个配置,Spring AI会自动去Chroma里创建Collection。但如果Collection已经存在,并且内部数据结构和预期不一致,自动初始化不会帮你清空数据。所以真正要清空重建时,要么在Chroma管理界面/API中删除Collection,要么整体换一个新的Collection名称。

5.2 元数据类型不一致导致写入失败

Chroma对元数据的类型要求相对严格。我第一次往同一个Collection里写入时,第一条文档的source字段存的是字符串,后面又有代码意外把这个字段写成了数字,结果Chroma直接报了类型不一致的错误,整个批次写入失败。

这个问题的排查看似很小,一旦遇到还蛮折磨人的。建议在代码里把元数据的类型固定好,并封装成统一的方法来构建Document,不要把随意拼出来的Map直接塞进Document里。团队的规范尤其重要。

5.3 片段切分导致语义残缺

之前有一位同事问为什么他做出来的RAG效果很差。我查了一下,他把一份文档按每200个字符切成一个片段,而且没有重叠。结果一个完整的“申请流程说明”,中间被硬生生切开了。用户问后半段流程时,模型拿到的是没有前因后果的半截文字,自然答不出来。

这个经历让我总结出一个经验:切分策略一定要结合文档本身的结构。如果文档有章节标题,可以优先按标题或段落切分;如果文档没有结构,再退而用固定长度切分;固定长度切分一定要设置重叠,避免语义被截断。代码层面其实容易做,但需要根据业务语料来做配置,而不是默认所有文档都一套参数。

5.4 Spring AI不同版本间的接口变化

Spring AI还很年轻,版本迭代带来的不兼容问题比一般Spring项目要多。网上各种教程有很多是基于0.8.x或1.0.0-M系列写的,早期代码里的similaritySearch(String query, int topK)这种写法,在后面的版本中已经慢慢被SearchRequest方式取代。

如果你看到老教程的接口编译不过,先别急着怀疑自己写错,先去看当前版本里VectorStore接口到底定义了什么方法。要稳定构建,最好直接以当前Spring AI文档和源码为准。这也是为什么我不喜欢把大量版本相关代码写死在一篇文里,因为过几个月再读可能已经过时。但整体思想是稳定的:先有EmbeddingModel,再通过VectorStore写入和查询。

5.5 向量化接口的超时和限流

批量索引文档时,很容易忽略一个实际问题:向量化接口对并发请求有限制。我之前一次性索引500个文档片段,直接并发发起了几百个Embedding请求,结果被供应商接口限流,大量的请求超时。

这个问题在代码上解决比较直接:给批量向量化加一个简单的信号量控制并发数,比如限制同时只有8个请求。如果做生产级的索引任务,还要考虑失败重试、进度记录、断点续传。很多人觉得RAG落地难,难题往往不在算法,而在这种工程化的小事上,处理不好就是线上事故。

最后分享一点我的调试习惯

我记得第一次做RAG项目时,总喜欢一出问题就怀疑大模型Prompt写得不对,花大量时间调Prompt,结果调了半天还是答不准。后来才发现,根本不是Prompt的问题,而是向量检索召回的内容压根就是错的或无关的,你怎么提示大模型都没用,巧妇难为无米之炊。

我现在排查这类功能的固定顺序一直是三条线:先验证Embedding模型能不能正常输出,再单独调试VectorStore查询看召回结果,最后才去检查Prompt和前端的完整链路。如果查询阶段就没召回正确内容,那大概率要查文档切分策略、向量模型选择或者相似度阈值;如果召回没问题但回答不对,再回头查上下文拼装和Prompt约束。用这个顺序排查,让我避免过很多次“白调Prompt”的无效工作。

这一篇主要是把向量检索和RAG的底座讲清楚了。索引怎么写、Collection怎么配、检索怎么调、阈值怎么选,这些问题解决后,你的Spring Boot工程已经拥有了基本的“语义搜索”能力。下一篇文章我再继续聊,怎样在这种检索能力的上面,构建真正稳定可靠的RAG问答,包括上下文压缩、重排序、多轮对话记忆,以及怎么把召回结果做得更准。到时候见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 16:50:06

TMS320F28335 DSP最小系统核心板设计:从原理图到PCB布局实战解析

简介&#xff1a;面向DSP开发者和电子工程师&#xff0c;这份完整资料包围绕TMS320F28335浮点DSP最小系统核心板&#xff0c;提供ALTIUM格式硬件原理图、PCB图&#xff08;未布线&#xff09;及配套测试软件源码&#xff0c;可辅助理解电源、复位、晶振时钟、存储器接口等最小系…

作者头像 李华
网站建设 2026/9/8 16:49:08

消除AI味:一套可复用的文本人性化改写方法

想让AI写的东西读着像个真人&#xff0c;这件事看起来不难&#xff0c;真做起来全是细节。我在日常内容生产里试过各种方法&#xff0c;从纯粹改词到重新搭结构&#xff0c;折腾了大半年&#xff0c;才慢慢总结出一套能稳定复用的套路。这套东西业内叫 humanizer&#xff0c;或…

作者头像 李华
网站建设 2026/9/8 16:48:37

SillyTavern 完整指南:如何十分钟搭好你的 AI 角色扮演前台

SillyTavern 完整指南&#xff1a;如何十分钟搭好你的 AI 角色扮演前台 【免费下载链接】SillyTavern LLM Frontend for Power Users. 项目地址: https://gitcode.com/GitHub_Trending/si/SillyTavern SillyTavern 是一个开源的 LLM 前端&#xff0c;负责把你本地的或云…

作者头像 李华
网站建设 2026/9/8 16:47:15

水下机器人AUV建模与仿真:从运动学方程到控制器调试全解析

简介&#xff1a;面向使用Matlab开展自主水下航行器研究的学生与工程师&#xff0c;这份资料提供了完整的AUV建模与仿真环境。代码基于Matlab 2014a、2019b、2024b编写&#xff0c;采用参数化编程模式&#xff0c;参数可灵活修改&#xff0c;便于观察不同工况下AUV的运动特性。…

作者头像 李华