如何画和理解轨道图:以 Elasticsearch Query DSL 为例
《查询为什么没命中》里有一张图,长得像铁路线路:从 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 | |
其中 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 | |
这几行不是 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 | |
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 | |
读图时先从根部选择 bool,再进入 must,再选择数组中的第一个 QueryClause。第一个子句走到 match 叶子后结束;数组中的第二个元素重新进入同一个 QueryClause 入口,选择 term 叶子。
用“主干、分支、回路、回边”拆语法
模式公式:主干定边界 + 分支定选择 + 回路定重复 + 回边定递归
| 图形 | 语法含义 | Query DSL 示例 |
|---|---|---|
| 主干 | 必须经过的顺序 | { → 成员 → } |
| 分支 | 从候选中选择一种 | query / sort / size |
| 回路 | 同一类元素重复出现 | 逗号后继续成员 |
| 回边 | 子结构再次使用自身规则 | bool 的 child QueryClause |
当一段 JSON 规则变得复杂时,不要先增加颜色和说明文字;先判断每条线属于这四类中的哪一种。这个拆法也适用于命令行参数、配置文件和协议消息格式。
从轨道反推 JSON
读图的最好方法不是“看懂所有节点”,而是沿着一条路径生成一个具体实例。每走到一个分支,就记录选择;每走过一次回路,就在实例中增加一组重复结构;每遇到非终结符,就展开它。
以最小的 match 查询为例,可以走出这条路径:
1 | |
对应的 JSON 是:
1 | |
再沿主轨道回路一次,选择 size:
1 | |
得到:
1 | |
这解释了轨道图的一个实用价值:它可以把“这个字段放在哪一层”变成路径问题。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))
画图时可以遵守五条规则:
- 起点和终点只出现一次,避免读者不知道哪条线是完整路径。
- 字面量、语法类别和选择点使用稳定的形状或颜色。
- 分支箭头写清“选择条件”,不要只画没有标签的多条线。
- 回路标明重复条件,区分“可选一次”和“可重复多次”。
- 递归回边加注释,说明它回到的是同一语法入口,不是执行循环。
如果一张图已经需要大量颜色图例、跨页连线和长段落注释,说明它可能塞进了过多规则。拆成“请求体主轨道”“查询子句轨道”“具体 query type”三张图,读者更容易沿着一条路径验证一个问题。
一份画图检查表
画完后,沿着图至少走出三条路径:
| 路径 | 应验证的内容 |
|---|---|
| 最短路径 | 能否生成最小合法输入 |
| 一次重复路径 | 回路是否真的能增加第二个元素 |
| 一次递归路径 | 子查询是否能回到同一语法入口并在叶子处结束 |
再检查四个边界:
- 是否把语法关系误画成执行顺序?
- 是否把可选项误画成必选项?
- 是否把递归误画成无条件循环?
- 是否把代表性示例误写成完整规范?
轨道图的核心不是“画得像铁路”,而是让每条合法路径都能回译成一个输入实例,让每个不合法的结构都能指出卡在哪一段轨道上。用这个标准检查《查询为什么没命中》中的 Query DSL 图,query、sort、bool 和 child QueryClause 各自处于哪一层,就会变成可以沿路径验证的具体问题。
模式速查表
| 看到的图形 | 应该想到 | 读图动作 |
|---|---|---|
start / end |
完整输入的边界 | 从起点走到终点 |
| 多条分支 | 语法选择 | 每次只选一条 |
| 带逗号回路 | 列表或重复成员 | 走一次就多一个元素 |
| 回到同一入口 | 递归嵌套 | 展开子结构,直到叶子 |
| 非终结符名称 | 规则占位符 | 找它的展开图 |
... |
代表性省略 | 不当成完整清单 |
记忆句可以压缩成一句:主干看边界,分支看选择,回路看重复,回边看递归;沿路走完,再把路径翻译回 JSON。

