登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

向量检索先做元数据过滤再召回的查询链路设计

来源:17golang原创

时间:2026-09-20 08:14:46 183浏览 收藏

向量检索同时面对“语义相似”和“用户能不能看”两个问题。更稳妥的链路是:先把租户、空间、权限范围等条件作为结构化元数据过滤器,和向量 query 一起交给检索引擎,再在满足授权边界的候选中按相似度排序。不要先取全局 top-k,再在应用层删掉无权文档;这样既可能只剩很少结果,也容易把权限控制放在过晚的位置。

本文用 Qdrant 的 payload/filter 作为具体示例。官方过滤文档地址:https://qdrant.tech/documentation/search/filtering/。其他向量数据库虽然 API 名称不同,但都可以套用“元数据建模—过滤索引—过滤与向量查询同边界—结果验收”的思路。

要点速览
  • tenant_id、acl_scope、language 等字段要和正文 embedding 分开保存,并使用稳定类型。
  • 权限条件应进入向量查询的 filter,而不是拿到 top-k 后才由业务代码补过滤。
  • 过滤后没有结果时,先区分授权集合为空、相似度不够和字段类型错误,再决定是否调整召回参数。

一、先把租户与权限字段设计成可过滤元数据

一条向量记录至少可以拆成三部分:用于相似度计算的 embedding、用于返回展示的正文或引用信息、用于缩小候选集合的 metadata。租户和权限字段不要只拼进文本再重新向量化,因为“tenant-a”是否可见不是语义相似度能可靠表达的条件。

字段用途建议类型常见边界
tenant_id租户隔离keyword/string不能为空,不用展示名代替稳定 ID
acl_scope空间或权限范围keyword/数组明确数组是“任一命中”还是“全部满足”
language语言或内容路由keyword统一大小写和缺省值
updated_at时间范围过滤日期/整数统一时区与精度
向量检索中 tenant_id、acl_scope 元数据过滤与 embedding 相似度召回的静态结构说明图
图1:向量检索的静态结构说明图,展示租户与权限元数据如何在相似度召回前限定候选边界。

以 Qdrant 为例,payload index 应建在经常过滤的字段上;精确匹配的租户 ID、标签和类别适合 keyword 类型,时间范围则应使用数值或日期类型。字段名、类型和缺省值一旦确定,写入端与查询端必须共用同一套约定。

二、把过滤条件放进向量查询边界

查询层可以先把业务身份转换成不可变的过滤条件,再把过滤条件和向量一起发送。下面的 JSON 是一个可迁移的请求形状:must 表示必须满足的约束,query 是问题向量,排序只发生在过滤后的候选范围内。

{
  "query": [0.12, -0.08, 0.44, 0.31],
  "filter": {
    "must": [
      {"key": "tenant_id", "match": {"value": "tenant-a"}},
      {"key": "acl_scope", "match": {"any": ["finance-read", "owner"]}}
    ]
  },
  "limit": 8,
  "with_payload": ["document_id", "title", "updated_at"]
}

真实项目中不要让前端直接传入 tenant_id 或权限范围。服务端应从登录态、授权服务或请求上下文生成这些值,并限制可查询的字段集合。向量库的 filter 是召回边界的一部分,应用层仍要在最终响应前做一次轻量的权限一致性检查,但它不应承担首次隔离。

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

def search_documents(embedding, tenant_id, scopes):
    # 租户身份由服务端上下文传入,不能信任客户端自由修改。
    must = [models.FieldCondition(
        key="tenant_id",
        match=models.MatchValue(value=tenant_id),
    )]
    if scopes:
        # 只有授权服务确认过的范围才进入向量查询过滤器。
        must.append(models.FieldCondition(
            key="acl_scope",
            match=models.MatchAny(any=list(scopes)),
        ))
    return client.query_points(
        collection_name="knowledge_base",
        query=embedding,
        query_filter=models.Filter(must=must),
        limit=8,
        with_payload=["document_id", "title", "updated_at"],
    )

三、用候选数量和相似度阈值控制结果质量

过滤后结果变少,不等于向量模型失效。至少要把三种情况分开记录:授权集合本来为空、集合有数据但相似度低、查询字段没有按预期匹配。只有第一种需要回到权限或数据同步链路排查;第二种可以评估 embedding、score_threshold 或用户问题改写;第三种通常是字段类型、大小写、数组语义或索引配置问题。

不要简单把全局 limit=8 改成 100 来弥补后过滤。若后端支持在向量查询中应用 filter,应优先使用原生过滤;若某个系统只能先召回再过滤,则应明确标记为降级路径,采用受控的过采样、授权复核和结果不足提示,并设置上限避免查询成本失控。

向量检索中预过滤召回与应用层后过滤的候选数量和权限边界对比说明图
图2:结果质量说明图,对比同一授权边界内召回与拿到全局 top-k 后再删除结果的差异。

四、用隔离与性能清单验收查询链路

上线前可以用一组固定数据做回归:同一语义问题分别由两个租户查询;同一租户切换权限范围;再把某个过滤字段改成错误类型,确认系统能观察到异常。验收关注的不是“总能返回 8 条”,而是结果是否只来自允许集合、过滤字段是否可解释、延迟是否随着过滤选择性变化而可控。

检查项通过标准
租户隔离tenant-a 的结果集合不出现 tenant-b 的文档 ID
权限变更撤销 scope 后新查询不再返回旧范围文档
索引覆盖高频过滤字段有对应 payload/metadata index
空结果诊断日志能区分无授权候选、低分和字段匹配失败
降级控制后过滤路径有明确上限、告警和最终授权复核

这条链路的核心取舍是:让结构化条件负责“能不能进入候选集合”,让 embedding 负责“在候选里哪个更相似”。两者职责清楚,召回数量、延迟和权限风险才有可观测的调节空间。

常见问题

过滤字段为什么不能只写进文档正文?

正文 embedding 表达语义,不适合承担严格的租户和权限判断。结构化字段才能做精确匹配、索引和审计。

过滤后没有结果,应该先调大 top-k 吗?

先查授权集合是否为空、字段类型是否一致、索引是否可用,再判断相似度阈值。盲目增大 top-k 不能修复错误的权限条件。

应用层还需要做权限检查吗?

需要保留最终一致性检查,但它是防线和审计点,不应替代向量查询阶段的元数据过滤。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>