《查询为什么没命中》里有一张图,长得像铁路线路:从 start 出发,经过若干分叉和回路,最后抵达 end。它不是 Elasticsearch 的执行流程图,也不是网络拓扑图,而是一张轨道语法图(railroad diagram)。

轨道图只回答一个问题:一段结构化输入有哪些合法的排列方式?

以 Query DSL 为例,它可以把“请求体里能放哪些顶层参数”“query 的值应该是什么形状”“bool 如何继续嵌套查询”画在一张图里。读图时沿着轨道走,画图时把语法规则翻译成可以走的路径。

本文只讲这张图的画法和读法,不展开 Elasticsearch 字段、分析器或评分模型。示例沿用原文中的 SearchRequestBody、TopLevelMember 和 QueryClause。

先分清:轨道图不是流程图

流程图通常描述“先做什么,再做什么”,节点是动作,箭头是时间或控制流。例如:

flowchart LR
    A[接收请求] --> B[解析 JSON] --> C[执行查询] --> D[返回结果]

轨道图描述“输入可以长什么样”,节点是语法元素,箭头是合法路径。例如,下面的图不是在说 Elasticsearch 先执行 query 再执行 sort,而是在说一个请求对象可以同时包含这两个成员:

flowchart LR
    START((start)) --> LBRACE["{"] --> MEMBER{选择成员}
    MEMBER --> Q[query]
    MEMBER --> SORT[sort]
    Q --> MORE{还有成员?}
    SORT --> MORE
    MORE -->|是| MEMBER
    MORE -->|否| RBRACE["}"] --> END((end))

query 和 sort 在 JSON 中是同级字段。它们在轨道图里先后出现,只是因为图需要把“多个成员”画成一条可行走的路径,不代表搜索执行有这个先后顺序。

三个快速判断

看见一张“像铁路”的图,可以先问三个问题:

问题 轨道图的答案 流程图的答案
节点表示什么? 字面量、语法类别或占位符 动作、状态或系统组件
箭头表示什么? 输入的合法排列方向 时间、控制流或数据流
回路表示什么? 重复出现、列表或递归 重试、循环或事件回环

这一步很重要。把语法图当执行流程图,后面的每一条线都会被误读。

先判定图的语义

模式公式:图中节点的含义 + 箭头的含义 → 正确读法

图的节点 箭头通常表示 适合回答的问题
语法符号 合法输入路径 JSON、命令或表达式怎样组成
动作 执行顺序 系统先后做什么
数据对象 数据流向 数据从哪里到哪里

看到“分支、可选、重复、递归”这些词时,应优先把图当作语法图,而不是性能或执行图。这个判断可以迁移到 SQL 语法图、命令行参数图和协议报文图。

轨道图里的四类节点

一张可读的轨道图,通常只需要四类节点。节点颜色可以辅助理解,但颜色不是语法;没有颜色时,节点文字仍应足够明确。

1. 起点和终点

start 和 end 表示一条完整路径的边界。它们不属于输入内容,只告诉读者从哪里开始走、走到哪里算完成。

flowchart LR
    START((start)) --> TOKEN[一个合法输入] --> END((end))

如果一张图没有清楚的起点,读者不知道哪些节点是必经的;没有清楚的终点,读者也无法判断某个分支是否已经组成了完整结构。

2. 字面量节点

字面量是输入中真的要出现的字符或单词,例如 {、}、query、,。在 Mermaid 中可以用方框表示:

flowchart LR
    START((start)) --> LBRACE["{"] --> KEY["query"] --> COLON[":"] --> VALUE[QueryClause] --> END((end))

这张图对应一个最小请求:

1
2
3
{
"query": QueryClause
}

其中 QueryClause 不是要原样输入的字符串,而是一个需要继续展开的语法类别。

3. 语法类别节点

QueryClause、SortValue、TopLevelMember 这类名称是非终结符(non-terminal):它们代表一组可能的结构,不能只看名字就算走完。

flowchart LR
    A[TopLevelMember] -.展开.-> B{选择一种成员}
    B --> C[query : QueryClause]
    B --> D[sort : SortValue]
    B --> E[size : Number]

图中 TopLevelMember 是一个入口,右侧分支列出它允许的具体形状。非终结符的价值在于复用:同一个 QueryClause 可以被根查询使用,也可以被 bool.must 的子查询使用。

4. 选择、重复和回到入口

菱形通常表示选择点,带标签的箭头说明选择条件。回到之前的节点,通常表示重复或递归。

flowchart LR
    CHOICE{选择}
    CHOICE -->|A| A[A 分支]
    CHOICE -->|B| B[B 分支]
    A --> MORE{还要一个?}
    B --> MORE
    MORE -->|是| CHOICE
    MORE -->|否| END((end))

这张图表达的是“可以选择多个成员”,而不是“先执行 A,再执行 B”。如果回路上没有“是/否”“逗号后继续”或“重复一次”等说明,读者很难知道回路的边界,画图时应补上标签。

把 Query DSL 规则翻译成轨道

画图前先写出一份短语法。不要从 JSON 示例直接拉箭头;JSON 只展示了一个实例,语法图要表达一组实例。

原文中的代表性规则可以先压缩成下面四条:

1
2
3
4
5
6
7
8
9
10
11
SearchRequestBody ::= "{" [TopLevelMember ("," TopLevelMember)*] "}"

TopLevelMember ::= "query" ":" QueryClause
| "sort" ":" SortValue
| "from" ":" Number
| "size" ":" Number

QueryClause ::= "{" QueryType ":" TypeSpecificBody "}"

ChildQueryValue ::= QueryClause
| "[" QueryClause ("," QueryClause)* "]"

这几行不是 Elasticsearch 的配置,也不是可直接提交的 Query DSL,而是帮助画图的语法摘要。符号含义如下:

符号 含义
::= 左侧可以展开成右侧
` `
[ ... ] 可选,出现零次或一次
( ... )* 括号内的结构可以重复零次或多次
"..." 输入中要出现的字面量
大写名称 还需要继续展开的语法类别

第一步:画一条最小主轨道

SearchRequestBody 的主干只有三个部分:左大括号、成员、右大括号。

flowchart LR
    START((start)) --> LBRACE["{"] --> MEMBER[TopLevelMember] --> RBRACE["}"] --> END((end))

这张图还不完整,因为 TopLevelMember 只能出现一次。规则中的 ("," TopLevelMember)* 表明第一个成员之后可以继续添加“逗号 + 新成员”。

第二步:把重复画成回路

flowchart LR
    START((start)) --> LBRACE["{"] --> MEMBER[TopLevelMember] --> MORE{还有成员?}
    MORE -->|是:先写逗号| COMMA[","] --> MEMBER
    MORE -->|否| RBRACE["}"] --> END((end))

沿着“否”走,得到只有一个成员的对象;沿着“是”走一次,再选择一个 TopLevelMember,就得到包含两个成员的对象。继续走回路,就得到三个或更多成员。

因此,下面的请求在主轨道上是合法的:

1
2
3
4
5
{
"query": { "match": { "message": "timeout" } },
"from": 20,
"size": 10
}

query、from 和 size 是三个不同的 TopLevelMember,它们可以沿着同一条回路依次出现。

第三步:把选择画成分支

TopLevelMember 有多个候选形状,因此从选择点分出多条轨道:

flowchart TB
    MEMBER{选择 TopLevelMember}
    MEMBER --> Q["query : QueryClause"]
    MEMBER --> SORT["sort : SortValue"]
    MEMBER --> FROM["from : Number"]
    MEMBER --> SIZE["size : Number"]

分支的含义是“选其中一个形状”,不是“这些字段都必须出现”。字段是否可选、是否能重复,需要由外层轨道表达。这里的外层回路允许不同成员继续出现,但没有表达“同名字段能否重复”;如果规则禁止重复 key,就应在文字说明中明确这一点,不要让读者从回路猜出错误结论。

第四步:把递归画成回边

bool 的 must、filter、should 和 must_not 可以承载一个或多个查询子句。子查询本身仍然是 QueryClause,所以它可以再次选择 match、term 或另一个 bool。

flowchart LR
    ROOT[QueryClause] --> TYPE{选择 query type}
    TYPE --> LEAF[match / term / range]
    TYPE --> BOOL[bool]
    BOOL --> SLOT[must / filter / should / must_not]
    SLOT --> CHILD[QueryClause 或 QueryClause 数组]
    CHILD -.再次展开.-> TYPE

这条回边表示递归,不表示查询执行时会无限循环。每次递归都要消耗一个实际的 JSON 对象;输入有限,树就会在某个叶子查询处结束。

例如,下面的 bool 嵌套了两个叶子查询:

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"must": [
{ "match": { "message": "timeout" } },
{ "term": { "status": "open" } }
]
}
}
}

读图时先从根部选择 bool,再进入 must,再选择数组中的第一个 QueryClause。第一个子句走到 match 叶子后结束;数组中的第二个元素重新进入同一个 QueryClause 入口,选择 term 叶子。

用“主干、分支、回路、回边”拆语法

模式公式:主干定边界 + 分支定选择 + 回路定重复 + 回边定递归

图形 语法含义 Query DSL 示例
主干 必须经过的顺序 { → 成员 → }
分支 从候选中选择一种 query / sort / size
回路 同一类元素重复出现 逗号后继续成员
回边 子结构再次使用自身规则 bool 的 child QueryClause

当一段 JSON 规则变得复杂时,不要先增加颜色和说明文字;先判断每条线属于这四类中的哪一种。这个拆法也适用于命令行参数、配置文件和协议消息格式。

从轨道反推 JSON

读图的最好方法不是“看懂所有节点”,而是沿着一条路径生成一个具体实例。每走到一个分支,就记录选择;每走过一次回路,就在实例中增加一组重复结构;每遇到非终结符,就展开它。

以最小的 match 查询为例,可以走出这条路径:

1
2
3
4
5
6
7
8
start
→ {
→ TopLevelMember
→ query : QueryClause
→ QueryType = match
→ TypeSpecificBody
→ }
→ end

对应的 JSON 是:

1
2
3
4
5
6
7
{
"query": {
"match": {
"message": "timeout"
}
}
}

再沿主轨道回路一次,选择 size:

1
2
3
4
5
6
7
start
→ {
→ query : QueryClause
→ ,
→ size : Number
→ }
→ end

得到:

1
2
3
4
5
6
7
8
{
"query": {
"match": {
"message": "timeout"
}
},
"size": 10
}

这解释了轨道图的一个实用价值:它可以把“这个字段放在哪一层”变成路径问题。size 走的是 SearchRequestBody 的主轨道,不能塞进 match 的 TypeSpecificBody;must 走的是 bool 的子查询槽位,不能与 query 并列成同一个根查询类型。

用轨道图定位常见误读

把同级参数看成执行顺序

图上从 query 走到 sort,不表示先执行查询再执行排序。它只表示 JSON 对象可以先写 query,再写 sort。把两个字段交换顺序,仍然可以得到同样结构的请求。

把分支看成同时发生

TopLevelMember 的分支是“选择一个成员形状”。一次经过选择点只能走一条分支;要同时拥有 query 和 size,必须回到“还有成员”的回路,再走一次选择点。

把递归回边看成无限循环

QueryClause → bool → child QueryClause → QueryClause 是语法递归。它表达“查询可以嵌套查询”,不代表 Elasticsearch 会无限执行。真正的输入总是有限 JSON,解析树也会在叶子查询或其他终止节点结束。

把占位符当成字面量

QueryClause、Number 和 SortValue 不是要原样输入的字符串。只有带引号的 {、}、query、, 等才是字面量。把 QueryClause 写进 JSON,会得到一个示意文本,而不是可执行请求。

把代表性分支当成完整清单

为了保持可读性,图里常写 match / term / range / ...。省略号表示还有其他合法类型,不表示列表已经穷举。图注应说明“这是代表性结构”,否则读者容易把教学图当成版本无关的完整规范。

用 Mermaid 画轨道图的最小规则

Mermaid 没有专门的铁路语法图语法,通常用 flowchart 模拟。重点不在于画出标准铁路图的外观,而在于保持语法关系准确。

flowchart LR
    START((start)) --> A[必经字面量]
    A --> CHOICE{选择}
    CHOICE -->|一种| B[候选 A]
    CHOICE -->|另一种| C[候选 B]
    B --> MORE{重复?}
    C --> MORE
    MORE -->|是| COMMA[\",\"] --> CHOICE
    MORE -->|否| END((end))

画图时可以遵守五条规则:

  1. 起点和终点只出现一次,避免读者不知道哪条线是完整路径。
  2. 字面量、语法类别和选择点使用稳定的形状或颜色。
  3. 分支箭头写清“选择条件”,不要只画没有标签的多条线。
  4. 回路标明重复条件,区分“可选一次”和“可重复多次”。
  5. 递归回边加注释,说明它回到的是同一语法入口,不是执行循环。

如果一张图已经需要大量颜色图例、跨页连线和长段落注释,说明它可能塞进了过多规则。拆成“请求体主轨道”“查询子句轨道”“具体 query type”三张图,读者更容易沿着一条路径验证一个问题。

一份画图检查表

画完后,沿着图至少走出三条路径:

路径 应验证的内容
最短路径 能否生成最小合法输入
一次重复路径 回路是否真的能增加第二个元素
一次递归路径 子查询是否能回到同一语法入口并在叶子处结束

再检查四个边界:

  • 是否把语法关系误画成执行顺序?
  • 是否把可选项误画成必选项?
  • 是否把递归误画成无条件循环?
  • 是否把代表性示例误写成完整规范?

轨道图的核心不是“画得像铁路”,而是让每条合法路径都能回译成一个输入实例,让每个不合法的结构都能指出卡在哪一段轨道上。用这个标准检查《查询为什么没命中》中的 Query DSL 图,query、sort、bool 和 child QueryClause 各自处于哪一层,就会变成可以沿路径验证的具体问题。

模式速查表

看到的图形 应该想到 读图动作
start / end 完整输入的边界 从起点走到终点
多条分支 语法选择 每次只选一条
带逗号回路 列表或重复成员 走一次就多一个元素
回到同一入口 递归嵌套 展开子结构,直到叶子
非终结符名称 规则占位符 找它的展开图
... 代表性省略 不当成完整清单

记忆句可以压缩成一句:主干看边界,分支看选择,回路看重复,回边看递归;沿路走完,再把路径翻译回 JSON。

参考资料