这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
零、序言:
本文记录和整理了本人在字节青训营中学习的一些所得所想,用于本人回顾和梳理相关知识点,也欢迎大家参考,一同学习。如果发现有问题或者错误,可以在下方留言或者私信我(^-^)
一、课程目标和重点内容
1.1 课程目标
- 了解规则引擎的组成部分和应用场景。
- 学习并掌握规则引擎的设计与实现原理。
- 明确一个规则引擎的设计目标,并完成各部分的设计与实现步骤拆解。
- 动手实现规则引擎项目,完成预定目标。
- [课外扩展] 结合其他课程,完成一个在线 规则引擎 服务。
1.2 课程重点
- 理解规则引擎的组成部分和应用场景
- 理解规则引擎的核心原理——编译原理的基础概念
- 规则引擎的设计 。明确设计目标、完成步骤拆解、完成各部分状态机的详细设计
- 规则引擎的实现。基于项目工程完成词法分析、语法分析、抽象语法树的执行功能
二、详细知识点介绍
2.1 认识规则引擎
规则引擎是一种嵌入在应用服务中的组件,可以将业务决策从应用程序代码中分离出来。
通过使用预定义的语义模块来编写业务决策。在执行时接受数据输入、解释业务规则,并做出决策。
解决了开发人员重复编码的问题,业务决策与服务本身解耦,提高服务的可维护性,缩短开发路径,提高效率
2.2 编译原理
2.2.1 编译
编译的过程发生了什么?
编译的过程就是把某种语言的源程序,在不改变语义的条件下,转换成另一种语言程序(目标语言程序)。
一般分为哪几个步骤,每个步骤的中间结果是什么?
规则引擎中的编译原理,一般为以下几个个阶段:
- 词法分析:把源代码字符串转换为词法单元(Token)。
- 语法分析:在词法分析的基础上识别出程序的语法结构。
- 抽象语法树:构造AST,表达式抽象化语法结构的树状表示。
- 类型检查:验证执行的结果是否为合适的数据类型。
- 目标代码生成(待定):
- 如果源代码编译后要在操作系统上运行,那目标代码就是汇编/机器代码。
- 如果编译后是在虚拟机里执行,那目标代码就可以不是汇编代码,而是一种解释器可以理解的中间形式的代码即可。
2.2.2 词法分析
词法分析指的是把源代码字符串转换为词法单元的过程。
词法如何表示?
正则文法。
词法分析阶段的输出是什么?
词法单元(Token),通常是标识符、关键字、符号等,比如+、A、* 等。
词法分析阶段是怎么做的?
通过确定的有限状态自动机 DFA | Deterministic Finite Automaton,确定的有限自动机就是一个状态机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。
词法分析可能会产生什么问题?
左递归问题,指的是在一个文法中存在一个非终结符号向自己递归的情况,这会导致语法分析器陷入无限递归的循环,分为直接左递归和间接左递归。
直接左递归
A -> Aɑ|β
间接左递归
A -> Bɑ|C
B -> Aβ|D
如何解决词法分析过程中产生的问题?
一个包含左递归的形式文法不能以简易的递归下降分析器进行语法分析,可以转换为右递归形式。
词法分析、语法分析常见的工具
在 TiDB 中,通过Lex & Yacc 是用来生成词法分析器和语法分析器的工具,而对Java来说,ANTLR 更为常见,Spark、Flink 的 SQL 解析就是使用了 ANTLR。
2.2.3 语法分析
词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构。
语法如何表示?
使用上下文无关语法或巴斯克范式来表示,具体实施时,可以参考 ANTLR4 的相关文档。
上下文无关语法、巴克斯范式怎么理解?
上下文无关语法 Context-Free Grammar
语言句子无需考虑上下文,就可以判断正确性,如下所示
...
a = 0;
...
这是一个赋值语句,无论此语句的前后是什么代码,此语句所代表的操作是确定的。即给变量a赋值等于0
上下文无关语法G:终结符集合T + 非终结符集合N + 产生式集合P + 起始符号S
G由T、N、S和P组成,由语法G推导出来的所有句子的集合称为G语言
终结符: 组成串的基本符号。可以理解为词法分析器产生的token集合。
非终结符: 表示token的的集合的语法变量。
产生式:表示形式,S : AB ,就是说S的含义可以用语法AB进行表达
S : AB
A : aA | ε
B : b | bB
展开(expand):将 P(A->u ) 应用到符号串 vAw 中,得到新串vuw
折叠(reduce):将 P(A->uu ) 应用到符号串 vuuw 中,得到新串 vAw
推导(derivate):符号串 u 应用一系列产生式,变成符号串 v ,则u =>v:S => ab | b | bb
巴科斯范式
BNF是描述上下文无关理论的一种具体方法,通过BNF可以实现上下文无关文法的具体化、公式化、科学化,是实现代码解析的必要条件。
<expr> ::= <expr> + <term>
| <expr> - <term>
| <term>
<term> ::= <term> * <factor>
| <term> / <factor>
| <factor>
<factor> ::= ( <expr> )
| Num
BNF本质上就是树形分解,分解成一棵抽象语法树
- 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
- 叶子节点叫做 终结符,非叶子节点叫做 非终结符。
语法分析阶段的输出是什么? 一般怎么表示?
语法分析就是在词法分析的基础上识别出程序的语法结构。其输出是抽象语法树(Abstract Syntax Tree,AST)。树的每个节点(子树)是一个语法单元,每个节点还可以有下级节点。
语法分析有哪些方式?什么是递归下降算法?
有递归下降算法、LL分析 和LR 分析等多种语法分析方式。
递归下降算法 (Recursive Descent Parsing)的基本思路就是按照语法规则去匹配 Token 串。
- 对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。
- 在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。
- 如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)。
2.2.4 抽象语法树
抽象语法树是什么?
抽象语法树(AST)是语法分析后的输出结果。通过树状结构来表示语法结果,树的每个节点(子树)是一个语法单元,每个节点还可以有下级节点。
抽象语法树如何执行?
执行方式可以视为一个递归的树状遍历。在数据库中,可以根据AST制定查询计划并优化。
2.2.5 类型检查
类型检查怎么做?有哪些方式?
拿到AST后,可以进行一些合法性检查和名称绑定。
类型检查规则有两种形式:综合和推导
类型综合(type synthesis):根据子表达式的类型构造出表达式的类型。它要求名字先声明再使用。
类型推导( type inference):根据一个语言结构的使用方式来确定该结构的类型。
类型检查根据进行时的区别,可以分为动态检查(即运行时检查)和静态检查(即编译时检查)。
类型检查什么时候做?有什么区别?
动态检查:发生在执行时的阶段,可以根据执行时的参数输入的值类型,在执行过程中进行类型检查。
静态检查:发生在表达式的编译阶段,需要提前声明参数的类型,在构建语法树阶段进行类型检查。
三、实践练习例子
编译原理与规则引擎在数据库中很常见,使用SQL的数据库需要将 SQL 语句文本按照预先定义的 SQL 语法规则进行解析,得到抽象语法树,后续才能对抽象语法树进行优化。而一些非关系性数据库也会通过规则引擎来提高系统的灵活性和扩展性。,比如Milvus,通过 Antlr 将 expression 的解析过程从业务代码中分离出来。以下本人将简单介绍两个实践项目,便于读者进一步了解规则引擎在数据库中的应用。
TiDB SQL Parser
PingCAP 家的 Talent Plan 项目这几年很火热,其中 TinySQL 是实现一个小型分布式数据库,和15-445,6.824的Lab类似,也分为多个 project,TinySQL proj2 中需要我们补充 SQL Parser 中关于 JoinTable 部分的语法。
TiDB SQL Parser 主要的功能是将 SQL 语句文本按照预先定义的 SQL 语法规则进行解析,并将其转换为抽象语法树(Abstract Syntax Tree, AST)。在 TinySQL 中,语法分析对应的 SQL 语法文件是 parser.y,goyacc 会根据这个文件生成对应的 SQL 语法解析器。可以参考 Mysql 和 TiDB 中 joined_table 的实现。
joined_table: {
table_reference {[INNER | CROSS] JOIN | STRAIGHT_JOIN} table_factor [join_specification]
| table_reference {LEFT|RIGHT} [OUTER] JOIN table_reference join_specification
| table_reference NATURAL [INNER | {LEFT|RIGHT} [OUTER]] JOIN table_factor
}
Milvus Expr Parser
Milvus 是一个为向量相似性搜索构建的向量数据库。其规则引擎是用于解析查询参数中的 expr,通过Antlr4 生成 expr parser。代码位于parser/planparserv2。Milvus 执行 hybrid search 时,前端 Proxy 完成表达式的解析形成 logical plan 抽象语法树,后端解析 logical plan 生成 physical plan ,前后端通过 logical plan 进行通信。physical plan 由 segcore 通过抽象语法树生成,最后由 segcore 执行 physical plan 返回最终结果。
search_param = {
"data": [[0.1, 0.2]],
"anns_field": "book_intro",
"param": {"metric_type": "L2", "params": {"nprobe": 10}},
"offset": 0,
"limit": 2,
"expr": "word_count <= 11000",
}
res = collection.search(search_param)