别把向量检索塞进应用进程:用 Qdrant 在本地建立可筛选的检索服务
把 RAG、相似内容推荐或 Agent 记忆做进第一个原型时,很多人会先在应用进程里加载 embedding、把向量放进列表,然后用一次余弦相似度循环完成检索。这个办法足以验证想法,却很难自然演进成服务:数据重启即丢、过滤逻辑散落在业务代码里、索引与权限边界不清晰,也没有适合排查的 HTTP 接口。
Qdrant 提供了另一种分层方式。它是用 Rust 编写的向量数据库与相似度搜索引擎,服务端保存的是 point:一个向量、一个唯一 ID,以及可选 payload。向量负责“像不像”,payload 负责“是不是当前租户、文档类型、时间范围或权限集合里的对象”。这使它特别适合语义检索和推荐场景:先由业务侧生成 embedding,再把相似性和结构化过滤交给独立服务完成。
本文不把 Qdrant 当成“装上就自动得到 RAG”的黑盒,而是搭一个本地、可持久化、可验证的最小检索服务,并说明其中真正需要由应用负责的部分。
先确定边界:向量库不负责生成 embedding
Qdrant 接收、存储和搜索向量,但不替你决定文本切分策略,也不提供某个模型的 embedding。把一篇文档切成多大、是否保留标题层级、使用哪种模型、向量维度是多少,仍是上游管道的职责。
这一区分会直接影响接口设计。集合(collection)创建时要声明向量大小和距离度量;随后写入的每个向量必须符合该大小。换模型后若维度不同,不能悄悄把新向量混进旧集合。更稳妥的做法是新建集合或明确完成迁移,再让应用切换读取目标。
payload 同样不是“随便塞一段 JSON”。它应该承载检索时会用到的业务字段,例如 tenant_id、source、language、published_at 或访问级别。这样,过滤在搜索请求中完成,而不是先取回一大批相似结果、再由应用丢弃越权或不匹配的数据。
用 Docker 启动一个有持久化目录的本地服务
Qdrant 官方快速开始给出了以下容器启动方式。6333 暴露 REST API 和 Web UI,6334 暴露 gRPC;挂载目录让容器重建后仍保留数据。
mkdir -p qdrant-local cd qdrant-local docker run -p 6333:6333 -p 6334:6334 \ -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \ qdrant/qdrant
启动后,先在另一个终端验证服务而不是立刻接入应用:
curl http://localhost:6333/collections
浏览器也可以打开 http://localhost:6333/dashboard 查看服务状态。这里有两个容易忽略的运维细节。第一,示例中的 :z 是 Docker 挂载选项,官方示例用于 SELinux 标签处理;在不需要该语义的环境中,应按本机 Docker/SELinux 策略调整,而不是盲目复制。第二,教程中的浮动镜像标签适合快速试验;生产部署应固定已验证的镜像版本或不可变 digest,并纳入镜像更新流程。
用最小数据验证“向量 + payload”这个模型
官方 Python 客户端的接口能清楚展示集合定义。下面示例创建一个四维、使用点积距离的集合;它不是现实 embedding 的维度,而是为了把数据模型和请求路径缩小到可检查的范围。先安装客户端:
python3 -m pip install qdrant-client
然后创建集合并写入两条带租户字段的数据:
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, PointStruct, VectorParams
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="notes_demo",
vectors_config=VectorParams(size=4, distance=Distance.DOT),
)
client.upsert(
collection_name="notes_demo",
wait=True,
points=[
PointStruct(id=1, vector=[0.05, 0.61, 0.76, 0.74],
payload={"tenant_id": "team-a", "title": "部署记录"}),
PointStruct(id=2, vector=[0.19, 0.81, 0.75, 0.11],
payload={"tenant_id": "team-b", "title": "测试笔记"}),
],
)
wait=True 的意义是让这次写入在返回前完成,便于本地实验按顺序验证。真实批量导入时,应结合吞吐、重试和幂等 ID 策略评估是否始终等待。更重要的是,示例中的向量只是数字:它们不能证明检索质量。质量取决于上游模型、切分、清洗、查询改写和评测集,而不是数据库容器是否成功启动。
让数据契约先于“能搜到结果”
在真正接入业务前,建议把集合当作一份明确的数据契约,而不是一个随时可以追加字段的桶。至少记录四项:集合名与用途、向量维度、距离度量、payload 字段含义。例如 notes_demo_v1 可以约定 tenant_id 必填且为字符串,title 只是展示字段;若后续需要按来源、时间或语言筛选,再明确字段类型和由谁写入。这样排查“为什么搜不到”时,能先区分是查询向量不匹配、过滤条件过窄,还是上游根本没有写入对应 payload。
point ID 也应由上游稳定地产生。用文档片段 ID、数据库主键加片段序号,或其他可重复计算的标识,通常比每次导入随机生成 ID 更适合重跑任务:同一片段再次写入可以更新对应 point,而不是不断制造副本。删除或重新切分文档时,也能据此定位应当清理的数据。具体批量写入、删除与过滤语法应跟随所用客户端版本的官方 API 文档;不要仅凭本地示例推断生产接口。
另一个常见误区是把“距离度量”看成可随意替换的开关。点积、余弦等度量如何适用,取决于 embedding 模型的训练与输出约定。创建集合前先阅读模型提供方的检索建议;已经写入数据的集合若要切换度量,不应只改一行配置并期待历史索引自动变成新语义。以新集合重建、抽样比较检索结果,再切换流量,通常更可控。
开发阶段还应留下最小的健康检查与数据检查。GET /collections 能确认服务可响应;在导入后再检查目标集合是否存在、写入数量是否符合预期。它们不能替代备份恢复演练,却能把“容器没起来”“写错地址”“集合名拼错”这些基础故障从模型效果问题中分离出来。
过滤必须成为检索请求的一部分
多租户或有访问边界的知识库,最危险的设计是“先按相似度取 Top K,再在应用层过滤”。如果业务代码某处忘记补过滤条件,结果就可能跨越本不该读取的范围。
Qdrant 的设计重点之一是为向量 point 附带 payload,并支持扩展过滤。因此可将 tenant_id、来源类型等约束和相似度检索一起表达。具体过滤条件、字段类型与索引策略应以当前 API/客户端文档为准;不要因为 payload 看起来像普通 JSON,就把它当作已经实现了认证授权。Qdrant 能按你给出的条件检索,谁有资格给出某个租户条件、API 是否暴露到公网、网络层是否有鉴权,仍必须由部署与应用架构保证。
一个实用的职责切分是:应用服务完成身份验证并推导可信的 tenant_id;应用生成查询 embedding;应用将该 ID 作为不可被前端任意覆盖的过滤条件提交;Qdrant 返回符合过滤条件的近邻 point。这样能把“相似度算法”和“业务授权”连接起来,但不会误称向量库本身是完整权限系统。
从实验走向可维护服务时,优先补这四件事
持久化与备份。 容器并不等于数据安全。确认宿主机挂载目录位于受管理的磁盘上,为它设计备份、恢复演练与容量监控;不要把唯一数据留在临时容器层。
版本与迁移。 embedding 模型、距离度量、切分规则和 payload schema 都是数据契约。给集合命名加入版本或迁移阶段,比静默复用旧集合更容易回滚和审计。
网络边界。 本地端口适合开发,但生产环境不应把数据库管理接口直接暴露到互联网。将服务置于私有网络,明确应用到 Qdrant 的访问路径,并依据官方部署文档配置认证、TLS 或反向代理等外围控制。
检索评测。 选几十到几百条真实查询,记录目标文档是否进入前几名,再比较不同模型、chunk 大小和过滤条件。只有这样才能判断系统是“容器能跑”,还是“读者真的能找到答案”。
Qdrant 的价值不在于消灭 RAG 工程,而在于把向量检索、payload 和过滤从应用内循环中抽成一个可独立运行的服务。先用本地 Docker 验证数据路径,再把 embedding、权限、版本迁移和评测当作同等重要的工程工作,才能避免把一个演示脚本直接带进生产。