最近在搞 RAG 相关的东西,遇到了一个实际需求:既要根据关键词把文档捞出来,又得能按语义找到内容相近的段落。单独用 Elasticsearch 的关键词查询,常碰到同义改写之后完全找不到的尴尬;单独做向量检索,精确数字、代码片段、人名又经常召回不准。后来我选了 LangChain4j 的 Elasticsearch 集成来做混合检索,把 BM25 和向量召回在结果层用 RRF 做融合,整套流程跑通之后,检索质量确实上了一个台阶。
这篇东西写给谁?写给用 Java 做后端、想在项目里接 RAG 或语义搜索,但又不想重新造轮子的同学。你不需要已经精通 LangChain4j,只需要熟悉 Spring Boot、Maven,以及最基础的 Elasticsearch 概念。我会把整个集成的选型思路、环境搭建、索引设计、查询代码、常见坑都过一遍,最后还会聊几个我实际踩过的坑。
1. 混合检索到底解决了什么问题
1.1 从“找出包含关键词”到“理解用户想找什么”
传统搜索的经典代表就是 ES 的 BM25 全文检索。它的思路很朴素:把文档拆成词条,统计 term frequency 和 inverse document frequency,相关性就是“这个词在哪些文档里出现得越多、在整个索引里越罕见,文档就越靠前”。这套东西在“精确命中”场景下非常可靠,比如用户搜“phone number: 12345”,你把它倒排索引里的精确词条拿出来,匹配得非常干脆。
但它的硬伤在于词汇鸿沟。用户查询和文档可能语义相同但用词完全不同,比如“怎么退订流量包”和“关闭数据套餐的步骤”,在词面上基本不重叠,BM25 就很难召回。这也是 RAG 应用里最常见的检索失败场景。
向量检索的思路完全不同。它先通过 embedding 模型把文本映射成稠密向量,然后拿查询向量去索引里做近邻搜索,比如余弦相似度或点积。语义相近的句子即使没有一个共同词,向量距离也会很近。缺点则是它比较擅长“模糊”,不擅长“精确”。虽然现在不少 embedding 模型对数字、标识符也有一定的表达能力,但真到“版本号 v2.3.1”这种级别,召回依旧容易漂移。
混合检索就是两边都做:先用关键词锁定精确信息,再用向量补上语义泛化,最后在结果集上做融合排序。LangChain4j 做这件事的妙处在于,它帮你把“写两套查询再合并”的琐碎过程封装进了统一的 API 里,而且底层 ES 客户端不用你手工维护连接和序列化。
1.2 LangChain4j 在 Java 生态里扮演什么角色
LangChain4j 是 Java 世界里的 LLM 应用框架,对标的是 Python 生态里很火的 LangChain。它提供了一整套抽象:ChatLanguageModel、EmbeddingModel、EmbeddingStore、ContentRetriever、AiServices 等等。你用它来做 RAG,流程大概是:先切分文档,调用 EmbeddingModel 把每个 chunk 转成向量,存入 EmbeddingStore;查询时再把问题转成向量,从 store 里检索出 top-k 文本片段,拼进 prompt 后交给 LLM 生成答案。
它和 Python 生态不太一样的地方在于:Java 项目里很多人其实已经有一套基于 Spring Boot 的现有服务,LangChain4j 的设计目标就是让你能低门槛地嵌进这种传统 Java 后端里,而不是另起炉灶。官方还提供了 langchain4j-spring-boot-starter,只要在 application.yml 里写配置就能启动。
而我选择它的另一个原因是版本迭代很有节奏感。早期版本分发包名是 com.langchain4j,后来按不同集成拆成了多个模块,比如 langchain4j-elasticsearch、langchain4j-open-ai、langchain4j-ollama。我最早在 0.31.0 上跑通过一个 demo,后来升级到 1.x,核心 API 变化不大,但模块名和配置项有调整,升级时留意 changelog 就好。
1.3 为什么向量库选了 Elasticsearch
可选方案很多:专用向量数据库如 Milvus、Qdrant、Pinecone,也有关系型数据库插件比如 pgvector,还有 Redis 的向量模块。如果你是新项目、纯做向量检索,这些都很合适。但如果是存量项目,ES 已经在作为核心检索引擎,那么再引入一个独立向量库意味着要处理两套存储、两套运维、两份数据同步。而在 ES 8.x 里,dense_vector 字段类型已经相当成熟,kNN 查询性能在中小数据量上表现可以接受,再加上 ES 本来就支持 BM25,天然就是做混合检索的底子。
另外,ES 8.8 版本以后,官方已经在查询 DSL 里直接支持了 rank: rrf 语法,可以把布尔查询和 kNN 查询结果在 ES 内部做融合,避免把 top-k 结果拉回应用层再手写融合逻辑。虽然这个能力在不同许可证下可能有版本差异,但在 8.x 某个较新的开源许可版本里已经可用;如果你手里是企业版,那就更不用担心。这个点我们后面实操部分单独说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前置准备:环境搭建和选型
2.1 版本怎么搭配才不折腾
我先说结论,这套组合我在本地和测试环境都验证过:
- Java 17 或 21,我推荐 21,后续 Spring Boot 3.2+ 对 21 支持已经很好。
- Spring Boot 3.2.x 以上,配合 langchain4j-spring-boot-starter 使用。
- Elasticsearch 8.11 以上,用 Docker 起单节点,开启安全认证但用简单密码,或者直接关闭安全,本地开发不折腾。
- LangChain4j 使用 1.x 稳定版,比如 1.1.0 左右;如果你还在用 0.31.0,注意坐标的变化。
如果你完全不用 Spring Boot,也可以只在 pom 里引入 langchain4j-elasticsearch,它不强制依赖 Spring。LangChain4j 的 spring-boot-starter 只是把一些 Bean 自动装配,核心逻辑在 langchain4j-core 和各个集成模块里。
2.2 Docker 快速起一个 Elasticsearch 实例
本地开发最简单的方式就是 Docker。先确认 Docker 可用,然后执行:
bash复制docker network create langchain4j-es-network
docker run -d \
--name langchain4j-es \
--net langchain4j-es-network \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
docker.elastic.co/elasticsearch/elasticsearch:8.11.1
这里把安全关掉,是因为本地 demo 不想搞证书和密码;如果要在测试环境用,建议至少开 basic auth。关闭安全之后,访问 http://localhost:9200 应该能看到类似这样的响应:
json复制{
"name" : "langchain4j-es",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "xxxx",
"version" : {
"number" : "8.11.1",
"build_flavor" : "default"
}
}
注意 xpack.security.enabled=false 在 8.x 中如果不做额外配置,会禁用 HTTPS 和认证;如果你之前起过带认证的容器,记得清掉数据卷再重启。
