4. 查询边界概念:查询上下文与过滤上下文、评分与不评分查询的区别

在构建具备权限过滤与字段级安全的搜索系统时,理解查询的“边界”是核心前提。Elasticsearch 等现代搜索引擎将查询执行环境严格划分为两种上下文:查询上下文(Query Context)与过滤上下文(Filter Context)。这两种上下文决定了查询是否参与评分、是否利用缓存、以及如何影响最终结果集。本章将深入剖析这两种上下文的本质区别,并阐明评分与不评分查询在实际权限过滤中的关键作用。

核心原则:查询上下文用于“相关性匹配”,过滤上下文用于“精确筛选”。权限过滤必须部署在过滤上下文中,以避免安全规则干扰相关性评分,同时利用缓存提升性能。

4.1 查询上下文(Query Context)

查询上下文是搜索引擎计算文档与查询之间相关性分数(_score)的环境。在此上下文中执行的查询会分析词频(TF)、逆文档频率(IDF)、字段长度归一化(Field-length Norm)等因子,为每个匹配文档生成一个浮点数分数,并按分数降序排列结果。

  • 评分行为:每个匹配文档都会获得一个 _score,用于排序。
  • 性能特征:计算开销较大,因为需要遍历倒排索引并执行评分算法(如 BM25)。
  • 缓存行为:查询上下文的结果通常不缓存,因为评分依赖查询词项,变化频繁。
  • 典型场景:全文搜索、关键词匹配、模糊查询——用户期望“最相关”的结果排在前面。

4.2 过滤上下文(Filter Context)

过滤上下文是一个“是/否”的二元决策环境。它只判断文档是否满足条件,不计算相关性分数。所有匹配文档的 _score 统一为 0(或在复合查询中被忽略)。

  • 不评分行为:不计算 _score,仅返回布尔匹配结果。
  • 性能特征:计算开销小,因为无需执行评分算法。
  • 缓存行为:结果可被节点级缓存(如 Elasticsearch 的 Request Cache 或 Filter Cache)高效缓存,因为过滤条件通常是静态或半静态的(如角色、权限标签)。
  • 典型场景:权限过滤、日期范围限定、状态筛选、字段级可见性控制。
架构建议:在实现字段级安全时,将用户角色、部门 ID、安全标签等元数据作为过滤条件放入 filter 子句中。这既能保证安全规则不被评分逻辑绕过,又能利用缓存加速重复查询。

4.3 评分与不评分查询的对比

对比维度 评分查询(Query Context) 不评分查询(Filter Context)
核心目标 相关性排序 精确筛选
输出分数 浮点数 _score(如 2.345) 无 _score(或固定为 0)
计算开销 高(TF/IDF/BM25) 低(布尔匹配)
缓存能力 通常不缓存 可缓存,命中率高
适用操作 match, match_phrase, query_string term, terms, range, exists, prefix
权限过滤 不推荐(会污染评分) 推荐(安全且高效)

4.4 查询上下文与过滤上下文的协作流程

在实际搜索请求中,两种上下文通常协同工作。一个典型的权限感知查询结构如下:

{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "机密报告" } }   // 查询上下文:评分
      ],
      "filter": [
        { "term": { "security_level": "L3" } }, // 过滤上下文:不评分
        { "term": { "department": "研发部" } }  // 过滤上下文:不评分
      ]
    }
  }
}

执行流程:

  1. 过滤阶段:首先执行 filter 子句,快速缩小候选集(仅保留 security_level 为 L3 且 department 为研发部的文档)。
  2. 评分阶段:在过滤后的子集上执行 must 中的评分查询,计算相关性分数。
  3. 合并输出:最终结果按 _score 排序,且已自动满足权限约束。
常见错误:将权限过滤条件放在 must 或 should 子句中。这会导致安全规则参与评分,可能使未授权文档因词频巧合获得高分而排在前面,造成权限绕过风险。始终将安全过滤条件放在 filter 子句中。

4.5 两种上下文的边界与性能影响

理解两种上下文的边界对于设计高性能搜索架构至关重要。以下 SVG 图展示了查询执行引擎内部如何划分这两种上下文,以及数据流的走向。

查询上下文与过滤上下文执行流程 查询上下文与过滤上下文执行流程 用户搜索请求 查询解析器(Bool Query) 过滤上下文 不评分 · 可缓存 · 布尔匹配 term: security_level=L3 term: department=研发部 查询上下文 评分 · 不缓存 · BM25 match: title=机密报告 结果合并与排序输出

4.6 字段级安全中的上下文选择策略

在字段级安全(Field-Level Security, FLS)实现中,查询边界的选择直接影响安全粒度和系统性能。以下策略是业界最佳实践:

  • 行级权限(文档级):使用过滤上下文中的 terms 查询,匹配用户所属的角色 ID 或安全组。例如:{"terms": {"allowed_roles": ["admin", "analyst"]}}。
  • 字段级可见性:在查询结果返回阶段,通过 _source 过滤或 fields 参数限制字段输出。这属于“响应边界”,而非查询上下文,但需与过滤上下文配合。
  • 字段值级过滤:如果某字段的值本身包含安全分类(如 salary_range 字段),则使用过滤上下文中的 range 查询限制可见范围。
性能数据参考:在生产环境中,将权限过滤从查询上下文迁移到过滤上下文后,缓存命中率可从 15% 提升至 85% 以上,查询延迟降低 40%-60%。对于包含 5000 万文档的索引,过滤上下文可在 5ms 内完成权限筛选,而同等条件下的评分查询需要 80-120ms。

4.7 复合查询中的上下文嵌套规则

Elasticsearch 的 bool 查询是两种上下文协作的典型容器。理解其嵌套规则是避免安全漏洞的关键:

bool 子句 上下文类型 行为说明
must 查询上下文 文档必须匹配,且参与评分
should 查询上下文 文档匹配则加分,用于提升相关性
filter 过滤上下文 文档必须匹配,不评分,结果可缓存
must_not 过滤上下文 文档必须不匹配,不评分,结果可缓存
安全陷阱:切勿在 must_not 中放置权限排除逻辑时使用评分查询。例如,{"must_not": {"match": {"blocked_users": "user123"}}} 虽然能工作,但会绕过缓存且增加不必要的评分开销。应改为 {"must_not": {"term": {"blocked_users": "user123"}}}。

以下 SVG 图展示了在复合查询中,两种上下文如何嵌套与协作,形成完整的查询边界:

复合查询中的上下文嵌套结构 复合查询中的上下文嵌套结构 Bool Query(根) must 查询上下文 · 评分 should 查询上下文 · 加分 filter 过滤上下文 · 不评分 must_not 过滤上下文 · 不评分 match: title=报告 match: content=紧急 term: role=admin term: status=deleted 安全边界说明 权限过滤(role=admin)放在 filter 中,不评分且可缓存 排除逻辑(status=deleted)放在 must_not 中,同样不评分

4.8 小结:查询边界的设计原则

在实现搜索索引的权限过滤与字段级安全时,遵循以下查询边界设计原则可确保系统既安全又高效:

  1. 分离关注点:相关性评分与权限筛选必须使用不同的上下文。评分任务交给查询上下文,权限任务交给过滤上下文。
  2. 缓存优先:所有静态或半静态的安全规则(角色、部门、安全等级)都应放入过滤上下文,以充分利用节点级缓存。
  3. 避免评分污染:安全条件不应出现在 must 或 should 中,否则恶意用户可能通过构造查询词项使未授权文档获得高分,从而绕过权限控制。
  4. 字段级安全在响应层:字段级可见性控制应在查询结果返回阶段实施,而非在查询上下文中。使用 _source 过滤或 fields 参数,配合过滤上下文中的文档级权限,形成双层安全屏障。
实践检查清单:每次构建搜索查询时,问自己三个问题:(1) 这个条件是否用于计算相关性?→ 放入查询上下文。(2) 这个条件是否用于限制用户可见数据?→ 放入过滤上下文。(3) 这个条件是否频繁变化?→ 考虑放入查询上下文(避免缓存失效风暴)。