☰
基于langchain4j的Spring Boot集成Milvus向量检索实战
2026/10/2 8:53:32 网站建设 项目流程

最近不少 Java 技术栈的朋友来问:Spring Boot 项目要接 Milvus 做向量检索,怎么搞?网上一搜,教程基本被 Python 和 pymilvus“霸屏”,Java 生态的资料不是太老就是太碎。很多人卡在第一步就懵了:到底是直接用官方 milvus-sdk-java,还是走 langchain4j?连接串到底怎么写?为什么本地把./data/milvus.db塞进去会报错?

这篇文章就是我最近把一个 Spring Boot 服务接入 Milvus 的完整记录,用的是 langchain4j 这套集成方案。我会直接从选型逻辑讲到 Docker 部署、核心代码、检索链路,最后附上这段时间踩过的坑。目标很明确:让你照着这篇文章,能从一个空项目开始,把 Spring Boot 连接 Milvus 跑通,并且知道每一步为什么这么干。适合准备做 RAG、知识库检索、语义搜索,但主力语言是 Java 的开发者。

1. 为什么我用 langchain4j 而不是直接调 Milvus SDK

先聊一个容易被忽略的问题:既然 Milvus 官方提供了 Java SDK,为什么还要绕一层 langchain4j?

我的判断是:如果你只是想在 Spring Boot 里“连上”Milvus,用官方milvus-sdk-java完全没问题;但如果你想做的是“检索增强生成”这类应用,那直接用 SDK 会非常痛苦。你自己得维护 embedding 调用、向量存储、相似度检索、上下文拼接……这些琐碎但高频的功能,本质上是 LLM 应用的基础设施,不是业务代码。langchain4j 把这些东西抽象好了,它对标的是 Python 生态里的 LangChain,在 Java 世界里算是最成熟的方案之一。

拿官方 SDK 和 langchain4j 做个直观对比更清楚:

对比项官方 milvus-sdk-javalangchain4j-milvus 集成
连接管理手动创建 MilvusServiceClient,处理 channel、超时通过 EmbeddingStore builder 封装,内部管理
Schema 定义手动建 Collection、字段、索引、度量方式自动建 Collection,配置化完成
Embedding 接入自己调用模型服务,自己拼向量与各类 EmbeddingModel 无缝集成
相似度检索手动构造 QueryParam、OutputField一行similaritySearch,自动处理向量化
与 Spring Boot 整合要自己写配置类、封装服务天然面向 Spring 场景,可以直接注册 Bean

最关键的一点:langchain4j 里的MilvusEmbeddingStore本身还是基于官方 SDK 封装的,所以底层通信能力没有缩水,只是把脏活累活给隐藏了。我在生产环境里实测下来,连接稳定性和检索性能都够用。我见过有团队坚持用官方 SDK 手搓检索服务,结果代码里塞满了向量维度校验、字段映射、结果解析这种重复代码。不是不能跑,是没必要。

提示:如果你的项目目标只是“往 Milvus 里灌向量”而不关心后续的语义检索,那用官方 SDK 反而更轻。但如果你做的是知识库问答、智能客服、文档检索,强烈建议直接用 langchain4j,省下的维护时间不是一点半点。

2. 先把 Milvus 跑起来:Mac 上 Docker 部署的实操细节

说实话,Milvus 服务的启动本身不难,难在环境细节。我开发机是 Mac,这里就以 Mac + Docker 为例讲,Linux 服务器上的部署思路完全一样,只是路径和防火墙略有差异。

2.1 Docker Compose 部署 standalone 模式

官方推荐的生产模式是分布式,但本地开发用 standalone 就够了。我是用 Docker Compose 一次性拉起 etcd、minio、milvus 三个组件。很多人看到三个服务就头大,其实 standalone 模式下它们就是 Milvus 的三个内部依赖,一个 Compose 文件全部搞定:

version: '3.5' services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.5 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 minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 milvus: container_name: milvus-standalone image: milvusdb/milvus:v2.4.9 command: ["milvus", "run", "standalone"] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - "19530:19530" - "9091:9091" depends_on: - etcd - minio

这里有个很容易踩的坑:镜像版本尽量指定,不要用latest。我试过在 Mac 上拉最新版镜像,偶尔会遇到与本地 Docker 版本不兼容导致的启动失败,报错信息还特别含糊。指定v2.4.9这类具体版本,至少你能确定这组依赖是官方测试过的。

部署命令就是:

docker compose up -d

然后看日志确认启动成功:

docker logs -f milvus-standalone

如果能看到milvus server started之类的日志,说明服务起来了。

2.2 健康检查与可视化工具

服务起来之后,先验证监听端口是否正常。Milvus 的 gRPC 端口是 19530,我习惯用 curl 快速探测一下:

curl -X GET http://localhost:9091/healthz

返回正常就说明 Milvus 的健康检查接口通了。接下来建议装一个 Attu,这是 Milvus 的可视化管理界面,对排查数据特别有用。一行命令启动:

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

浏览器打开http://localhost:3000,填入host.docker.internal:19530就能连上。有了 Attu,集合里的数据、索引、检索结果都可视化,调试效率高很多。

2.3 本地文件模式与 Docker 模式别搞混

热词里提到的milvus_uri: str = "./data/milvus.db",这是 Python 专用里的本地文件模式,很多人在 Python 教程里看到后,想在 Java 里照抄,结果发现根本没有milvus.db这个文件生成,直接报错。

这个必须说清楚:Java SDK 和 langchain4j 连接 Milvus,走的都是 gRPC 网络协议,连接串是host:port的形式,不是本地文件路径。想用本地模式,只有 Python 的 pymilvus 在特定场景下支持,而且它本质上是面向测试和单机小数据量的,不是常规连接方式。在 Spring Boot 里就别惦记这个了,老老实实连 Docker 里的服务。

注意:在 Mac 上如果用localhost:19530连接失败,先检查 Docker Desktop 是否在运行,再检查端口映射是否生效。docker ps看不到容器的话,大概率是 Compose 文件里的环境变量没配好,或者端口被宿主机其他进程占了。

3. Spring Boot 工程初始化:依赖版本和配置的讲究

Milvus 服务部署好了,接下来就是搭建 Spring Boot 工程。这部分看起来简单,实际上版本坑最多。

3.1 pom.xml 依赖怎么加才不打架

我建了一个普通的 Maven 项目,Java 版本用的 17。Spring Boot 版本选择有个原则:别追最新,选一个 langchain4j 明确兼容过的版本。我自己一开始用了 Spring Boot 3.3.x,结果和某个 langchain4j 旧版本存在依赖冲突,启动报错,降级到 3.2.x 就好了。后来我直接用 Spring Boot 3.2.x,一路顺畅。

核心依赖如下:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <langchain4j.version>0.36.2</langchain4j.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-milvus</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>${langchain4j.version}</version> </dependency> </dependencies>

注意这里我只加了langchain4j-milvus和langchain4j-open-ai,没有单独引官方 milvus Java SDK,因为langchain4j-milvus会把需要的 SDK 依赖带进来。如果你有自定义 embedding 模型需求,可以在后面自己替换langchain4j-open-ai为其他实现。

3.2 配置项:连接参数和集合参数

Spring Boot 项目里我习惯把 Milvus 相关参数统一放到application.yml,方便切换环境。核心配置结构如下:

langchain4j: milvus: host: localhost port: 19530 collection-name: my_kb dimension: 1536 index-type: AUTOINDEX metric-type: COSINE consistency-level: STRONG

这里几个参数背后都有讲究:

  • dimension必须和你的 embedding 模型输出维度一致。我用的是 OpenAI 的text-embedding-3-small模型,输出 1536 维,所以配置里写的 1536。如果你用其他模型,一定要先查清楚维度,不然后面插入数据会报错。
  • metric-type我用的 COSINE,适合做文本语义相似度。如果你做的业务对欧氏距离或者内积更敏感,可以换 EUCLIDEAN 或 IP。这个要和检索场景匹配,选错了效果差别很大。
  • consistency-level默认用 STRONG 就好,开发阶段避免出现“写进去了查不到”这种诡异问题。生产环境可以按需调成 BOUNDED 降低一致性开销。

3.3 为什么我不用./data/milvus.db做连接配置

这其实呼应 2.3 节。Spring Boot 里如果你看到网上有资料让你把milvus.db当作本地加载路径,直接忽略。Java 这边的连接方式永远是网络连接。application.yml里的host和port,在 Mac 本地开发时就是localhost:19530,在服务器上就是你远程部署 Milvus 的 IP 和端口。

4. 核心代码:从连接、建集到写入和检索

4.1 MilvusEmbeddingStore 的构造方式

langchain4j 里,连接 Milvus 的核心类是MilvusEmbeddingStore。它是EmbeddingStore接口的一个实现,内部封装了建集合、插入向量、检索相似度这些能力。我写了一个配置类,专门用来创建这个 Bean:

package com.example.milvusdemo.config; import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MilvusConfig { @Value("${langchain4j.milvus.host}") private String host; @Value("${langchain4j.milvus.port}") private Integer port; @Value("${langchain4j.milvus.collection-name}") private String collectionName; @Value("${langchain4j.milvus.dimension}") private Integer dimension; @Bean public MilvusEmbeddingStore milvusEmbeddingStore() { return MilvusEmbeddingStore.builder() .host(host) .port(port) .collectionName(collectionName) .dimension(dimension) .build(); } }

这段代码里有个容易忽略的细节:MilvusEmbeddingStore.builder()是流式构造,每次启动都会检查集合是否存在,不存在就自动建。所以我前面application.yml里的collection-name必须是全局唯一的业务集合名,避免多个环境共用同一个 Milvus 时互相干扰。

4.2 构建 EmbeddingModel

有了存储,还得有 embedding 模型才能把文本变成向量。我用OpenAiEmbeddingModel来举例,它支持 OpenAI 官方接口,也支持兼容 OpenAI 协议的自建服务:

package com.example.milvusdemo.config; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class EmbeddingModelConfig { @Value("${embedding.base-url:https://api.openai.com}") private String baseUrl; @Value("${embedding.api-key:}") private String apiKey; @Value("${embedding.model-name:text-embedding-3-small}") private String modelName; @Bean public OpenAiEmbeddingModel openAiEmbeddingModel() { return OpenAiEmbeddingModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .build(); } }

提示:如果你用的是国产模型或者企业内部模型,只要它提供 OpenAI 兼容的 embedding 接口,即便不用baseUrl这个配置名也行。关键是确认它返回的向量维度,并同步修改dimension。

4.3 文本写入 Milvus 的完整服务

存储和模型都有了,接下来就是把他们组合起来。我写了一个VectorKnowledgeService,负责接收一段文档,分块后写入知识库,并提供检索接口。这里直接演示最常用的两条链路。

先看写入:

package com.example.milvusdemo.service; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.model.embedding.EmbeddingModel; import org.springframework.stereotype.Service; import java.util.List; @Service public class VectorKnowledgeService { private final EmbeddingStore<TextSegment> embeddingStore; private final EmbeddingModel embeddingModel; public VectorKnowledgeService(EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) { this.embeddingStore = embeddingStore; this.embeddingModel = embeddingModel; } public void saveDocument(String documentContent) { Document document = Document.from(documentContent); List<TextSegment> segments = DocumentSplitters.recursive(300, 50) .split(document); for (TextSegment segment : segments) { embeddingStore.add(embeddingModel.embed(segment.text()).content(), segment); } } }

这段代码做了三件事:把文档字符串封装成Document;用递归分割器按 300 字符一块、50 字符重叠切成TextSegment;逐段生成向量后写入 Milvus。注意它依赖的embeddingStoreBean 是MilvusEmbeddingStore,因为EmbeddingStore是接口,Spring 会自动注入我们前文配置的实现类。

切块参数(300 / 50)不是随便写的。300 这个块大小是我在中文场景下做了多次效果对比后选择的,太小则语义不完整,太大则检索粒度太粗。重叠的 50 个字符则是为了保留跨块上下文。如果你的文档偏英文或者结构化明显,可以适当调大块大小。

4.4 检索:从问题到答案的链路

写入不是目的,能查出来才是。检索的核心方法是similaritySearch,它接受一个查询向量,返回 Milvus 中最相近的 TopK 个文本片段:

public List<String> search(String query, int topK) { var queryEmbedding = embeddingModel.embed(query).content(); var relevant = embeddingStore.search(queryEmbedding, topK); return relevant.stream() .map(match -> match.embedded().text()) .toList(); }

我在实际项目里会把topK设为 4,把返回的片段拼起来作为 LLM 回答的上下文。如果你只想验证 Milvus 连接是否通,可以先写一个简单的 Controller 调一下search方法,直接看返回的文本片段是不是和 query 语义相关。

完整的控制器如下:

package com.example.milvusdemo.controller; import com.example.milvusdemo.service.VectorKnowledgeService; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/kb") public class KnowledgeController { private final VectorKnowledgeService knowledgeService; public KnowledgeController(VectorKnowledgeService knowledgeService) { this.knowledgeService = knowledgeService; } @PostMapping("/documents") public void save(@RequestBody String content) { knowledgeService.saveDocument(content); } @GetMapping("/search") public List<String> search(@RequestParam String q, @RequestParam(defaultValue = "4") int topK) { return knowledgeService.search(q, topK); } }

到这里,一个最小闭环已经跑通了:往/kb/documents发一段文本,文本被切片向量化后写入 Milvus;再通过/kb/search发一个查询,就能拿到最相关的片段列表。后面接大模型做生成就是顺理成章的事。

5. 我踩过的几个典型坑:连接失败、维度不一致、版本冲突

这部分是我最想写的内容,因为网上教程通常只讲到“跑通 Demo”就停了,真正的麻烦往往在 Demo 之后。

5.1 连接失败:Connection refused的排查链路

我第一次启动项目时,控制台直接抛Connection refused。当时的第一反应是代码写错了,后来排查发现是 Docker Compose 里的 Milvus 容器根本没起来。

排查链路其实有规律:

  1. 先看 Docker 容器状态:docker ps -a,确认milvus-standalone是否在运行。
  2. 看日志:docker logs milvus-standalone,如果日志停在某个依赖服务超时,多半是 etcd 或 minio 没就绪。
  3. 检查端口占用:lsof -i :19530,确认没有其他进程占用。
  4. 最后才看代码里的 host 和 port 是否拼错。

还有一次,我换了台机器部署,把localhost硬编码在配置里,结果服务部署在其他机器上访问不到。正确做法是配置可注入的 IP,或者用环境变量覆盖。

5.2 插入向量报错:维度不匹配是最常见的问题

另一个高频报错是类似“collection dimension is 1536, but data dimension is 768”这样的错误。

这个错误基本只有一种原因:application.yml里的dimension和实际 embedding 模型输出维度不一致。我一开始用的本地 embedding 模型输出是 768 维,但配置里还留着 1536,结果插入第一条数据就崩了。后来我把配置改成 768,问题立刻消失。

给个经验法则:先确认模型,再写配置。任何情况下,代码里嵌入的模型输出维度都需要和 Milvus 集合的维度保持一致。如果后续换了模型,哪怕只是换了个版本,也可能导致维度变化。你需要在换模型时重建集合并重新灌数据,这不是改个配置就能解决的问题。

5.3 langchain4j 版本与 Spring Boot 版本的兼容性

版本冲突问题藏得比较深。我遇到过项目启动时NoSuchMethodError的情况,排查到最后发现是 langchain4j 传递依赖的某个类与 Spring Boot 内置的版本不一致。

建议是:Spring Boot 用 3.2.x,langchain4j 用 0.36.x,这是目前最稳的组合。如果你用 Spring Boot 3.4 或更高版本,先查一下 langchain4j 官方 release notes 里有没有声明兼容性,确认之后再升级。不要盲目追新,尤其是这种依赖链比较长的集成库。

如果已经出现冲突,最快的解决方式是统一版本:

mvn dependency:tree | grep langchain4j

查看所有 langchain4j 相关模块是否同一版本,如果有模块版本不一致,在 pom 的dependencyManagement里强制指定统一版本号。

5.4 集合自动创建与幂等性

MilvusEmbeddingStore的 builder 会自动创建集合,这个功能很方便,但它在重复插入时会带来一个问题:你重复跑保存文档的方法,文档不会自动去重,而是每次都往集合末尾追加。实测下来,如果我写了个测试脚本反复插入同一段文本,集合里会出现多条完全一样的向量,检索时返回的相似片段就会重复。

解决方案有两个:

  • 维护文档 ID,插入前先查询,存在就跳过或用add方法更新(langchain4j 提供了带 ID 的add方法)。
  • 干脆建一个清理定时任务,开发环境下定期删除整个集合并重建。

我在开发环境用的是第二种,简单粗暴;正式环境里,我会为每篇文档生成一个 hash 作为 ID,避免重复写入。

6. 一点工程化建议:从 Demo 走向可用

跑通基础链路之后,有几个事情值得提前想清楚。

第一,连接管理。MilvusEmbeddingStore默认会管理自己的连接,但长时间运行后如果出现连接假死,可以考虑定时发送心跳请求或者定期重建连接。我在做压测时遇到过偶发超时,重启应用就好了,说明连接状态并不是百分百稳定。这部分官方文档写得比较少,建议你在生产环境加一层监控。

第二,数据生命周期。Milvus 里的数据不会自动清理,集合会一直膨胀。如果你的知识库内容经常变动,最好给每条数据打上业务标签,比如通过TextSegment的自定义 metadata 存一个category字段。检索时可以按标签过滤,避免全量集合上的无效搜索。

第三,检索质量调优。similaritySearch返回相关片段只是第一步,真正决定用户体验的是 TopK 怎么选、片段怎么拼、大模型怎么用。我在实践中发现,固定 TopK=4、把返回的文本按相似度排序拼接,回答效果通常好于盲目调大 TopK。TopK 太大会引入噪音,太小会丢失关键上下文,这个需要根据文档粒度慢慢试。

第四,安全与合规。Milvus 默认没有开启认证,如果部署在公网服务器,一定要通过安全组限制 19530 端口只对应用服务器开放,或者开启用户名密码认证。知识库里的内容也建议做访问控制,避免通过检索接口把所有数据暴露出去。

我在实际使用中的体会是:Spring Boot 接 Milvus 这件事,技术上不复杂,复杂度全在细节里。用 langchain4j 能帮你挡住大部分底层琐事,但连接配置、维度一致性、版本兼容这些关键参数,还是得自己心里有数。最后再分享一个小技巧:开发阶段把application.yml里的consistency-level调成 STRONG,配合 Attu 的可视化界面,你能很直观地看到数据写的时机和查的结果,排查问题会轻松非常多。

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

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

立即咨询