Spring AI集成Milvus:从零搭建RAG知识库向量检索全流程
2026/9/7 20:54:11 网站建设 项目流程

1. RAG 项目里,为什么偏偏是 Milvus

先说结论:做 RAG 知识库,向量数据库是整个系统的“记忆中枢”。前面文档解析、切块做得再好,embedding 模型选得再强,最后检索阶段拉闸,整个问答体验一样稀碎。我见过太多项目,把精力全花在调 prompt 上,结果向量库里存的都是一坨没法稳定召回的数据,应用上线后用户随便问一句就答非所问。所以这个系列到专题二,我决定先把 Milvus 单独拎出来讲透。

这一篇不是教你背 Milvus 的 API 文档,而是带着你从零搭一套“Spring AI + Milvus”的完整 RAG 服务。我默认你已经会用 Spring Boot 写接口,也大概知道 RAG 是“检索增强生成”,但对向量库可能还停留在概念层面。看完这篇,你能回答这几个问题:Milvus 在这条链路里到底干了什么、它和 ChromaDB、PGVector、Qdrant 有什么区别、Docker 怎么装、Spring AI 怎么接、检索出来的东西怎么和 ChatClient 串成最终答案,以及生产环境最容易踩的坑有哪些。

1.1 先看一条完整内容链路

一个典型的 RAG 问答系统,跑完整流程是这样的:拿一批文档,PDF、Word、Markdown 都行,先做解析和清洗,然后按固定长度切块,每块文本丢给 embedding 模型转成向量,向量和原文一起写入向量数据库。用户提问时,把问题也转成向量,去向量库里做相似度检索,召回最相关的几个片段,最后把这些片段和问题拼成 prompt,交给大模型生成回答。

这个链路里,向量数据库服务两个核心动作:写入和检索。写入要快,不能因为文档一多就写入超时;检索要准,语义相关的片段必须排到前面,同时还要支持按照文档来源、时间、业务类型等 metadata 做过滤。Milvus 恰恰在这三点上做得比较均衡,这也是我在 Java 技术栈里优先选它的原因。

1.2 向量库选型:ChromaDB、PGVector、Qdrant、Milvus 怎么选

很多人第一次接触向量数据库,上来就问“哪个最火”,我不太喜欢这种问法。选型先看场景,再看团队维护成本。我整理了一张对比表,是最近给一个知识库项目做技术调研时用过的,直接放出来:

方案适合场景部署方式检索能力维护成本
ChromaDB个人项目、原型快速验证单机进程内基本向量搜索极低
PGVector已有 PostgreSQL,数据量中等作为 PG 插件基本向量搜索 + SQL 过滤
Qdrant中小规模生产、Rust 技术栈Docker / K8s向量 + payload 过滤
Milvus 2.x生产级知识库、千万级向量、高并发Docker / K8s / 云服务向量 + 标量过滤 + 混合检索中到高

如果你是个人博客问答、几百个文档的小玩具,ChromaDB 装在本地就行,别折腾。如果公司已经重度使用 PostgreSQL,数据量又不到百万级,PGVector 8 核机器也能扛。但一旦你的目标是做相对正式的知识库产品,要考虑多人并发、定期全量更新、按业务线做隔离,Milvus 的收益就出来了:它把向量索引和标量过滤做成了原生能力,不需要你手动拼 SQL 去降级检索质量。

Milvus 明显的短板是部署比 ChromaDB 重。单机 standalone 至少要依赖 etcd 和 MinIO,你可能觉得“不就存个向量吗,怎么还要对象存储”。这个后面第二章会解释,先记住一个结论:这个重是有价值的,换来了数据持久化和索引扩展能力。

1.3 Milvus 架构简读

Milvus 2.x 的架构拆开看,核心角色有这么几个:access layer 负责接收请求,coordinator 管元数据和调度,worker node 干实际的索引和查询的活。单机部署时,这些角色封装在一起,对外只暴露一个 19530 端口,内部走 etcd 存元数据、MinIO 存日志和索引文件。

理解这个架构对写代码没直接影响,但对排查问题帮助很大。比如你发现 Milvus 容器一直重启,大概率是 etcd 连不上;查询性能骤降,可能不是索引问题而是 MinIO 磁盘满了。Spring AI 接入时,你只需要面向 19530 端口写 gRPC 连接,剩下的内部组件不用关心,但心里要有这张拓扑图,出问题时候能猜到是哪层的事。

2. 先把 Milvus 跑起来:Docker 部署与健康检查

2.1 Docker Compose 一键起 standalone

Milvus 官方现在推荐用 Docker Compose 部署 standalone 模式。我在本地和测试环境都是这么干的,一条命令拉起整套依赖,不污染宿主机。先准备一个docker-compose.yml

version: '3.5' services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.14 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 - ETCD_SNAPSHOT_COUNT=50000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd healthcheck: test: ["CMD", "etcdctl", "endpoint", "health"] interval: 30s timeout: 20s retries: 3 minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin ports: - "9001:9001" - "9000:9000" volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 standalone: container_name: milvus-standalone image: milvusdb/milvus:v2.4.13 command: ["milvus", "run", "standalone"] security_opt: - seccomp:unconfined environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"] interval: 30s start_period: 90s timeout: 20s retries: 3 ports: - "19530:19530" - "9091:9091" depends_on: - "etcd" - "minio"

这个文件里有几个值得注意的地方。etcd 的ETCD_QUOTA_BACKEND_BYTES我设了 4GB,这是 Milvus 元数据能增长的上限,如果你的 collection 数量非常多,可以调大,但别随随便便弄到几十 GB,etcd 本身不适合存大数据量。MinIO 的账号密码默认minioadmin,本地测试无所谓,生产环境必须改成强密码,并且建议用环境变量注入,不要写死在 compose 里。

Milvus 2.4 这个版本是目前我用下来比较稳的,也是和 Spring AI 集成兼容性最好的一个主线版本。2.5 之后的结构化过滤和 full text search 更强,但 API 变动也大,等社区把坑填完再上不迟。

2.2 起完容器后先做这几件事

执行docker compose up -d之后,不要急着去写代码,先确认三个状态。

第一,三个容器都健康。看日志用docker compose ps,如果 standalone 容器卡在 unhealthy,多半是 etcd 或 MinIO 没起来,先修依赖再重试。

第二,确认 19530 端口通了。我用一个简单的 Java 测试类验证 gRPC 连接:

import io.milvus.client.MilvusServiceClient; import io.milvus.param.ConnectParam; import io.milvus.param.RpcStatus; public class MilvusConnectionCheck { public static void main(String[] args) { MilvusServiceClient client = new MilvusServiceClient( ConnectParam.newBuilder() .withUri("http://localhost:19530") .build()); RpcStatus status = client.getVersion(); if (status.getStatus() == RpcStatus.Success.getStatus()) { System.out.println("连接成功:" + new String(status.getMessage())); } else { System.err.println("连接失败:" + status.getMessage()); } client.close(); } }

第三,确认 9091 端口的健康检查接口能返回OKcurl http://localhost:9091/healthz,如果返回的不是 OK,说明 Milvus 内部组件还没就绪,等一会儿再试。

2.3 CentOS 7 / 资源紧张机器上的注意事项

热词里有人搜“centos7安装milvus”,我多说两句。CentOS 7 默认内核和 Docker 版本都比较老,装 Milvus 容易遇到两个坑:一是 Docker 版本低于 20,compose 语法解析失败,先升级 Docker;二是内存不足导致 etcd 频繁挂掉,Milvus 单机版本建议至少 8GB 内存,4GB 机器能跑但非常吃力,我实测在 4GB 机器上启动一个 collection 就要七八分钟。

如果机器内存紧张,可以给 compose 文件加上资源限制,比如:

deploy: resources: limits: memory: 4G

这样至少不会把宿主机拖死。另外,磁盘记得预留 20GB 以上,MinIO 默认会把索引和日志文件写进 volume,小磁盘很快会被撑满。

3. Spring AI 集成:依赖、配置与仓库抽象

3.1 引入 spring-ai-milvus-store 依赖

Spring AI 官方把向量数据库的集成拆成了很多独立模块,Milvus 对应的是spring-ai-milvus-store。要注意,Spring AI 1.0.0 之前还在迭代,artifact 版本经常带M1M6这类里程碑后缀,所以我建议用 BOM 统一管理版本,避免手动写错。

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-milvus-store</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai</artifactId> </dependency> </dependencies>

顺带解释一下为什么用 BOM:Spring AI 的spring-ai-milvus-store会传递依赖 Milvus 的 Java SDK,如果你手动引入 SDK 版本和模块内置版本不一致,很容易出现MethodNotFound这类诡异报错。BOM 能把所有组件锁在同一批版本上,这种问题基本就避免了。

3.2 YAML 配置与 Client 构建

Spring AI 的 Milvus 集成没有提供完全自动的spring.autoconfigure配置,需要你手动声明MilvusServiceClientVectorStoreBean。这一开始我也觉得麻烦,但后来发现反而更透明,连接参数和 collection 行为一目了然。

milvus: uri: http://localhost:19530 token: "" database-name: default collection-name: knowledge_base embedding-dimension: 1024 index-type: HNSW metric-type: COSINE consistency-level: Strong

Java 配置类如下:

@Configuration public class MilvusConfig { @Bean public MilvusServiceClient milvusClient(@Value("${milvus.uri}") String uri, @Value("${milvus.token:}") String token) { ConnectParam.Builder builder = ConnectParam.newBuilder() .withUri(uri); if (StringUtils.hasText(token)) { builder.withToken(token); } return new MilvusServiceClient(builder.build()); } @Bean public VectorStore vectorStore(MilvusServiceClient milvusClient, EmbeddingModel embeddingModel, @Value("${milvus.collection-name}") String collectionName, @Value("${milvus.embedding-dimension}") int dimension, @Value("${milvus.consistency-level}") String consistency) { MilvusVectorStoreConfig config = MilvusVectorStoreConfig.builder() .withCollectionName(collectionName) .withEmbeddingDimension(dimension) .withConsistencyLevel(consistency) .build(); return new MilvusVectorStore(milvusClient, config, embeddingModel); } }

这里有个关键点:collection-nameembedding-dimension是强绑定的。embedding 模型一旦换掉,向量维度跟着变,但 Milvus 里的 collection schema 不会自动重建,必须手动删除旧 collection 或者换一个 collection 名。我见过有人把 OpenAI 的 1536 维数据写完以后,切到 768 维的本地模型,检索结果一直为空,查了半天才发现是维度不一致导致的数据类型检查失败。

3.3 VectorStore 抽象:换向量库不用改业务代码

Spring AI 把向量库操作收敛成了VectorStore接口,核心方法就几个:addsimilaritySearchdelete。业务层拿到的是VectorStore,而不是某个具体实现类。

这意味着你把 Milvus 换成 PGVector、ChromaDB、Qdrant,业务代码几乎不用动,只要换掉 Bean 实现就行。我一开始也觉得这个抽象有点过度设计,直到有一次某篇文章推荐了另一个向量库,我为了评估性能,把整个存储层切过去跑 benchmark,改的只是配置类和依赖,三天就完成了对比测试。如果你后续有“向量库替换”的潜在需求,这个抽象能省下大量返工成本。

3.4 Embedding 模型选型:接 OpenAI 还是 Qwen 还是本地 BGE

Spring AI 的EmbeddingModel是可以替换的,这也是整个链路里最影响最终效果的一环。选型上我分三种情况说。

接 OpenAI 的text-embedding-3-small最简单,效果稳,1536 维,但调用要花钱,而且数据要出网,隐私敏感场景直接排除。接通义的Qwen embedding(也就是热词里那个qwen embedding)在中文场景下效果不错,1024 维,能走 Spring AI Alibaba 的 dashscope 通道,适合国内业务数据合规的场景。本地部署 BGE 系列模型,比如bge-m3,1024 维,用 Ollama 或者 Xinference 起一个 embedding 服务,延迟低、免费、数据不出内网,但机器需要额外显存,模型效果上中文略弱于商业 API。

我的建议是,项目初期先用商业 API 把链路跑通,验证业务效果,等到文档量大了、调用成本上来了,再切本地模型。维度变化时,记得重建 collection,别偷懒。

4. 从文档到问答:一个完整 RAG 流程的 Java 实现

4.1 文档加载与切块

Spring AI 提供了DocumentReader家族,常见的有PagePdfDocumentReaderTikaDocumentReaderJsonReader。PDF 解析我用PagePdfDocumentReader比较多,它按页返回Document,每页自带页码 metadata,方便后面统计和溯源。

var reader = new PagePdfDocumentReader("classpath:/docs/spring-ai-guide.pdf"); List<Document> documents = reader.get();

切块用的是TokenTextSplitter。它的底层是按 token 数切,而不是字符数,这对中文更友好。基本用法:

var splitter = new TokenTextSplitter(); List<Document> chunks = splitter.apply(documents);

默认参数下,切出来的块可能偏大偏小,我一般在项目里自定义参数。下面是调参后的示例,每块约 500 token,重叠 80 token:

var splitter = TokenTextSplitter.builder() .withChunkSize(500) .withChunkOverlap(80) .build(); List<Document> chunks = splitter.apply(documents);

为什么重叠不能省?因为切块时如果正好把一个完整语义段落砍成两半,后半个片段单独召回时,缺少前文信息,大模型拿到手里也读不明白。重叠 60~120 token 能在语义连续性和检索精度之间取个平衡。

4.2 写入 Milvus:id 幂等、metadata 字段

切完块以后,调用VectorStore.add()写入。我建议在Document里预留好id和自定义 metadata,别用默认生成的随机 id,否则后续数据更新时会很痛苦。

String docId = "doc_spring_ai_guide"; for (int i = 0; i < chunks.size(); i++) { Document chunk = chunks.get(i); chunk.setId(docId + "_" + i); chunk.getMetadata().put("source", "spring-ai-guide.pdf"); chunk.getMetadata().put("page", chunk.getMetadata().get("page")); chunk.getMetadata().put("docId", docId); } vectorStore.add(chunks);

幂等性怎么保证?如果同一份文档重新导入,你需要先删掉旧的同 docId 的向量,再写入新的。Milvus 的 delete 支持表达式过滤,可以按docId删:

vectorStore.delete(List.of(docId)); // 等价于按 id 删除,但只删精确 id // 更推荐的做法:遍历 docId 前缀,再删除

这里有个容易踩的坑:Spring AI 的delete(List<String>)删除的是 Document 的 id(也就是我上面拼的doc_spring_ai_guide_0),而不是 metadata 里的 docId。如果你希望“按业务文档维度批量删除”,要靠 metadata 过滤实现。Spring AI 1.0 之后在VectorStore接口增加了delete(SearchRequest)的重载,但不同版本兼容性有差异,我建议在没有把握时,直接调 Milvus SDK 的 delete 表达式完成批量清理。

4.3 向量检索与 metadata 过滤

写入之后,最关键的就是检索。Spring AI 的检索入口:

List<Document> hits = vectorStore.similaritySearch( SearchRequest.builder() .query("什么是 Spring AI 的 RAG 流程") .topK(5) .build() );

这背后做的事情是:把问题文本转成向量,去 Milvus 里按 COSINE 相似度召回 topK 个片段。如果你要限制只搜某一份文档,或者只看某个页面,可以加过滤条件:

SearchRequest.builder() .query("什么是 RAG") .topK(5) .filterExpression("docId == 'doc_spring_ai_guide'") .build();

filterExpression 的语法不是 SQL,是 Milvus 的布尔表达式。等号要用==,字符串用单引号,多个条件用&&。第一次写的时候容易手滑写成=或者漏了引号,然后报Expr evaluate error,报错信息还贼隐晦。

4.4 混合检索与重排

纯向量检索在语义匹配上很强,但在关键词精确匹配上有时反而拉胯。比如用户搜“Spring AI 1.0 发布”,如果文档里写的是“Spring AI 1.0.0 版本发布”,语义相近,但关键词不完全一致,向量检索通常也能召回。但搜“错误码 404”这种强标识性内容时,向量检索可能把语义相近但完全无关的文档翻出来。这时候就需要混合检索:向量召回 + 关键词召回,再合并重排。

Milvus 2.4 之后支持了 full text search,可以在同一个集合里做 BM25 检索,然后和向量检索做 RRF(Reciprocal Rank Fusion)合并。我用 Java SDK 直接调的话,大致是两层:先做向量search,再做 full textsearch,最后在 Java 侧合并。

Spring AI 1.0 的VectorStore接口对混合检索的原生支持还不够完整,所以我的做法是在 Service 层手动实现。核心代码结构如下:

public List<Document> hybridSearch(String query, String collectionName, int topK) { // 第一步:向量检索 List<Document> vectorHits = vectorStore.similaritySearch( SearchRequest.builder().query(query).topK(topK).build()); // 第二步:关键词检索(走 Milvus SDK) List<Document> bm25Hits = fullTextSearch(query, collectionName, topK); // 第三步:RRF 合并 return RrfFusion.merge(vectorHits, bm25Hits, topK); }

RRF 的核心逻辑是给每个候选分配一个1/(k + rank)的分数,k 一般取 60。我实际测下来,RRF 合并比简单拼接两个结果列表稳定得多,简单拼接会让重复命中的片段被排两次,导致最终返回的上下文重叠度太高,浪费大模型上下文窗口。

重排列(re-rank)又是另一层。如果你有资源跑一个交叉编码器模型,或者调外部 rerank API,可以在混合检索之后再精排一次。我目前在生产环境没有接 rerank,因为延迟会多出来 200~500 毫秒,对内部知识库问答来说收益不大。但如果你的场景是“从 100 个候选里精挑 5 个”,rerank 的价值就非常明显了。

4.5 交给 ChatClient 生成回答

检索回来的片段,最终要拼进 prompt。Spring AI 1.0 里可以这么组织:

String context = hits.stream() .map(Document::getText) .collect(Collectors.joining("\n\n---\n\n")); PromptTemplate promptTemplate = new PromptTemplate(""" 你是企业知识库助手,请基于以下资料回答用户问题。 如果资料中没有相关信息,请直接说明“未找到相关内容”,不要编造。 资料: {context} 用户问题:{question} 回答: """); Message message = promptTemplate.createMessage(Map.of("context", context, "question", question)); var response = chatClient.call(new Prompt(message));

这一步有个容易忽视的细节:召回片段拼接时,最好在每段之间加上分隔符,并在 prompt 里明确告诉模型“资料用分隔符隔开”。如果不加分隔符,模型可能把两段不相干的内容当成一个连续上下文,回答出现逻辑混乱。

还有一个经验:topK 不是越大越好。我之前调到 8,结果大模型回答里混入了两个不相关片段,答案反而比 topK=3 时更差。后来固定 topK=4,并在 prompt 里要求“如果某条资料与问题明显无关,忽略它”,效果稳定很多。这个值的取舍,一定要拿评测集去试,别拍脑袋。

5. 影响效果的几个关键参数

5.1 Collection 与索引参数

Milvus 的 collection 建好后,索引类型、metric type、索引参数都不能随意改了。所以建 collection 之前,最好先想清楚。Spring AI 的MilvusVectorStoreConfig默认建的索引是 HNSW。HNSW 有两个关键参数:M控制每个节点的连接数,efConstruction控制建索引时的搜索范围。

M一般设 16 或 32。M越大,召回精度越高,但内存和检索耗时都会增加。efConstruction我习惯设 200,构建慢一点没关系,查询阶段的召回率提升明显。如果你对延迟更敏感,可以降到 64。

metric type 我推荐 COSINE。L2 距离在向量没有归一化时可能受向量模长干扰,内积(IP)在高维稀疏向量里有一些特效,但对常规 dense embedding,COSINE 是“不怎么会错”的选择。

5.2 切分参数对召回率的影响

切分参数是 RAG 调优里性价比最高的地方。chunk size 太小,比如 200 token,召回片段很多,但每个片段信息量不足,大模型需要拼凑多个片段才能回答,容易漏信息。chunk size 太大,比如 1000 token,单片段信息是够了,但语义可能混杂,检索时精确匹配率下降。

我按文档类型给过一套经验值:

  • 技术文档 / FAQ:chunk 400~600 token,overlap 80~100
  • 长文本 / 书籍:chunk 600~800 token,overlap 120~150
  • 对话记录 / 工单:chunk 300~400 token,overlap 50~80

这是一个起手配置,不是银弹。最稳的方法还是抽一批真实问题做评测集,用召回率指标来定参数。热词里有“rag文档加载解析详细全流程”,说明大家确实在这块踩了很多坑,我这里先给一个能跑的经验起步值。

5.3 检索 topK 与 score 阈值

score 阈值是个很玄学的东西。Milvus 返回的相似度分数在不同 metric 下含义不同,COSINE 在 0~1 之间(严格说是 -1~1,但 embedding 模型输出通常非负)。如果你设scoreThreshold=0.6,可能把一些语义相关的长尾片段过滤掉,也可能放进来噪音。

我的建议是:开发阶段不要设 score 阈值,先看 topK 召回的内容是否合理,确认之后再根据实际分数分布定阈值。如果某个查询的平均相似度是 0.75,把阈值设成 0.7 会安全一些。最忌讳的是从别处抄一个“0.5 阈值”直接上线,不同 embedding 模型的分数分布差异巨大,照搬必踩坑。

5.4 数据更新与一致性

知识库不是只写一次就完事。文档更新时,我习惯先按业务 docId 批量删除旧向量,再重新加载新文档写入。Milvus 默认的一致性级别是 Bounded,也就是允许一小段时间内的数据滞后,对知识库场景完全够用。

如果你用的是 Strong 级别的强一致,写入完立刻查询,能保证读到最新数据,但吞吐会受一点影响。Spring AI 的配置里可以用withConsistencyLevel设置。内部知识库如果更新不频繁,用 Strong 更省心;高频写入的日志分析类场景,Bounded 更合适。

6. 排查实录与避坑清单

6.1 容器起不来还疯狂刷日志

我遇到最多的问题是 standalone 容器一直 unhealthy。第一反应去看完整日志:docker compose logs standalone,如果看到fail to init meta client字样的,十有八九是 etcd 没就绪。此时不是重启 standalone 就行,而是先看 etcd 的健康状态。

还有一种情况,之前在 CentOS 7 上遇到的:Docker 默认存储驱动是overlay2,但老内核支持不好,MinIO 写入时直接把磁盘写满,Milvus 报no space left on device。解决方法是清理 Docker 无用的镜像和 volume,或者给 MinIO 单独挂一块大磁盘。

6.2 连接失败 / 鉴权问题

连接http://localhost:19530失败,先确认端口有没有被防火墙拦截。CentOS 7 上我踩过无数次firewalld的坑,19530端口没放行,Java 客户端一直超时,但本机 curl 又正常。开端口命令:

firewall-cmd --zone=public --add-port=19530/tcp --permanent firewall-cmd --reload

鉴权问题多发生在生产环境。如果你配置了 token,Milvus 客户端连接时必须要带 token,否则报authentication failed。Spring AI 的配置里可以给MilvusServiceClientConnectParam设置.withToken(token)。这里有个我没想通的坑:Milvus 的 token 是username:password格式,而不是普通密钥字符串,如果你直接填一个 UUID 格式的 token,认证一样报错。构造方式可以去查一下 root 用户的 token 规则。

6.3 明明有数据却搜不到

“有数据但搜不到”这种情况,第一反应查 consistency level。我刚从 Bounded 切到 Strong 时遇到过,写入成功但立刻查询空结果。因为 Milvus 的查询默认走副本/分段的数据可见性,写入到可读之间有小窗口,Strong 能规避这个问题。

第二个原因,是没有指定 partition 或者过滤条件写错。如果你的代码里加了filterExpression,先把它去掉再试一次,如果去掉就有结果,问题在表达式语法或是指错了 metadata 字段名,需要对比写入时的 metadata key 和过滤表达式里的 key 是否完全一致。Spring AI 写入 metadata 时,有些类型会被转换,比如 Integer 可能变成 Long,导致page == 1匹配不上,要写成page == 1L

第三个原因,是 collection 的 schema 里没有定义能用于过滤的字段。Spring AI 的MilvusVectorStore默认会把 metadata 存成 dynamic field,支持过滤,但如果你手动建过 collection 且没有开启 dynamic field,过滤就会静默失败。日志里看不到任何报错,只有结果为空。

6.4 常见错误速查表

现象可能原因处理方式
Connection refusedMilvus 端口未开放 / 容器未启动检查容器状态、防火墙
Collection not foundcollection 名拼错或未自动创建确认配置里的 collection-name
dimension mismatch换模型后维度变了删掉旧 collection 重建
Expr evaluate errorfilterExpression 语法错误检查==、单引号、cast
检索结果总是第一页重复没有正确存储或恢复 Document id设置稳定的业务 id
大批量写入后查询变慢索引参数不够 / 内存不足调 HNSW 参数或扩容
disk fullMinIO 数据过多清理 volume / 扩容磁盘

6.5 推荐一个调试利器 Attu

Attu 是 Milvus 官方出的 Web 管理界面,Docker 一键起。调试的时候,我经常用它直接看 collection 里到低存了什么、metadata 长什么样、检索结果分数是多少,省去了写各种临时代码的麻烦。

docker run -p 8000:3000 -e MILVUS_URL=http://host.docker.internal:19530 zilliz/attu:latest

然后浏览器打开http://localhost:8000,连上 Milvus 就能看到所有 collection。排查“明明有数据却搜不到”这类问题时,Attu 里直接跑一次查询,能看到原始向量和分数的分布,比在代码里猜测快得多。

最后再分享一点自己的体会

Milvus 给我的整体感觉是:学习曲线比 ChromaDB 高,但换来的确定性和扩展性对得起这份投入。尤其是当你手里的文档从几百页涨到几十万页、查询并发从 1 个涨到 100 个的时候,ChromaDB 会先崩,PGVector 会先慢,Milvus 可能还在稳定输出。当然,这并不意味着每个项目都必须上 Milvus。如果只是个人博客和几百个文档,别折腾,ChromaDB 就能满足;如果你的目标是认真做知识库产品、要支撑团队协作和企业级并发,那把 Milvus 作为核心组件,是很值的一笔投资。

另外再补一个经验:RAG 系统的瓶颈从来不是单点组件的性能,而是“文档处理质量 + 切片策略 + embedding 模型 + 检索策略”这套组合拳。Milvus 只是其中一环,但也是最核心的一环。把这一环打牢,后面换模型、换 prompt、换应用层框架,都不会伤筋动骨。下一篇专题我打算把文档加载、清洗、切分的完整流程单独展开,那个环节的细节经常决定知识库最终能不能用,建议先把 Milvus 这一篇里的代码跑通,再往下走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询