--- title: "Easysearch 布尔查询子句重排序(一)|你的 BoolQuery 写法,真的影响性能吗?" date: 2026-08-26 lastmod: 2026-08-26 description: "本文是系列第一篇,聚焦构建层:从你发出 JSON 到查询进入执行引擎,中间经历了哪些变换?哪些优化在这个阶段完成?哪些要留到执行层?后续三篇将分别深入合取查询的 cost 排序、析取查询的 WAND 剪枝,以及 Block-Max 块级剪枝与实战验证。" tags: ["Easysearch", "布尔查询", "BoolQuery", "排序"] summary: "从 Easysearch 到 Lucene,查询构建层的 11 条优化规则 INFINI Easysearch 是一款专注于企业级场景的分布式近实时搜索与分析引擎。它以 Apache Lucene 为内核进行了增强,在保持与行业主流搜索协议及开发生态完全兼容的同时,重点强化了安全性、稳定性、压缩率及信创兼容性。 一、开篇:一个常见的误解 # “must 里面,是不是应该把匹配文档少的条件写在前面?这样能提前过滤掉大量文档,性能更好?” 这个直觉来得很自然,但它是错的。 ┌─────────────────────────────────────┐ │ 用户的直觉: │ │ must: [高频词, 低频词] → 慢 │ │ must: [低频词, 高频词] → 快 │ │ │ │ 实际情况: │ │ 两种写法性能完全相同! │ │ Lucene 执行时自动按 cost 排序 │ └─────────────────────────────────────┘ Easysearch 执行时会按 cost 自动重排子句顺序,与你写查询时的顺序无关。不过"子句顺序不重要"并非处处成立,它有一条跟版本挂钩的边界——must/filter 里混入 prefix/wildcard 这类多词查询时,老版本引擎会重新对顺序敏感(详见 第二篇 §八的边界说明)。而且"子句顺序不重要"也不代表"怎么写都一样"——理解引擎自动优化的边界在哪里,才能设计出更合理的查询结构。 本文是系列第一篇,聚焦构建层:从你发出 JSON 到查询进入执行引擎,中间经历了哪些变换?哪些优化在这个阶段完成?哪些要留到执行层?后续三篇将分别深入合取查询的 cost 排序、析取查询的 WAND 剪枝,以及 Block-Max 块级剪枝与实战验证。" --- > 从 Easysearch 到 Lucene,查询构建层的 11 条优化规则 _INFINI Easysearch 是一款专注于企业级场景的分布式近实时搜索与分析引擎。它以 Apache Lucene 为内核进行了增强,在保持与行业主流搜索协议及开发生态完全兼容的同时,重点强化了安全性、稳定性、压缩率及信创兼容性。_ --- ## 一、开篇:一个常见的误解 > "must 里面,是不是应该把匹配文档少的条件写在前面?这样能提前过滤掉大量文档,性能更好?" 这个直觉来得很自然,但**它是错的**。 ``` ┌─────────────────────────────────────┐ │ 用户的直觉: │ │ must: [高频词, 低频词] → 慢 │ │ must: [低频词, 高频词] → 快 │ │ │ │ 实际情况: │ │ 两种写法性能完全相同! │ │ Lucene 执行时自动按 cost 排序 │ └─────────────────────────────────────┘ ``` Easysearch 执行时会按 cost 自动重排子句顺序,与你写查询时的顺序无关。不过"子句顺序不重要"并非处处成立,它有一条跟版本挂钩的边界——must/filter 里混入 prefix/wildcard 这类多词查询时,老版本引擎会重新对顺序敏感(详见[第二篇](/blog/2026/easysearch-boolean-query-clause-reordering-part-2/) §八的边界说明)。而且"子句顺序不重要"也不代表"怎么写都一样"——理解引擎自动优化的边界在哪里,才能设计出更合理的查询结构。 本文是系列第一篇,聚焦**构建层**:从你发出 JSON 到查询进入执行引擎,中间经历了哪些变换?哪些优化在这个阶段完成?哪些要留到执行层?后续三篇将分别深入合取查询的 cost 排序、析取查询的 WAND 剪枝,以及 Block-Max 块级剪枝与实战验证。 --- ## 二、全景:一次布尔查询的完整旅程 先建立一张全局地图,再深入每一层。 举个例子:一条布尔查询就像一个包裹进入工厂流水线,经过三道工序: - **第一道**(Easysearch 层):质检员检查包裹格式是否合规,缺不缺东西,但**不重新排列**里面的物品顺序 - **第二道**(Lucene rewrite):工艺师合并重复部件、去掉矛盾组合、把"可选"升级为"必选"——改变的是包裹的**内容结构**,不是物品顺序 - **第三道**(Scorer 层):调度员拿到最终包裹,按每个部件的"处理成本"自动安排加工顺序——**这才是代价排序发生的地方** 用技术语言描述,这三道工序对应的是(注意①和②③分属不同阶段): ``` 用户 JSON │ ▼ ┌──────────────────────────────────────────┐ │ Easysearch 层(构建层) │ │ ① doRewrite() — 递归重写 + 早期终止 │ │ ② applyMinimumShouldMatch │ │ ③ fixNegativeQueryIfNeeded │ │ (①在 rewrite 阶段,②③在 doToQuery()内)│ │ 职责:结构合法化,不改子句顺序 │ └────────────────┬─────────────────────────┘ │ toQuery() → BooleanQuery ▼ ┌────────────────────────────────────────────┐ │ Lucene rewrite 层(逻辑重写层) │ │ ④ BooleanQuery.rewrite() │ │ (IndexSearcher 中 rewrite→createWeight) │ │ 职责:11条逻辑等价改写,不改结果只改形态 │ └────────────────┬───────────────────────────┘ │ createWeight() ▼ ┌──────────────────────────────────────────┐ │ Scorer 构造层(执行层) │ │ ⑤ ConjunctionDISI:按 cost() 排序 ✅ │ │ ⑥ WANDScorer:按 maxScore 动态重排 ✅ │ │ 职责:代价感知,真正的性能优化在这里 │ └────────────────┬─────────────────────────┘ │ ▼ 执行查询,返回结果 ``` **一个关键认知**:代价排序发生在第⑤步(Scorer 层)。前两道工序只做**逻辑等价改写**——合并重复、升级类型、展平嵌套,但不改变查询结果。 本文讲前两层(①~④),后三篇讲第⑤⑥步。 --- ## 三、Easysearch 层:结构合法化,不碰顺序 你发出的 JSON,首先被 Easysearch 的 `BoolQueryBuilder` 解析成内部的查询对象。这一层做的事情很克制:**保证查询结构合法,但不改变子句顺序**。 ### 3.1 子句是如何被添加的 `BoolQueryBuilder.doToQuery()` 按照固定顺序把子句添加到 Lucene 的 `BooleanQuery.Builder` 中: ``` must → mustNot → should → filter ``` 不同类型之间的添加顺序是固定的(无论你的 JSON 里先写 `should` 还是先写 `must`),但同一类型内的子句顺序与 JSON 书写顺序一致——这通常不影响性能,因为代价排序发生在更下游的 Scorer 层;唯一的例外见[第二篇](/blog/2026/easysearch-boolean-query-clause-reordering-part-2/) §八:老版本上混入多词查询时,书写顺序仍会起作用。 ### 3.2 三种特殊处理 Easysearch 层会做三类结构合法化处理: **`doRewrite()` 的早期终止**: - 如果整个 BoolQuery 为空(没有任何子句),退化为 `MatchAllQueryBuilder`(等价于 Lucene 的 `MatchAllDocsQuery`) - 如果任何 `must` 或 `filter` 子句重写后变为 `MatchNoneQueryBuilder`,整个 BoolQuery 直接返回该 `MatchNoneQueryBuilder`——不需要继续执行 - 如果没有 `must`/`filter` 子句,但所有 `should` 子句都重写为 `MatchNoneQueryBuilder`,整个 BoolQuery 也退化为 `MatchNoneQueryBuilder`——没有必须匹配的子句,所有可选子句又都匹配零文档,结果必然为空 **`fixNegativeQueryIfNeeded()`**: 当查询只有 `must_not` 子句、没有任何正向匹配条件时,Lucene 的 `BooleanQuery` 不知道"从哪些文档里排除"。Easysearch 自动插入一个 `MatchAllDocsQuery` 作为基础集合。该修复受 `adjust_pure_negative` 开关控制(默认为 `true`,可设为 `false` 关闭): ``` 输入:must_not: [term:spam] 处理:加入 MatchAllDocsQuery (作为 FILTER) 输出:filter: [MatchAll] + must_not: [term:spam] = "所有文档 除了 spam" ``` **`applyMinimumShouldMatch()`**: 把用户设置的 `minimum_should_match` 规格字符串(支持整数 `"2"`、百分比 `"75%"`、条件式 `"3<75%"` 等)解析为 `int`,写入 Lucene 的 `BooleanQuery.setMinimumNumberShouldMatch()`。 总结:**Easysearch 层不改变子句顺序,只做合法化修补。**真正的优化交给下游。 --- ## 四、Lucene rewrite 层:11 条逻辑等价改写规则 查询经过 `toQuery()` 变成 Lucene 的 `BooleanQuery` 对象后,会调用 `BooleanQuery.rewrite()`。这是本文的核心章节。 这一层不做代价排序,而是通过 11 条规则**改写查询的形态**——去重、提升、展平——但保证改写前后查询结果完全一致,为后续执行层的高效优化铺路。 > 📌 本文按理解难度递进排列规则编号。源码中 `BooleanQuery.rewrite()` 实际包含 12 个步骤,本文将其中 SHOULD 去重和 MUST 去重合并为规则 8,并按逻辑将 MatchAll→ConstantScore 编为规则 11,因此源码实际执行顺序按本文编号为:1→2→3→4→5→6→7→8→**11**→9→10(ConstantScore 转换在展平和 minShouldMatch 对齐之前执行)。 --- ### 规则 1-3:消除不可能、去重、矛盾检测 这三条是防御性规则,含义很容易理解,快速过一遍: | # | 规则 | 触发条件 | 行为 | | --- | --------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | 空查询消除 | 没有任何子句 | → `MatchNoDocsQuery` | | 2 | 单子句拆包 | 只有 1 个子句 | 拆掉 BooleanQuery 外壳,直接用内部查询。SHOULD/MUST 直接返回内部查询;FILTER 包裹为 `BoostQuery(ConstantScoreQuery(query), 0)`(确保得分为零);MUST_NOT → `MatchNoDocsQuery` | | 3 | 递归重写 + MatchNoDocs 短路 | 子句重写后变化,或含 MatchNoDocs | 递归简化每个子句(FILTER/MUST_NOT 先包裹 `ConstantScoreQuery` 再重写再剥壳,SHOULD/MUST 直接重写);SHOULD/MUST_NOT 中的 MatchNoDocs 直接移除,MUST/FILTER 中的 MatchNoDocs 导致整体短路 | ``` 规则 2 示例: BooleanQuery { MUST: [TermQuery(status:published)] } ↓ rewrite TermQuery(status:published) BooleanQuery { FILTER: [TermQuery(status:published)] } ↓ rewrite BoostQuery(ConstantScoreQuery(TermQuery(status:published)), 0) 规则 3 示例: must: [MatchNoDocsQuery], should: [TermQuery(A)] ↓ rewrite(MUST 中含 MatchNoDocs → 整体短路) MatchNoDocsQuery ``` --- ### 规则 4-6:去重、矛盾检测、冗余移除 继续快速过: | # | 规则 | 触发条件 | 行为 | | --- | -------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | 4 | FILTER/MUST_NOT 去重 | 相同子句重复出现 | `HashSet` 自动去重(基于 `Query.equals()`)。SHOULD/MUST 用 `Multiset` 保留重复(以便后续规则 8 做 boost 求和) | | 5 | 矛盾检测 | MUST 或 FILTER 与 MUST_NOT 含同一子句,或 MUST_NOT 含 MatchAll | → `MatchNoDocsQuery` | | 6 | 冗余 FILTER 移除 | FILTER 与 MUST 重叠,或 FILTER 含 MatchAll | FILTER/MUST 重叠:无条件移除冗余 FILTER;FILTER 含 MatchAll:仅当移除后仍有正向子句时移除(`filters.size() > 1 \|\| !mustClauses.isEmpty()`) | ``` 规则 4 示例: filter: [term:active, term:active, range:age>18] → filter: [term:active, range:age>18] 规则 6 示例: must: [term:active], filter: [term:active, range:age>18] → must: [term:active], filter: [range:age>18] ``` --- ### 规则 7:SHOULD + FILTER → MUST 提升 ⭐ **触发条件**:同一个子查询同时出现在 SHOULD 和 FILTER 子句中。 **行为**:将该子查询的 SHOULD 子句改为 MUST(原 FILTER 子句直接丢弃,因为 MUST 已隐含了 FILTER 的过滤语义)。源码里会同时下调 `minimumNumberShouldMatch`(每提升一个子句 `minShouldMatch--`,循环结束后统一 `Math.max(0, minShouldMatch)` 确保不低于 0): - 若提升后 `minShouldMatch == 0`,mix 路径走 req+opt(`ReqOptSumScorer`) - 若提升后 `minShouldMatch > 0`,仍走 conjunction-disjunction mix(`ConjunctionScorer(req,opt)`) 也就是说,规则 7 一定会改变查询形态,但是否切到 `ReqOptSumScorer` 取决于 `minShouldMatch` 是否归零。 ``` 优化前: ┌───────────────────────┐ │ SHOULD: [term:A] │ │ FILTER: [term:A] │ │ SHOULD: [term:B] │ └───────────────────────┘ ↓ rewrite 优化后: ┌───────────────────────┐ │ MUST: [term:A] │ ← 提升为 MUST! │ SHOULD: [term:B] │ └───────────────────────┘ ``` > 💡 类比:一个人同时是"候选人"(SHOULD)又是"已入职"(FILTER)。既然已经入职,直接列入正式编制(MUST)。 > ⚠️ **`minShouldMatch` 联动**:每将一个 SHOULD 提升为 MUST,`minimumNumberShouldMatch` 相应减 1(循环结束后 `Math.max(0, minShouldMatch)` 兜底),确保语义等价。例如原有 `minimum_should_match: 2` 且两个 SHOULD 中有一个被提升,重写后 `minShouldMatch` 变为 1。 --- ### 规则 8:SHOULD / MUST 去重(boost 求和) **触发条件**:SHOULD 或 MUST 中出现了相同的子句(解包 BoostQuery 后底层查询相同)。注意:SHOULD 去重仅在 `minimumNumberShouldMatch ≤ 1` 时触发;若 `minShouldMatch > 1`,Lucene 不会合并重复的 SHOULD 子句(多个重复出现在高 minShouldMatch 场景下语义不可简单合并)。MUST 去重则**没有**此限制,无论 `minShouldMatch` 为何值都会合并重复的 MUST 子句。 **行为**:合并重复子句,将它们的 boost 相加。FILTER 和 MUST_NOT 的去重由规则 4 处理(直接删除),而 SHOULD 和 MUST 的重复是有意义的——不同的 boost 意味着不同的评分权重,所以求和保留。不合并的话,同一个 term 会创建两套独立的迭代器,都遍历相同的文档列表,浪费翻倍。 ``` should: [term:hello^1.5, term:hello^2.0, term:world] ↓ rewrite should: [term:hello^3.5, term:world] (两个 hello 的权重合并:1.5 + 2.0 = 3.5) ``` > 💡 类比:一个学生选了同一门课两次,一次记 1.5 学分,一次记 2 学分。不需要上两次课,合并为 3.5 学分即可。 --- ### 规则 9:SHOULD 嵌套展平 ⭐ **触发条件**:一个 `bool` 查询的 SHOULD 子句里,嵌套了另一个**纯 SHOULD** 的 `bool` 查询(即内层没有 MUST、FILTER、MUST_NOT,只有 SHOULD,且 `minimum_should_match` ≤ 1)。 **行为**:把内层 SHOULD 子句全部"提升"到外层,展平为同一级别的 SHOULD 子句。展平后 WAND 能看到每个子句的独立 maxScore,估算更紧,剪枝更激进;如果不展平,WAND 只能看到内层查询的总体上界(黑盒),剪枝不够狠。 ``` 优化前: BoolQuery (外层) / \ SHOULD SHOULD (term:A) (内层 BoolQuery) / \ SHOULD SHOULD (term:B) (term:C) ↓ rewrite 优化后: BoolQuery / | \ SHOULD SHOULD SHOULD (term:A)(term:B)(term:C) ``` **实践建议**:如果你在 `should` 里嵌套了多层 `bool`,且内层全是 SHOULD 子句,EasySearch 会自动展平。但如果内层有 `minimum_should_match >= 2`,则不会展平(语义不等价),这类情况应尽量手动展平或重构查询结构。 > 💡 为什么展平能更激进地剪枝?假设内层 bool 有两个子句,maxScore 分别是 8 和 3。展平前,WAND 只看到"这个内层查询最多得 8+3=11 分",不管当前文档匹配了哪些子句,上界永远是 11;展平后,WAND 逐子句检查——某个文档如果不匹配 maxScore=8 的子句,只剩 maxScore=3 的子句可能匹配,上界从 11 降到 3。如果录取线是 5,3 < 5,这个文档不可能入选——直接跳过,不用再算分了。 --- ### 规则 10:SHOULD 数量与 minimumShouldMatch 的对齐 **触发条件**:SHOULD 子句数量与 `minimum_should_match` 的大小关系。 **行为**:分两种情况: - SHOULD 数量 **<** minimumShouldMatch:不可能满足 → 直接返回 `MatchNoDocsQuery`,省去无用计算 - SHOULD 数量 **==** minimumShouldMatch:所有 SHOULD 提升为 MUST,从 WANDScorer(调度开销大)转入 ConjunctionDISI(cost 排序,更高效) ``` should: [A, B],minimum_should_match: 3 ↓ rewrite(2 < 3,不可能满足) MatchNoDocsQuery should: [A, B, C],minimum_should_match: 3 ↓ rewrite(3 == 3,等价于全部 MUST) must: [A, B, C] ``` > 💡 类比:开会时,如果"3 个可选发言人必须全部到场"——那"可选"就没意义了,等价于"3 个必须到场"。如果要求"3 人到场但只有 2 人可选"——不可能,直接取消会议。 **一个容易忽略的场景**:在动态拼接查询时(例如从用户的多个筛选条件生成 `should`,然后设置 `minimum_should_match` 等于条件数量),这条规则会自动把它转化为更高效的 MUST 查询,无需手动改写。 --- ### 规则 11:MatchAll + FILTER → ConstantScoreQuery **触发条件**:BooleanQuery 恰好只有一个 MUST 子句且为 `MatchAllDocsQuery`,且至少有一个 FILTER 子句。 **行为**:将所有 FILTER + MUST_NOT 组成内部 BooleanQuery,整体包裹为 `ConstantScoreQuery`(绕过评分,返回固定分数),作为外层 MUST 加入;SHOULD 子句加回外层(**不丢弃**);原始 `MatchAllDocsQuery` 被消耗。`MatchAllDocsQuery` 在 BooleanQuery 框架里有额外调度开销,ConstantScoreQuery 执行路径更直接。 > ⚠️ 注意:纯 `filter` 查询没有 MUST 子句,不满足 `musts.size() == 1` 的前提,不触发本规则。FILTER 子句直接进入合取路径,由 `ConjunctionDISI` 按 cost 排序处理。 --- 在 11 条规则中,标 ⭐ 的规则 7(SHOULD+FILTER→MUST)和规则 9(SHOULD 嵌套展平)对执行性能影响最大——前者决定子句能否进入 ConjunctionDISI 的 cost 排序路径,后者决定 WANDScorer 能否做全局剪枝。 --- ## 五、实战:一条 rewrite 规则如何改变执行路径 前面列了 11 条规则,这一节用具体例子展示:构建层的一条 rewrite 规则,如何直接影响执行层的路径选择。 假设你有一个查询: ```json { "bool": { "should": [ { "term": { "status": "published" } }, { "term": { "category": "ai" } } ], "filter": [{ "term": { "status": "published" } }] } } ``` `status:published` 同时出现在 `should` 和 `filter`——规则 7 会把它提升为 MUST,并下调 `minShouldMatch`。下图假设提升后 `minShouldMatch=0`,执行路径因此发生根本变化: ``` ┌─────────────────────────────────────────────────────────────────┐ │ 没有 rewrite 优化(假设) │ │ minShouldMatch=1,走 conjunction-disjunction mix 路径 │ │ │ │ ┌────────────────────────────┐ │ │ │ ConjunctionScorer │ │ │ │ ├─ FilterScorer │ published 出现两次: │ │ │ │ published (score=0) │ • FILTER 里遍历一遍(只过滤) │ │ │ └─ DisjunctionSumScorer │ • SHOULD 里再遍历一遍(评分) │ │ │ ├─ published │ = 同一个 term 被两个迭代器 │ │ │ └─ ai │ 各跑一遍,浪费! │ │ └────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ rewrite 优化后(实际) │ │ minShouldMatch=0,走 req+opt 路径 │ │ │ │ ┌────────────────────────────┐ │ │ │ ReqOptSumScorer │ published 只出现一次: │ │ │ ├─ req: published (有评分) │ • 作为 MUST,一次迭代同时 │ │ │ └─ opt: ai (可选加分) │ 完成过滤和评分 │ │ └────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` 优化前,`status:published` 被两个迭代器各遍历一遍;优化后,一次迭代同时完成过滤和评分——**一条 rewrite 规则,改变了执行路径的选择**。它不做代价排序,但决定了哪些子句有资格进入更高效的路径。 --- ## 六、动手验证:用 Profile API 观察 rewrite 效果 理论再多,不如自己跑一遍。Easysearch 的 Profile API 可以直接暴露 rewrite 后的查询形态,不需要读源码,几秒钟就能验证。 ### 6.1 验证规则 7:SHOULD + FILTER → MUST 提升 准备好一个含有 `status` 和 `category` 字段的索引,执行: ```json GET /products/_search { "profile": true, "query": { "bool": { "should": [ { "term": { "status": "published" }}, { "term": { "category": "ai" }} ], "filter": [ { "term": { "status": "published" }} ] } } } ``` 找到响应中 `profile.shards[0].searches[0].query[0].description` 字段(具体格式可能因版本略有不同): - **你写的**:SHOULD + FILTER 并存(两个地方都有 `status:published`) - **实际执行**:`+status:published category:ai` 注意 `+` 前缀——在 Lucene 的查询 description 语法中,`+` 表示 MUST,没有符号表示 SHOULD。`status:published` 前面有 `+`,说明 rewrite 已经把它提升为 MUST,查询形态已经发生了变化。 ### 6.2 验证规则 10:SHOULD 数量 == minimumShouldMatch → 全部提升为 MUST ```json GET /products/_search { "profile": true, "query": { "bool": { "should": [ { "term": { "status": "published" }}, { "term": { "category": "ai" }} ], "minimum_should_match": 2 } } } ``` profile 的 `description` 应该显示 `+status:published +category:ai`——两个 term 都带 `+` 前缀,说明全部提升为 MUST,这个查询实际上会走第二篇要讲的 ConjunctionDISI 路径,而非 WANDScorer。 ### 6.3 理解 Profile 响应的结构 一个完整的 Profile 响应包含大量信息,但读懂核心字段只需关注三个位置: ```json { "profile": { "shards": [{ "searches": [{ "query": [{ "type": "BooleanQuery", "description": "+status:published +category:ai", "breakdown": { "next_doc": 12750, "next_doc_count": 1, "advance": 0, "advance_count": 0, "create_weight": 375375, "create_weight_count": 1, "build_scorer": 248958, "build_scorer_count": 2 }, "children": [ { "type": "TermQuery", "description": "status:published", ... }, { "type": "TermQuery", "description": "category:ai", ... } ] }], "rewrite_time": 146958 }] }] } } ``` 快速解读三个关键位置: | 字段 | 含义 | 怎么看 | | --------------------------- | ----------------------- | --------------------------------------------------------- | | `description` | rewrite 后的查询形态 | `+` 表示 MUST,无前缀表示 SHOULD,`-` 表示 MUST_NOT | | `advance` / `advance_count` | 迭代器跳转的耗时 / 次数 | 第二篇核心指标。count 越小 = 跳转越少 = cost 排序效果越好 | | `rewrite_time` | rewrite 阶段的总耗时 | 本文 11 条规则的总执行时间,通常很小(微秒级) | > 💡 小技巧:`breakdown` 里每个指标都有两个 key——`xxx` 是耗时(纳秒),`xxx_count` 是调用次数。想看"做了多少次"看 `_count`,想看"花了多少时间"看不带 `_count` 的。 --- ## 七、小结与预告 我们走完了布尔查询在执行前的两个构建层: **Easysearch 层**做的是合法化处理——保持子句顺序,修补极端情况(纯否定、空查询、MatchNone 短路),设置 minShouldMatch。它不会改变你查询的"形状"。 **Lucene rewrite 层**通过 11 条逻辑等价改写规则改变查询的"形态": - 规则 1-6 是防御性规则,消除空查询、冗余和矛盾 - **规则 7 和规则 10** 是提升性规则,把更多子句导入 ConjunctionDISI 的合取路径,为 cost 排序创造更大的发挥空间 - **规则 9** 是为析取优化准备的,展平 SHOULD 嵌套让 WANDScorer 能做全局剪枝 - 规则 8 是评分优化(合并重复 boost),规则 11 绕过不必要的评分计算 这两层都**不做**代价排序,但它们决定了哪些子句有资格进入更高效的执行路径。 --- **下一篇预告** 当 MUST 子句进入 Scorer 构造层,Lucene 会创建 `ConjunctionDISI`,对所有迭代器按 `cost()` 升序排序——最稀疏的放在第一位,承担"领头"角色,最大限度地用跳转(`advance()`)跳过不满足条件的文档。 匹配文档最少的迭代器,为什么反而被选来驱动整个遍历?——它产生的候选集最小,所有迭代器的验证次数因此被压到最低。这个看似"以弱领强"的设计,正是合取查询性能优化的核心。我们在第二篇,通过源码、图解和 Profile API 实测,把这个机制讲透。