在 LangChain4j 中使用 Qdrant 作为向量存储:从依赖接入、构建配置到元数据过滤实战
【免费下载链接】langchain4jLangChain4j is an idiomatic, open-source Java library for building LLM-powered applications on the JVM. It offers a unified API over popular LLM providers and vector stores, and makes implementing tool calling (including MCP support), agents and RAG easy. It integrates seamlessly with enterprise Java frameworks like Quarkus and Spring Boot.项目地址: https://gitcode.com/GitHub_Trending/la/langchain4j
Qdrant 是一个开源的向量数据库与相似性搜索引擎,LangChain4j 通过langchain4j-qdrant模块将其封装为标准EmbeddingStore实现,用于持久化 embedding 向量并执行相似性检索。本文以仓库中的 qdrant.md 文档为主线,结合 QdrantEmbeddingStore.java 源码,完整讲解依赖接入、Builder 配置参数、增删查操作、元数据过滤机制与底层实现原理,读完即可在 RAG 与语义检索场景中直接落地使用。
Maven 依赖
在项目中引入langchain4j-qdrant模块:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-qdrant</artifactId> <version>1.20.0-beta30</version> </dependency>从模块的 pom.xml 可以看到该模块的依赖关系:
- 核心依赖
langchain4j-core,提供EmbeddingStore、Embedding、TextSegment、Metadata等基础模型; - 官方 Java 客户端
io.qdrant:client:1.17.0,通过 gRPC 与 Qdrant 服务端通信; io.grpc:grpc-protobuf:1.77.0,用于 Qdrant gRPC 接口的 Protobuf 消息定义;- 测试依赖使用 Testcontainers 的
qdrant容器与langchain4j-embeddings-all-minilm-l6-v2-q量化 embedding 模型(详见后文测试章节)。
需要注意:该模块的 parent 版本为1.21.0-beta31-SNAPSHOT,发布版本号以 Maven Central 实际发布的坐标为准,引用时请替换为当前最新稳定版本。
核心 API
该集成暴露的 API 只有一个核心类:
QdrantEmbeddingStore— 位于包dev.langchain4j.store.embedding.qdrant,实现EmbeddingStore<TextSegment>接口,将 Qdrant 中的一个集合(collection)作为一个 embedding 存储,并在存储TextSegment时同步持久化其Metadata。
构建 QdrantEmbeddingStore:两种方式与全部配置项
方式一:通过 Builder 自动创建 QdrantClient
最常用的方式是使用QdrantEmbeddingStore.builder(),由模块内部根据主机、端口等信息创建 gRPC 客户端。Builder 的全部配置项如下(来源于 QdrantEmbeddingStore.java):
| Builder 方法 | 类型 | 默认值 | 必填 | 说明 |
|---|---|---|---|---|
collectionName(String) | String | 无 | 必填 | Qdrant 集合名称,构建时若为空会抛出NullPointerException |
host(String) | String | "localhost" | 否 | Qdrant 实例的主机地址 |
port(int) | int | 6334 | 否 | Qdrant 的gRPC 端口(注意不是 HTTP 端口 6333) |
useTls(boolean) | boolean | false | 否 | 是否启用 TLS/HTTPS 加密连接 |
payloadTextKey(String) | String | "text_segment" | 否 | 文本片段在 Qdrant payload 中的字段名 |
apiKey(String) | String | null | 否 | Qdrant API Key,用于身份认证 |
client(QdrantClient) | QdrantClient | null | 否 | 直接传入自定义的QdrantClient实例 |
最小可用示例:
QdrantEmbeddingStore store = QdrantEmbeddingStore.builder() .collectionName("my_collection") // 必填 .build(); // host 默认 localhost,port 默认 6334连接远程(或云托管)实例并启用认证与 TLS:
QdrantEmbeddingStore store = QdrantEmbeddingStore.builder() .host("qdrant.example.com") .port(6334) .useTls(true) .apiKey("your-api-key") .collectionName("my_collection") .payloadTextKey("text_segment") .build();从源码可以看出,构造时会通过QdrantGrpcClient.newBuilder(host, port, useTls)创建 gRPC 客户端;当apiKey非空时,会调用grpcClientBuilder.withApiKey(apiKey)附加认证信息(见 QdrantEmbeddingStore.java)。
方式二:传入已有的 QdrantClient
当需要复用连接、自定义 gRPC 配置或管理客户端生命周期时,可先自行构建QdrantClient再传入:
QdrantClient client = new QdrantClient( QdrantGrpcClient.newBuilder("localhost", 6334, false).build()); QdrantEmbeddingStore store = QdrantEmbeddingStore.builder() .client(client) // 使用已有的 QdrantClient .collectionName("my_collection") .build();Builder.build()内部会判断:若client非空则使用传入的客户端,否则回退到 host/port/TLS/apiKey 方式新建客户端(见 QdrantEmbeddingStore.java)。
常用操作:写入、搜索与删除
QdrantEmbeddingStore实现了EmbeddingStore<TextSegment>的全部标准操作,可直接与 LangChain4j 的EmbeddingModel(如各类 embedding 模型)配合使用。
写入 embedding
// 添加单个 embedding(仅向量) String id = store.add(embedding); // 添加 embedding + 文本片段(文本与其 Metadata 一并写入 payload) String id = store.add(embedding, textSegment); // 批量添加(可同时指定 id、embedding、textSegment 三个列表) List<String> ids = store.addAll(ids, embeddings, textSegments);源码中addAll的写入逻辑(见 QdrantEmbeddingStore.java)值得关注:
- 先通过
ensureConsistentSizes校验三个列表长度一致,空 embedding 列表直接返回; - 每个条目构建一个
PointStruct,向量通过vectors(embedding.vector())写入; - 若提供
TextSegment,其Metadata经ValueMapFactory.valueMap()转为 Qdrant payload,同时把文本内容放入payloadTextKey指定的字段; - 最后调用
client.upsertAsync(collectionName, points).get()异步批量写入并阻塞等待结果。
点 ID 的两种形式:写入时若未指定 ID 会生成随机 UUID;指定 ID 时,toPointId会先尝试按无符号长整型(Long.parseUnsignedLong)解析,失败则按 UUID 解析(见 QdrantEmbeddingStore.java)。因此同时支持整数 ID 与 UUID 两种点 ID,测试 QdrantEmbeddingStoreIT.java 中专门用"42"验证了整数 ID 的写入与检索。
相似性搜索
EmbeddingSearchResult<TextSegment> result = store.search( EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(5) .minScore(0.7) .build()); for (EmbeddingMatch<TextSegment> match : result.matches()) { String id = match.embeddingId(); double score = match.score(); TextSegment segment = match.embedded(); String text = segment.text(); Metadata metadata = segment.metadata(); }搜索实现(见 QdrantEmbeddingStore.java)的关键流程:
- 构造
QueryPoints,使用QueryFactory.nearest(...)执行最近邻查询,开启返回向量与 payload,limit取request.maxResults(); - 若请求携带过滤条件,则通过
QdrantFilterConverter.convertExpression转为 Qdrant 的Filter附加到查询上; - 对返回的每个
ScoredPoint,从 payload 中还原TextSegment与Metadata,并基于余弦相似度重新计算相关度分数:RelevanceScore.fromCosineSimilarity(CosineSimilarity.between(embedding, referenceEmbedding)); - 过滤掉低于
minScore的结果,按分数降序返回。
也就是说,最终返回给调用方的score是 LangChain4j 统一的相关度分数(由余弦相似度换算),而非 Qdrant 原始的距离值,这保证了跨向量库 API 的一致性。
删除操作
// 按 ID 删除 store.remove(id); // 按多个 ID 批量删除 store.removeAll(ids); // 按过滤条件删除(元数据过滤,见下一节) store.removeAll(filter); // 清空整个集合 store.removeAll(); // 等价于 store.clearStore()其中clearStore()使用空Filter选中全部点并执行删除(见 QdrantEmbeddingStore.java),集成测试 QdrantEmbeddingStoreWithRemovalIT.java 专门覆盖了全部删除场景,每个用例前都会清空并断言存储为空。
释放资源
使用完毕后调用store.close()关闭底层 gRPC 客户端(对应源码中的client.close())。
元数据过滤:Filter 与 Qdrant 条件的映射
LangChain4j 的通用Filter表达式由 QdrantFilterConverter.java 转换为 Qdrant 原生条件。该转换器支持以下过滤器类型:
逻辑组合
And→ Qdrantmust子句Or→ Qdrantshould子句Not→ Qdrantmust_not子句
比较条件(映射到 Qdrant Condition)
| LangChain4j 过滤器 | 支持的比较值类型 | Qdrant 底层实现 |
|---|---|---|
ContainsString | String | matchText全文匹配 |
IsEqualTo | String / UUID / Boolean / Integer / Long / Float / Double | 字符串与布尔用match/matchKeyword;整数用match(key, long);浮点用range且gte == lte == value(因 Qdrant Match 协议无浮点字段) |
IsNotEqualTo | 同上 | 对上述条件取must_not |
IsGreaterThan/IsGreaterThanOrEqualTo/IsLessThan/IsLessThanOrEqualTo | Number | Range的gt/gte/lt/lte |
IsIn | String / UUID / Integer / Long | matchKeywords或matchValues |
IsNotIn | String / UUID / Integer / Long | matchExceptKeywords或matchExceptValues |
使用示例:
import static dev.langchain4j.store.embedding.filter.Filter.and; import static dev.langchain4j.store.embedding.filter.Filter.eq; Filter filter = and( eq("category", "java"), eq("year", 2024) ); EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .filter(filter) .maxResults(5) .build(); EmbeddingSearchResult<TextSegment> result = store.search(request);注意事项(源码注释与测试均明确体现,见 QdrantEmbeddingStoreIT.java):
IsEqualTo/IsNotEqualTo/IsIn/IsNotIn不支持浮点值,只支持字符串与整数;- 大小比较条件只接受数值;
- 对
IsNotIn而言,如果元数据中不存在该 key,则该条不会命中; - 未识别的过滤器类型会抛出
UnsupportedOperationException,不支持的比较值类型会抛出IllegalArgumentException或RuntimeException。
底层实现:元数据 payload 的序列化与反序列化
Qdrant 以 JSON 形式在 payload 中保存附加数据,模块通过两个工具类完成与 LangChain4jMetadata的双向转换:
- ValueMapFactory.java:写入方向。把
Metadata.toMap()的 Java 对象转为 Qdrant 的JsonWithInt.Value,支持String、Integer、Long、Double、Float、Boolean、UUID(转为字符串)、null、数组与嵌套 Map。注意Float在写入时经ValueFactory.value(Float)提升为 double 存储,这也是过滤器对浮点相等比较用range并统一按doubleValue()换算的原因(见 QdrantFilterConverter.java)。不支持的 Java 类型会抛出IllegalArgumentException。 - ObjectFactory.java:读取方向。把 Qdrant payload 的
Value还原为 Java 对象,支持整数、字符串、double、布尔、列表、嵌套结构与 null。
读取时,toEmbeddingMatch会把payloadTextKey字段单独取出作为TextSegment的文本,其余字段全部还原为Metadata(见 QdrantEmbeddingStore.java)。因此写入时放入 Metadata 的任意字段,检索时都会原样返回,便于下游 RAG 组装引用信息。
测试与验证:Testcontainers 驱动的集成测试
模块内置了完整的测试套件,可作为自行搭建开发环境的参考:
- QdrantEmbeddingStoreIT.java:基于
org.testcontainers.qdrant.QdrantContainer拉起qdrant/qdrant:latest容器,使用AllMiniLmL6V2QuantizedEmbeddingModel生成 embedding,覆盖增删查与全部元数据过滤场景; - QdrantEmbeddingStoreWithRemovalIT.java:专门验证各删除路径;
- QdrantEmbeddingStoreContractTest.java:实现
EmbeddingStoreAddAllContract与EmbeddingStoreRemoveAllContract,用 Mockito 验证addAll、removeAll的契约行为; - QdrantFilterConverterTest.java:纯单元测试,验证过滤器到 Qdrant 条件的转换。
测试中还揭示了集合创建的关键前提:必须先创建 Qdrant 集合,且向量维度要与 embedding 模型维度一致,距离度量使用Distance.Cosine(见 QdrantEmbeddingStoreIT.java)。这一点对自建集合的读者尤为重要——QdrantEmbeddingStore本身不会自动建集,向量维度、度量方式(建议 Cosine)都需在 Qdrant 侧通过 gRPC/HTTP API 或 Dashboard 预先配置,且要与所用 embedding 模型的输出维度严格对齐。
使用前提与注意事项小结
- 端口与协议:模块使用 Qdrant 的 gRPC 接口,默认端口为 6334(非 HTTP 的 6333),
useTls需与 Qdrant 服务端 TLS 配置保持一致; - 集合预创建:写入前须先在 Qdrant 中创建好集合,向量维度须与 embedding 模型一致,度量建议使用 Cosine;
- 点 ID 兼容:ID 支持无符号长整型与 UUID 两种形式,二者在检索结果中会按原类型还原;
- Metadata 类型约束:payload 字段只支持基本类型、数组与嵌套 Map,
UUID会被存储为字符串,不受支持的类型会在写入时报错; - 过滤能力边界:浮点值不支持等值与 In/NotIn 过滤,
IsNotIn对缺失 key 的条目不命中,编写过滤条件时需留意。
结合以上内容,你可以在 LangChain4j 应用中把 Qdrant 作为持久化的向量存储:通过 Builder 一行完成连接配置,借助标准EmbeddingStoreAPI 实现写入、相似检索与元数据过滤,并利用 Testcontainers 在本地快速复现测试环境。
【免费下载链接】langchain4jLangChain4j is an idiomatic, open-source Java library for building LLM-powered applications on the JVM. It offers a unified API over popular LLM providers and vector stores, and makes implementing tool calling (including MCP support), agents and RAG easy. It integrates seamlessly with enterprise Java frameworks like Quarkus and Spring Boot.项目地址: https://gitcode.com/GitHub_Trending/la/langchain4j
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考