三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Java RAG 实战(第 7 篇):使用 Qdrant 保存和检索向量

Java RAG 实战(第 7 篇):使用 Qdrant 保存和检索向量

系列导航

  • 所属专栏:《Java 开发者从零实现 RAG 知识库》
  • 学习位置:第 7 篇 / 共 12 篇
  • 上一篇:《第6篇:Markdown 知识库与 RAG》
  • 下一篇:《第8篇:Spring Boot RAG 查询 API》

上一篇已经用内存检索跑通 RAG。本篇把 Chunk 向量预先保存到 Qdrant,并通过官方 Java SDK 完成 Top-K 搜索。

完成进度

  • 1. 用 Docker 启动 Qdrant
  • 2. 创建 1024 维 Cosine Collection
  • 3. 理解 Point、Vector 和 payload
  • 4. 把 Markdown Chunk 写入 Qdrant
  • 5. 使用 Java SDK 完成 Top-K 查询
  • 6. 验证相似度阈值会拦截无关资料

为什么要学

内存版每次运行都要重新读取所有 Chunk 并生成全部向量,知识量增加后会浪费时间和计算资源。Qdrant 可以持久化 Point,并使用专门的向量索引快速查找相似内容。

引入 Qdrant 后,查询阶段只需要向量化用户问题,再从数据库取回相关 Chunk。它负责保存和检索知识,最终答案仍由 qwen3 根据 Prompt 生成。

第 1 步:启动 Qdrant

第一次启动:

dockerrun-d\--nameqdrant-study\-p6333:6333\-p6334:6334\-vqdrant-study-data:/qdrant/storage\qdrant/qdrant:v1.19.0

以后容器停止后只需执行:

dockerstart qdrant-study

这里的两个端口用途不同:

  • 6333:REST API,适合使用curl查看状态。
  • 6334:gRPC,当前 Java 官方 SDK 通过它连接 Qdrant。

确认服务正常:

curl-shttp://localhost:6333/collections|jq

第 2 步:创建 Collection

bge-m3生成 1024 维向量,所以 Collection 的向量维度也必须是 1024:

curl-XPUT http://localhost:6333/collections/kubernetes_chunks\-H'Content-Type: application/json'\-d'{ "vectors": { "size": 1024, "distance": "Cosine" } }'

检查配置:

curl-shttp://localhost:6333/collections/kubernetes_chunks|jq'{ status: .result.status, points_count: .result.points_count, vector_size: .result.config.params.vectors.size, distance: .result.config.params.vectors.distance }'

第 3 步:理解 Point 和 payload

Qdrant 中的一条 Point 包含三部分:

Point ID:唯一编号,例如 2 Vector: bge-m3 生成的 1024 个数字 Payload: title、content、source 等原始资料

Vector 用来计算相似度,payload 用来把命中结果还原成人能阅读的知识。RAG 最终交给大模型的是 payload 中的正文,不是 1024 个数字。

本篇的固定示例使用简单数字 ID 方便观察。后面动态入库时,会改用documentId + chunkIndex生成稳定 UUID,避免更新文档时不断产生重复 Point。

第 4 步:把 Markdown 写入 Qdrant

在仓库根目录运行:

mvn-f04-qdrant-rag/pom.xml compile exec:java\-Dexec.args=--index

入库流程是:

  1. MarkdownKnowledgeLoader按二级标题拆分文档。
  2. OllamaEmbeddingClientbge-m3生成 Chunk 向量。
  3. QdrantPointMapper把向量、标题、正文和来源组成 Point。
  4. QdrantIndexer通过官方 SDK 的 gRPC 接口执行 upsert。

检查 Point 数量:

curl-shttp://localhost:6333/collections/kubernetes_chunks|\jq'.result.points_count'

当前示例应输出4

第 5 步:理解搜索代码

查询阶段不再读取并重新向量化全部 Markdown,只生成用户问题的一个向量:

QdrantVectorSearch.search(...) ↓ EmbeddingClient.embed(List.of(query)) ↓ QdrantSearchMapper.toRequest(...) ↓ QdrantClient.searchAsync(...) ↓ QdrantSearchMapper.toResult(...)

搜索请求中的关键参数:

  • limit = 2:最多返回两个结果,也就是 Top-2。
  • scoreThreshold = 0.60:低于 0.60 的结果不返回。
  • withPayload = true:要求结果带回标题、正文和来源。

Qdrant 已经按相似度从高到低返回结果,Java 不需要再自己遍历所有向量计算余弦相似度。

对应源码:

04-qdrant-rag/src/main/java/com/example/ai/rag/QdrantVectorSearch.java 04-qdrant-rag/src/main/java/com/example/ai/rag/QdrantSearchMapper.java

查询方法的核心只有三步:

double[]queryEmbedding=embeddingClient.embed(List.of(query)).get(0);SearchPointsrequest=mapper.toRequest(collectionName,queryEmbedding,limit,minimumScore);List<ScoredPoint>points=searchOperation.search(request);returnpoints.stream().map(mapper::toResult).toList();

mapper.toRequest(...)会把教程中的三个检索要求真正写入 Qdrant SDK 请求:

returnSearchPoints.newBuilder().setCollectionName(collectionName).addAllVector(vector).setLimit(limit).setScoreThreshold((float)minimumScore).setWithPayload(WithPayloadSelector.newBuilder().setEnable(true).build()).build();

withPayload = true不能省略。如果只返回 Point ID 和分数,Java 就无法取回要放入 Prompt 的标题和正文。

第 6 步:运行 Qdrant 版完整 RAG

确认 Ollama 和 Qdrant 已启动,然后执行:

mvn-f04-qdrant-rag/pom.xml compile exec:java

知识库内问题的示例输出:

问题:Pod 重建后 IP 会变化,应该怎样提供稳定访问地址? Qdrant 检索结果(最低相似度 0.60): 1. Service,相似度 0.7388,Point ID 2 AI 回答: 可以通过 Service 为 Pod 提供稳定的集群内访问地址和负载均衡。

也可以传入自己的问题:

mvn-f04-qdrant-rag/pom.xml compile exec:java\-Dexec.args='密码、令牌和证书应该保存在哪里?'

第 7 步:观察阈值保护

查询知识库没有覆盖的内容:

mvn-f04-qdrant-rag/pom.xml compile exec:java\-Dexec.args='Ingress 应该怎样配置 TLS 证书?'

如果没有结果达到0.60,程序不会调用聊天模型,而是直接输出:

根据现有资料无法确定。

这可以减少无关上下文和不必要的模型调用,但阈值不是越高越好。过高会漏掉有用资料,过低会把无关资料交给模型,需要根据真实问题和知识库评估后调整。

第 8 步:运行 Qdrant 模块测试

mvn-f04-qdrant-rag/pom.xmltest

测试会隔离外部 Ollama 和 Qdrant,重点验证:

  • double[]是否正确转换成 Qdrant 的 float 向量。
  • Collection、Top-K、阈值和 payload 开关是否正确。
  • Qdrant 返回值是否正确还原成 Chunk。
  • 搜索过程是否只向量化用户问题。

常见问题

Connection refused: localhost:6334

Qdrant 没有启动,执行:

dockerstart qdrant-studydockerps--filtername=qdrant-study

Collection 不存在

先执行“第 2 步”创建kubernetes_chunks,然后再运行入库程序。

Collection 是空的

Collection 只定义了存储结构,还没有资料。使用“第 4 步”的--index模式完成入库。

向量维度错误

Embedding 模型与 Collection 配置不一致。当前bge-m3输出 1024 维,因此 Collection 的size必须是 1024。

成功标准

  • kubernetes_chunks状态为green,并且有 4 个 Point。
  • 03-markdown-rag能使用内存检索完成一次 RAG 回答。
  • 默认问题能检索到Service
  • 知识库外问题不会调用qwen3:14b
  • mvn -f 04-qdrant-rag/pom.xml test全部通过。

下一步

下一阶段可以把命令行程序改造成 Spring Boot HTTP 服务,让浏览器或其他应用通过接口提交问题并获得 RAG 回答。

本篇自测

  1. Qdrant Point 中的 Vector 和 payload 分别有什么用?
  2. 查询时为什么只需要向量化用户问题?
  3. Top-K 和相似度阈值有什么区别?
  4. Java SDK 为什么连接 6334,而curl通常访问 6333?

参考答案:Vector 用于相似度计算,payload 用于还原正文;知识向量已经在入库时保存;Top-K 限制最多返回数,阈值排除低分结果;6334 是 gRPC,6333 是 REST。


本篇小结

  1. Qdrant 预先保存 Chunk 向量,查询时无需重新向量化全部知识。
  2. Collection 维度必须和 Embedding 模型一致,bge-m3对应 1024 维。
  3. 一条 Point 由 ID、Vector 和 payload 构成,Java 通过官方 SDK 执行写入和搜索。
  4. Top-K 与最低相似度共同控制进入 Prompt 的资料数量和质量。

下一篇

👉 本专栏下一篇:《第8篇:Spring Boot RAG 查询 API》

完整代码都在 GitHub(欢迎 Star ⭐)

本专栏的全部示例代码都已开源,包含 5 个可独立运行的 Maven 模块、自动化测试和完整分篇教程。建议Fork / Clone下来,边读边跑:

🔗 https://github.com/bysbsh/ai-rag-learning-guide

  • 代码与教程同步更新,对照每一篇动手实践效果最好。
  • 如果这份教程帮到了你,点个Star就是对我最大的支持,也方便你之后找回最新版本。
  • 遇到问题或发现错漏,欢迎在仓库提 Issue / PR。项目采用 MIT 协议,可自由学习与二次创作。
← 返回列表