如何画和理解轨道图:以 Elasticsearch Query DSL 为例
《查询与检索》里有一张像铁路线路的图:从 start 出发,经过分叉和回路(loop),最后抵达 end。它不是 Elasticsearch 的执行流程图,也不是网络拓扑图,而是一张轨道语法图(railroad diagram)。
轨道图只回答一个问题:一段结构化输入有哪些合法的排列方式?
以 Query DSL 为例,图中可以同时表达请求体的顶层参数、query 的值以及 bool 的嵌套方式。读图就是沿着轨道走;画图就是把语法规则翻译成路径。
这里的“节点是语法元素”需要再分细一点。query、sort、{ 和 , 是终结符,最终会出现在输入中;QueryClause、SortValue 和 Number 是非终结符,只表示还要继续展开的语法类别。标准铁路图用两侧边界标记起点与终点。借用 Mermaid flowchart 画关系图时,可以增加“引用”之类的连线标签,但它们不属于实际输入。
EBNF 是 Extended Backus–Naur Form 的缩写,中文通常译为“扩展巴科斯范式”。这里的 form 是“形式、记法”的意思,不是另外一个隐藏缩写;Backus–Naur 是人名。EBNF 用纯文本描述语法规则:左侧写语法类别,右侧写它允许展开成的结构。EBNF 不是轨道图,轨道图也不是 EBNF;前者是文字表示,后者是图形表示,两者可以表达同一套语法。
本文采用的是便于阅读的常见、简化的 EBNF-like 方言,而不是逐字符照抄某一个标准。它借用了传统 BNF 的 ::=、常见 EBNF 的引号和可选结构,并用 ( ... )* 表示重复。传统 BNF 常把非终结符写成尖括号,例如 <QueryClause> ::= ...;尖括号在这种记法中帮助读者识别非终结符。本文用不带尖括号的 QueryClause,用引号明确标出终结符,因此 QueryClause 与 "query" 的角色已经可以区分。ISO EBNF 也采用这种不要求尖括号的思路,并使用 =、;、逗号和 {...} 等自己的符号约定。不同资料中的 BNF/EBNF 记法并不完全统一,关键是同一份文档开头先声明并始终保持一套约定。
词法、句法与文法
lexical 和 syntactic 是两个层级的形容词。词法层从字符流中识别标识符、数字、关键字和标点等 Token;句法层再把 Token 组合成表达式、语句和更大的结构。return return 可以被词法分析器识别成两个关键字 Token,却不一定是合法语句,这正是两个层级的边界。
grammar 是名词,指规定合法形式的一组规则;grammatical 才是对应的形容词,表示某段输入符合这组规则。技术资料中的 syntax 有时泛指一门语言的全部形式结构,有时狭义地只指 Token 之上的句法,因此规范常用 lexical grammar 和 syntactic grammar 分别命名两层规则。
EBNF 是书写 grammar 的记法,而 grammar 定义了 syntax。EBNF 本身不限定终结符必须是哪一层:终结符是字符时,它可以描述词法;终结符是 Token 时,它可以描述句法。例如:
1 | |
第一条规则描述字符怎样组成标识符,第二条规则描述项和运算符怎样组成表达式。两者都属于 syntax,因为它们只规定输入可以具有哪些合法形式,不解释名字指向什么、类型是否匹配或表达式执行后得到什么结果;这些问题属于名称解析、类型规则和语义。
下面的规则采用本文约定的 EBNF 方言:
1 | |
这条规则没有写成更短的 "{" TopLevelMember* "}",原因是成员之间要有逗号,并且逗号只能出现在两个成员之间。这种写法是一个固定模式,后文「模式:可选的分隔列表」专门展开。
这条规则先用 SVG 和 Mermaid 各画一次。两张图都表达可选成员、逗号分隔和重复回路;SVG 用专用生成器输出,Mermaid 则直接读取 EBNF。bool 的递归另画一张引用关系图,不与请求体的合法路径混在一起。
图 1:SearchRequestBody 的标准 SVG 铁路图
从左向右走,先经过 {。上方旁路允许跳过所有成员,得到 {};中间经过 TopLevelMember;下方经过逗号的轨道从右向左返回成员入口,允许增加下一个成员。圆角框表示终结符,直角框表示非终结符。两侧短竖线标记起点和终点。
图 2:用 Mermaid 原生铁路图展开顶层成员
railroad-ebnf-beta
title "SearchRequestBody 与 TopLevelMember"
accTitle: SearchRequestBody 与 TopLevelMember 铁路图
accDescr: 请求体包含可选的逗号分隔顶层成员列表,每个顶层成员可以是 query、sort、from 或 size
SearchRequestBody ::= "{" [ TopLevelMember ("," TopLevelMember)* ] "}" ;
TopLevelMember ::= "query" ":" QueryClause
| "sort" ":" SortValue
| "from" ":" Number
| "size" ":" Number ;
Mermaid 11.16.0 起提供 railroad-ebnf-beta,可以把 EBNF 直接渲染成铁路图。图 1 和图 2 的 SearchRequestBody 部分表达同一条规则:上方旁路对应 [ ... ],经过逗号的返回轨道对应 ("," TopLevelMember)*。图 2 还把 TopLevelMember 展开成四个代表性分支。
图 3:QueryClause 递归引用关系
flowchart LR
accTitle: QueryClause 递归引用关系
accDescr: bool 查询通过 BoolBody、BoolMember 和 ChildQueryValue 再次引用 QueryClause
QUERY[QueryClause]
QUERY -->|bool 分支| BOOL[BoolClause]
QUERY -->|叶子分支| LEAF[MatchClause / TermClause / RangeClause]
BOOL --> BODY[BoolBody]
BODY --> MEMBER[BoolMember]
MEMBER --> VALUE[ChildQueryValue]
VALUE -. 引用同一个语法定义 .-> QUERY
图 3 不是从 start 走到 end 的铁路图,而是非终结符之间的引用关系图。它只表示 ChildQueryValue 又引用了 QueryClause 的定义,不表示查询的执行流程或无条件循环。
重复(repetition)是语法含义,例如 ("," TopLevelMember)*;返回轨道(loop)是表现重复的画法。递归(recursion)来自非终结符在展开过程中再次引用自身,通常保留为一个非终结符节点,再查阅对应子图。不能按“重复用 loop、递归用 back edge”划分标准轨道图的线条。
下文围绕这三张图说明铁路图的画法和读法,示例沿用原文中的 SearchRequestBody、TopLevelMember 和 QueryClause。
先分清:轨道图不是流程图
流程图通常描述“先做什么,再做什么”,节点是动作,箭头是时间或控制流。轨道图描述“输入可以长什么样”,节点是终结符、非终结符或辅助控制节点,箭头是合法路径。
query、sort、from 和 size 是 TopLevelMember 的同级候选字段。它们在请求体中的排列顺序不表示搜索执行顺序。
三个快速判断
判断一张图是不是轨道图,可以看三个地方:
| 问题 | 轨道图的答案 | 流程图的答案 |
|---|---|---|
| 节点表示什么? | 字面量、语法类别或占位符 | 动作、状态或系统组件 |
| 箭头表示什么? | 输入的合法排列方向 | 时间、控制流或数据流 |
| 回路(loop)表示什么? | 同层结构的重复或分隔列表 | 重试、循环或事件回环 |
节点是语法符号,箭头是合法输入路径。回路(loop)是图形上的返回路径,通常表达同一层结构的重复;递归则是一个非终结符在展开过程中再次进入自身。若节点是动作、箭头表示先后,才是流程图;若节点是数据对象、箭头表示传递,才是数据流图。
轨道图的节点与轨道
标准铁路图主要包含终结符、非终结符、边界标记,以及把它们连接起来的轨道。选择和重复可以直接用线条表达。Mermaid 原生铁路图使用这些构件;只有图 3 这类引用关系图才需要借用 flowchart 的节点和连线。
起点和终点
start 和 end 表示一条完整路径的边界。它们不属于输入内容,只告诉读者从哪里开始走、走到哪里算完成。
如果一张图没有清楚的起点,读者不知道哪些节点是必经的;没有清楚的终点,读者也无法判断某个分支是否已经组成了完整结构。
字面量节点
字面量是输入中真的要出现的字符或单词,例如 {、}、query、,。在铁路图中,字面量和非终结符使用不同形状;图 1 的圆角框是字面量,直角框是非终结符。
这张图对应一个最小请求:
1 | |
这是语法骨架,不是可直接提交的 JSON。QueryClause 不是要原样输入的字符串,而是一个需要继续展开的语法类别。
语法类别节点
QueryClause、SortValue、TopLevelMember 这类名称是非终结符(non-terminal):它们代表一组可能的结构,不能只看名字就算走完。
图 2 中的 TopLevelMember 是一个入口,它的独立规则列出允许的具体形状。非终结符的价值在于复用:同一个 QueryClause 可以被根查询使用,也可以被 bool.must 的子查询使用。
选择、重复和回到入口
标准铁路图用分叉和汇合表示选择,用旁路表示可选,用返回入口的轨道表示重复。Mermaid 原生铁路图也使用这套图形语言。
图 3 的连线表示非终结符之间的引用,不是可以直接生成输入的铁路。关系图与铁路图的线条必须分开解释。
把 Query DSL 规则翻译成轨道
画图先写一份短语法。JSON 只展示一个实例,轨道图要表达一组合法实例,不能直接照着一个 JSON 示例拉箭头。
原文中的代表性规则可以先压缩成下面几条。每个非终结符要么在左侧有定义,要么明确标为本文不再展开的叶子(SortValue、Number、MatchBody、TermBody、RangeBody),这样任何一个名字都能顺着规则找到它在哪里被使用、又展开成什么:
1 | |
上面的 MatchClause、TermClause、RangeClause 和 BoolClause 是把查询类型与查询体明确配对后的展开写法。它等价于先用抽象形式概括:
1 | |
但最后一行不能被当成真正的自由选择规则:"match" 必须配 MatchBody,"term" 必须配 TermBody,"range" 必须配 RangeBody,"bool" 必须配 BoolBody。如果写成 TypeSpecificBody ::= MatchBody | TermBody | RangeBody | BoolBody,就会错误地允许 "match" : BoolBody 这样的交叉组合。因此画总图时可以使用 QueryType 和 TypeSpecificBody 两个占位名;需要精确展开时,应回到前面的四条配对分支。
BoolMember 在本文示例范围内已经完整列出四种子查询成员:must、filter、should 和 must_not。BoolBody 本身还可以有 minimum_should_match、boost 等其他 bool 参数;这些参数不是 ChildQueryValue,因此没有混入这条“子查询成员”的简化规则。这里的“完整”是指本文讨论的子查询成员集合,不是声称穷举 Elasticsearch 全部版本的 bool 参数。
ChildQueryValue 只在 BoolMember 的右侧被引用,也就是说它是 must、filter、should、must_not 这些字段的值类型。沿规则从上往下读,引用链是:
1 | |
最后一步又回到 QueryClause,查询嵌套查询的递归就出在这里。MatchBody、TermBody 和 RangeBody 这类叶子查询体不引用 QueryClause,递归走到它们就结束。minimum_should_match、boost 等非子查询参数已在前文说明,没有写进这份简化文法。
这几行不是 Elasticsearch 的配置,也不是可直接提交的 Query DSL,而是帮助画图的语法摘要。判断一个符号怎么使用,不能只看它的形状,还要看它在规则中的位置:
| 符号 | 含义 | 使用判断 |
|---|---|---|
::= |
左侧可以展开成右侧 | 定义一个语法类别,例如 QueryClause ::= ... |
| ` | ` | 候选分支 |
[ ... ] |
可选,出现零次或一次 | 结构可以省略,例如请求体可以没有 TopLevelMember |
( ... )* |
括号内的结构可以重复零次或多次 | 重复的是整个括号内结构,例如 ("," TopLevelMember)* 每次重复都包含逗号和一个成员 |
"..." |
输入中要出现的字面量 | 要求输入出现这段固定文本,例如 "query"、"{"、",";引号表示它是字面量,不表示它不能重复 |
| 首字母大写的名称(本文约定,通常为 PascalCase) | 还需要继续展开的语法类别 | 例如 QueryClause、Number;这是本文的命名约定,不是 EBNF 标准强制的大小写规则 |
模式:可选的分隔列表
[X ("," X)*]规则里反复出现同一个形状:
SearchRequestBody的成员、BoolBody的成员、ChildQueryValue的数组元素,以及后文 JSON object 的成员。它要同时满足三点:元素可以有零个或多个;相邻元素之间必须有逗号;逗号不能出现在开头或结尾。几种看起来更短的写法各差一点:
写法 能生成 问题 X*{}、{ a b }表达不了逗号 (X ",")*{ a, b, }末尾多一个逗号,JSON 不允许 ("," X)*{ , a, b }开头多一个逗号 X ("," X)*{ a }、{ a, b }正确,但至少要有一个元素 [X ("," X)*]{}、{ a }、{ a, b }正确,允许空 关键在于把第一个元素单独提出来:
X ("," X)*读作“一个元素,之后每多一个元素就先写一个逗号”,逗号因此只会落在两个元素之间。它表达的是“一个或多个”。外面再套一层表示零次或一次的[ ],就把“一个或多个”变成了“零个或多个”,得到空对象{}。ChildQueryValue的数组分支没有套[ ],所以"must": []这种空数组不在本文文法的合法路径上。在铁路图里,这个模式对应一条返回轨道,逗号就画在返回轨道上(tabatkins railroad-diagrams 中的
OneOrMore(item, separator)),再加一条跳过整段的上方旁路(Optional)。图 1 和图 2 展示了这套构件;ChildQueryValue的数组分支与 JSON object 也使用同一种“首元素 + 分隔符 + 后续元素”模式。
引号和重复次数是两件事。逗号字面量虽然带引号,但它位于 ("," TopLevelMember )* 中,所以可以随着回路出现多次;相反,一个没有引号的 QueryClause 是非终结符,必须根据它的定义继续展开。| 也不是“把几段文字都连接起来”,而是一次展开时从候选分支中选择一个。
在 QueryClause 的 QueryType 位置,match 和 bool 都是并列可选的查询类型:
1 | |
这里有两个需要同时记住的层次:
- 语法层面,
match和bool都可以填入QueryClause的QueryType位置。 - Elasticsearch 的查询分类层面,
bool是一种compound query,因为它可以通过must、filter、should和must_not组合子查询;match通常是叶子查询。
所以,match 和 bool 可以互换的是“查询类型的位置”,不能直接互换查询体。选择 bool 后,需要继续展开它的组合查询结构;选择 match 后,通常沿字段和值走到叶子。
这里的“可以互换”有一个边界:凡是语法规则要求 QueryClause 的位置,都可以放入以 bool、match、term 或其他查询类型为 QueryType 的查询子句。反过来,bool 也只能出现在这些 QueryClause 插槽中。
同一个查询子句位置既可以选择 bool 这类组合查询,也可以选择叶子查询。这不意味着 bool 可以替代 Number、SortValue,或字段名 "query"、"must"。
主轨道:请求对象的边界
SearchRequestBody 的主干只有三个部分:左大括号、成员、右大括号。
图 1 和图 2 的主轨道先经过左大括号,再经过可选的成员列表,最后抵达右大括号。规则中的 ("," TopLevelMember)* 表明第一个成员之后还可以添加“逗号 + 新成员”。
非终结符展开:保留入口,另画子轨道
TopLevelMember 不是要原样输入的文本,而是主轨道上的一个非终结符入口。展开它时,可以在旁边画一条名为 TopLevelMember 的子轨道,也可以像图 2 那样在同一个 Mermaid 代码块中定义多条规则。主轨道上仍保留非终结符节点,不用子轨道替换或挤占原图。
因此,展开不是“在原图上再走一遍”,而是“从占位节点跳到它对应的定义”。规则较复杂时,把层级关系写进 EBNF、节点标签和路径示例,比继续增加图更容易保持整体结构。
这一步可以按分层关系阅读,不需要增加新的图:
- 先画
SearchRequestBody的主轨道,把TopLevelMember保留为占位节点。 - 再画
TopLevelMember的子轨道,把它展开为query、sort、from和size四个候选形状。 - 选择
query分支后,继续跳到QueryClause的子轨道;QueryClause再选择BoolClause时,继续进入BoolBody、BoolMember和ChildQueryValue。
TypeSpecificBody 必须匹配选定的 QueryType,例如选择 bool 后必须使用 BoolBody。它们不是两个可任意交叉搭配的选择,所以精确文法把二者写在 BoolClause 等配对分支中,而不是分别写成两个独立的选择。
主轨道、TopLevelMember 和 QueryClause 不是三条需要首尾相接的执行流程,而是同一套语法的三层定义。沿着主轨道遇到非终结符时,进入它对应的定义;定义走完后,回到外层轨道的下一个节点。
回路(loop):重复成员
图 1 和图 2 都保留了空对象旁路。如果选择成员轨道,先经过一个 TopLevelMember;之后每再走一次包含逗号的返回轨道,就增加一个新成员。
因此,下面的请求在主轨道上是合法的:
1 | |
query、from 和 size 是三个不同的 TopLevelMember,它们可以沿着同一条回路(loop)依次出现。
分支:选择成员形状
TopLevelMember 有多个候选形状,因此从选择点分出多条轨道:
从入口选择一条轨道,经过字段名、冒号和字段值,再汇合到出口。字段名终结符中显示的双引号属于实际 JSON;EBNF 中用于界定字面量的引号则是文法记号。本文其他 EBNF 简写省略了这一词法层细节。
图 2 的第二条规则已经把这四种代表性分支画在 TopLevelMember 入口后面。
分支的含义是“选其中一个形状”,不是“这些字段都必须出现”。字段是否可选、是否能重复,需要由外层轨道表达。这里的外层回路(loop)允许不同成员继续出现,但没有表达“同名字段能否重复”;如果规则禁止重复 key,就应在文字说明中明确这一点,不要让读者从回路(loop)猜出错误结论。
非终结符引用:递归子查询(recursion)
bool 的 must、filter、should 和 must_not 可以承载一个或多个查询子句。子查询本身仍然是 QueryClause,所以它可以再次选择 match、term 或另一个 bool。
ChildQueryValue 允许一个 QueryClause,或一个由逗号分隔的非空 QueryClause 数组;数组下方的返回轨道只负责重复数组元素。BoolMember 定义一个 bool 成员,不是完整 bool 对象,bool 还支持其他参数。图 3 表达的引用链是 QueryClause → BoolClause → BoolBody → BoolMember → ChildQueryValue → QueryClause。
BoolBody 用的是前面的可选分隔列表模式:"{" [BoolMember ("," BoolMember)*] "}"。递归体现在这些语法定义相互引用。分层绘制标准铁路图时,保留 QueryClause 这个非终结符节点即可,不需要画一根返回父图的线。
这里的递归路径需要按“字段名 → 字段值 → 字段值的语法类别”来读,而不是把每个单词都看成同一种节点:
1 | |
对应到 JSON:
1 | |
query 是顶层对象中的字段名;它的字段值是一个 QueryClause。这个 QueryClause 展开成一个对象,并选择 bool 作为 QueryType。bool 对应的 BoolBody 再展开出一个 BoolMember,即 must 字段,而 must 的字段值属于 ChildQueryValue。这里选择数组形式,数组中的每个元素又是一个 QueryClause;第一个元素选择 match,在叶子处结束。
上面的示例已经展示了“父 QueryClause 包含子 QueryClause”,但子句选择的是叶子类型 match。如果要展示 QueryClause 到 QueryClause 的递归,可以让 must 数组中的子句再次选择 bool:
1 | |
这里不能在 must 数组元素外再写一层 "query"。"query" 只属于最外层的 TopLevelMember;must 数组中的元素直接是 QueryClause。因此,递归路径是:
1 | |
第一层 QueryClause 选择 bool,must 引入第二层 QueryClause;第二层再次选择 bool,于是递归继续;当某一层选择 match、term 或 range 时,递归在叶子处结束。{ "query": { "bool": {} } } 中的 query 位于正确的顶层位置,但空 bool 没有通过 must、filter、should 或 must_not 引入子 QueryClause,因此不能用它展示递归链。
因此,QueryClause → bool → must → ChildQueryValue → QueryClause 只是省略中间类别后的简称,不是完整推导链。完整链路是 QueryClause → BoolClause → BoolBody → BoolMember → ChildQueryValue → QueryClause:外层 QueryClause 选择 BoolClause;BoolClause 引入 BoolBody;BoolBody 中选择 BoolMember;must 的值属于 ChildQueryValue;ChildQueryValue 再得到一个 QueryClause。如果子 QueryClause 再选择 bool,这条链可以继续递归;如果选择 match、term 或 range,就在叶子查询结束。
图 3 的虚线表示“引用同一个 QueryClause 定义”,属于关系图的注释约定。它不表示标准铁路图中的重复轨道,也不表示查询执行时无限循环。每次嵌套对应一个实际的子查询对象,有限输入的展开也必须结束。
例如,下面的 bool 嵌套了两个叶子查询:
1 | |
读图时先从根部选择 bool,进入 must,再选择数组中的第一个 QueryClause。第一个子句走到 match 叶子后结束;数组中的第二个元素重新进入同一个 QueryClause 入口,选择 term 叶子。
重复(repetition)和递归(recursion)确实需要区分,但它们描述语法结构,不对应两种标准回边。图 1 和图 2 的返回轨道表示同层成员的重复;数组元素的重复也使用同一种铁路图构件。
ChildQueryValue 中的 QueryClause 是对语法定义的引用。沿着 bool 查询体继续展开后再次引用 QueryClause,才形成递归。标准铁路图会把这类引用保留为非终结符节点;图 3 另用虚线说明引用链,不能把它当成铁路图特有的“递归回边”。
这里不采用 repetition loop 与 back-edge loop 两分法,也不靠实线、虚线判断重复和递归。若使用“回边”一词,只把它当作“向前方入口返回的线”的口语描述;它也不能直接等同于 DFS 或 JVM 控制流中具有各自定义的 back edge。“回边计数器”属于对应算法或虚拟机的语境,应按那里实际统计的边来理解。
从轨道反推 JSON
验证轨道图时,沿着一条路径生成一个具体实例。分支记录选择,回路(loop)增加重复结构,非终结符继续展开。
最小的 match 查询对应这条路径:
1 | |
对应的 JSON 是:
1 | |
主轨道再走一次并选择 size:
1 | |
得到:
1 | |
轨道图把“这个字段放在哪一层”变成路径问题。size 走的是 SearchRequestBody 的主轨道,不能塞进 match 的 MatchBody;must 走的是 bool 的子查询槽位,不能与 query 并列成同一个根查询类型。
用轨道图定位常见误读
把同级参数看成执行顺序
图上从 query 走到 sort,不表示先执行查询再执行排序。它只表示 JSON 对象可以先写 query,再写 sort。把两个字段交换顺序,仍然可以得到同样结构的请求。
把分支看成同时发生
TopLevelMember 的分支是“选择一个成员形状”。一次经过选择点只能走一条分支;要同时拥有 query 和 size,必须经过带逗号的返回轨道,再展开一次 TopLevelMember。
把递归引用(recursive reference)看成无限循环
QueryClause → BoolClause → BoolBody → BoolMember → ChildQueryValue → QueryClause 是语法递归。它表达“查询可以嵌套查询”,不代表 Elasticsearch 会无限执行。真正的输入总是有限 JSON,解析树也会在叶子查询或其他终止节点结束。
把占位符当成字面量
QueryClause、Number 和 SortValue 不是要原样输入的字符串。只有带引号的 {、}、query、, 等才是字面量。把 QueryClause 写进 JSON,会得到一个示意文本,而不是可执行请求。
把代表性分支当成完整清单
图里常写 match / term / range / ...。省略号表示还有其他合法类型,不表示列表已经穷举;图题应标明这是代表性结构,而不是版本无关的完整规范。
SVG、Mermaid 铁路图与关系图的取舍
图 1 通过专用铁路图生成器绘制,使用顺序(sequence)、分支(choice)、可选(optional)和重复(one or more)构件。SVG 是独立矢量图片,无需浏览器运行 Mermaid,也能保留平滑弯轨和旁路。本文图源定义保存在仓库的 tools/render-query-dsl-railroads.mjs 中。
JSON.org 的对象规则可以用同样的可选分隔列表模式表示,忽略词法层的空白字符:
1 | |
这条规则与图 1 使用同一个结构:上方旁路对应 [ ... ],下方经过逗号的返回轨道对应重复部分。Value 可以继续展开成对象或数组,这种嵌套来自非终结符引用。
Mermaid 的两种用法
需要从 EBNF 生成标准铁路图时,使用 Mermaid 11.16.0 起提供的 railroad-ebnf-beta。需要表达多条语法定义之间的引用关系时,可以使用 flowchart,但图题和正文必须说明它是关系图,不能把关系连线解读为铁路图的合法路径。
画图时保留三条约束:
- 铁路图的字面量、非终结符、选择和重复使用稳定的形状。
- 关系图的连线标明“引用”或“选择分支”,不伪装成可以从头走到尾的输入路径。
- 递归引用与表示重复的返回轨道分别说明。
如果关系图已经需要大量颜色图例、跨页连线和长段落注释,说明它承担了过多语法细节。把具体路径放回 EBNF 和铁路图,关系图只保留非终结符之间的引用。
画完后的验证
画完后,沿着图至少走出三条路径:
| 路径 | 应验证的内容 |
|---|---|
| 最短路径 | 能否生成最小合法输入 |
| 一次重复路径 | 回路(loop)是否真的能增加第二个元素 |
| 一次递归路径 | 子查询是否能回到同一语法入口并在叶子处结束 |
再检查四个边界:
- 是否把语法关系误画成执行顺序?
- 是否把可选项误画成必选项?
- 是否把递归误画成无条件循环?
- 是否把代表性示例误写成完整规范?
轨道图的核心不是“画得像铁路”,而是每条合法路径都能回译成一个输入实例,每个不合法结构都能指出卡在哪一段轨道上。用这个标准检查《查询与检索》中的 Query DSL 图,query、sort、bool 和 child QueryClause 各自处于哪一层,就能沿路径验证。


