Byzer-lang 拥有声明式语言的特点,这和 SQL 非常类似。不同的是,Byzer-lang 也支持 Python 脚本,用户也可以使用 Scala/Java 动态开发和注册 UDF 函数,这使得其灵活度得到了很大提高。
Byzer-lang 针对大数据领域的流程抽象出了如下几个句法结构:
而针对机器学习领域,也做了类似的抽象:
此外,在代码复用上,Byzer-lang 支持脚本和包的管理。
-- 通过 set 语法 定义一个 json 数据
set abc='''
{ "x": 100, "y": 200, "z": 200 ,"dataType":"A group"}
{ "x": 120, "y": 100, "z": 260 ,"dataType":"B group"}
''';
-- load json string
load jsonStr.`abc` as table1;
-- 定义数据类型为 csv, 传入路径,保留 header,开启数据类型推断
load csv.`/tmp/upload/green_tripdata_2022-01.csv` where header='true' and inferSchema='true' as trip_data;
connect jdbc where
url="jdbc:mysql://127.0.0.1:3306/wow?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false"
and driver="com.mysql.jdbc.Driver"
and user="xxxx"
and password="xxxx"
as mysql_instance;
load jdbc.`mysql_instance.test1` where directQuery='''
select * from test1 limit 10
''' as newtable;
select * from newtable as output;
-- 创建
select 1 as col1 as table1;
-- 查询
select * from table1 as output1;
方式一:
select "" as features, 1 as label as mockData;
select #set($colums=["features","label"])
#foreach( $column in $colums )
SUM( case when `$column` is null or `$column`='' then 1 else 0 end ) as $column,
#end
1 as a from mockData as output1;
方式二:
select "" as features, 1 as label as mockData;
set sum_tpl = '''
SUM( case when `{0}` is null or `{0}`='' then 1 else 0 end ) as {0}
''';
select ${template.get("sum_tpl","label")}, ${template.get("sum_tpl","label")} from mockData as output1;
基本用法,以保存 json 为例:
set rawData='''
{"jack":1,"jack2":2}
{"jack":2,"jack2":3} ''';
load jsonStr.`rawData` as table1;
save overwrite table1 as json.`/tmp/jack`;
set rawData1='''
{"jack":1,"jack2":2}
{"jack":2,"jack2":3}
''';
load jsonStr.`rawData1` as table1;
save overwrite table1 as json.`/tmp/jack`;
set rawData2='''
{"jack":3,"jack2":4}
{"jack":4,"jack2":5}
''';
load jsonStr.`rawData2` as table2;
save append table2 as json.`/tmp/jack`;
load json.`/tmp/jack` as output;
set rawData1='''
{"jack":1,"jack2":2}
{"jack":2,"jack2":3}
''';
load jsonStr.`rawData1` as table1;
save overwrite table1 as json.`/tmp/jack`;
set rawData2='''
{"jack":3,"jack2":4}
{"jack":4,"jack2":5}
''';
load jsonStr.`rawData2` as table2;
save ignore table2 as json.`/tmp/jack`;
load json.`/tmp/jack` as output;
set rawData1='''
{"jack":1,"jack2":2}
{"jack":2,"jack2":3}
''';
load jsonStr.`rawData1` as table1;
save overwrite table1 as json.`/tmp/jack`;
set rawData2='''
{"jack":3,"jack2":4}
{"jack":4,"jack2":5}
''';
load jsonStr.`rawData2` as table2;
save errorIfExists table2 as json.`/tmp/jack`;
load json.`/tmp/jack` as output;
使用 save 语法保存 jdbc 数据
select 1 as a as tmp_article_table;
connect jdbc where
url="jdbc:mysql://127.0.0.1:3306/wow?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false"
and driver="com.mysql.jdbc.Driver"
and user="xxxxx"
and password="xxxxx"
as db_1;
save append tmp_article_table as jdbc.`db_1.crawler_table`;
train
顾名思义,就是进行训练,主要是对算法进行训练时使用。下面是一个比较典型的示例:
load json.`/tmp/train` as trainData;
train trainData as RandomForest.`/tmp/rf` where
keepVersion="true"
and fitParam.0.featuresCol="content"
and fitParam.0.labelCol="label"
and fitParam.0.maxDepth="4"
and fitParam.0.checkpointInterval="100"
and fitParam.0.numTrees="4"
;
/tmp/train
目录下的,数据格式为 JSON 的数据,并且给着这样表取名为 trainData;
trainData
为数据集,使用算法 RandomForest,将模型保存在 /tmp/rf
下;fitParam
指定的是参数组。其中 fitParam.0
表示第一组参数,用户可以递增设置 N 组,Byzer-lang 会自动运行多组,最后返回模型结果列表。run
的语义是对数据进行处理
load json.`/tmp/test` as testData;
run testData as TableRepartition.`` where partitionNum="5" as newdata;
在这个例子中,TableRepartition 是 Byzer 语言内置的算子,可以用于将 table 分区,可配置的参数如下:
参数名 | 参数含义 |
---|---|
partitionNum | 重新分区的分区数 |
partitionType | 重新分区的分区类型,现支持:hash、range |
partitionCols | 重新分区使用的列,当 partitionType 为 range 类型时,partitionCols 必须指定;shuffle为 false 时不允许指定 |
shuffle | 重新分区时是否开启 shuffle。true 为开启,false为关闭 |
predict
顾名思义,用于机器学习预测相关。比如上面的 train 示例中,用户将随机森林的模型放在了 /tmp/rf
目录下,用户可以通过 predict
语句加载该模型,并且对表 testData
进行预测。
示例代码如下:
predict testData as RandomForest.`/tmp/rf`;
register ScriptUDF.`` as plusFun where
lang="scala"
and udfType="udf"
and code='''
def apply(a:Double,b:Double)={
a + b
}
''';
上面代码的含义是,使用 ET ScriptUDF 注册一个函数叫 plusFun
,这个函数使用 Scala 语言,函数的类型是 UDF,对应的实现代码在 code 参数里。
在 Byzer-lang 中, 执行完上面代码后,用户可以直接在 select
语句中使用 plusFun
函数:
-- 创建数据表
set data='''
{"a":1}
{"a":2}
{"a":3}
{"a":4}
''';
load jsonStr.`data` as dataTable;
-- 在 SQL 中使用 plusfun
select plusFun(a,2) as res from dataTable as output;
set plusFun='''
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
class SumAggregation extends UserDefinedAggregateFunction with Serializable{
def inputSchema: StructType = new StructType().add("a", LongType)
def bufferSchema: StructType = new StructType().add("total", LongType)
def dataType: DataType = LongType
def deterministic: Boolean = true
def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0, 0l)
}
def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
val sum = buffer.getLong(0)
val newitem = input.getLong(0)
buffer.update(0, sum + newitem)
}
def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0, buffer1.getLong(0) + buffer2.getLong(0))
}
def evaluate(buffer: Row): Any = {
buffer.getLong(0)
}
}
''';
--加载脚本
load script.`plusFun` as scriptTable;
--注册为UDF函数 名称为plusFun
register ScriptUDF.`scriptTable` as plusFun options
className="SumAggregation"
and udfType="udaf"
;
set data='''
{"a":1}
{"a":1}
{"a":1}
{"a":1}
''';
load jsonStr.`data` as dataTable;
-- 使用plusFun
select a,plusFun(a) as res from dataTable group by a as output1;
set echoFun='''
import java.util.HashMap;
import java.util.Map;
public class UDF {
public Map apply(String s) {
Map m = new HashMap<>();
Integer[] arr = {1};
m.put(s, arr);
return m;
}
}
''';
load script.`echoFun` as scriptTable;
register ScriptUDF.`scriptTable` as funx
options lang="java"
;
-- 创建数据表
set data='''
{"a":"a"}
''';
load jsonStr.`data` as dataTable;
select funx(a) as res from dataTable as output;
register RandomForest.`/tmp/rf` as rf_predict;
select rf_predict(features) as predict_label from trainData
as output1;
register
语句的含义是: 将 /tmp/rf
中的 RandomForest 模型注册成一个函数,函数名叫 rf_predict.
register
后面也能接 where/options
子句:
register RandomForest.`/tmp/rf` as rf_predict
options algIndex="0"
-- and autoSelectByMetric="f1"
;
如果训练时同时训练了多个模型的话:
algIndex
可以让用户手动指定选择哪个模型autoSelectByMetric
则可以通过一些指标,让系统自动选择一个模型。内置算法可选的指标有: f1|weightedPrecision|weightedRecall|accuracy。f1
指标。在流式计算中,有 watermark 以及 window 的概念。我们可以使用 Register
句式来完成这个需求:
-- 为 table1 注册 watermark
register WaterMarkInPlace.`table1` as tmp1
options eventTimeCol="ts"
and delayThreshold="10 seconds";
set hello="world";
-- 然后使用 select 语法查看变量
select "value: ${hello}" as title as output1;
在 select 语句中使用变量作为表名(需要用反引号括起来)
set hello="world";
select "hello William" as title as `${hello}`;
select * from world as output;
通常在 Byzer-lang 中,生命周期分成以下部分:
set hello="world";
set hello="world" where scope="session";
然后在其他 cell 或 notebook 中便可以执行:
select "hello William" as title
as `${hello}`;
select * from world as output1;
Byzer-lang 的变量被分为五种类型:
text
conf
shell
sql
defaultParam
set hello="world";
conf
表示这是一个配置选项,通常用于配置系统的行为,比如:
set spark.sql.shuffle.partitions=200 where type="conf";
shell
,也就是 set
后的 key 最后是由 shell 执行生成的。
set date=`date` where type="shell";
select "${date}" as dt as output;
sql,这意味着 set
后的 key 最后是由 sql 引擎执行生成的。
set date=`select date_sub(CAST(current_timestamp() as DATE), 1) as dt` where type="sql";
select "${date}" as dt as output;
defaultParam
set hello="foo";
set hello="bar" where type="defaultParam";
select "${hello}" as name as output;
上述代码中后面的"bar"
会覆盖前面的 "foo"
。而 Byzer-lang 引入了 defaultParam
类型的变量来达到一种效果:如果变量已经设置了,新变量声明就失效,如果变量没有被设置过,则生效。
Byzer-lang 有非常完善的权限体系,可以轻松控制任何数据源到列级别的访问权限,而且创新性的提出了预处理时权限, 也就是通过静态分析 Byzer-lang 脚本从而完成表级别权限的校验(列级别依然需要运行时完成)。
但是预处理期间,权限最大的挑战在于 set
变量的解析,比如:
select "foo" as foo as foo_table;
set hello=`select foo from foo_table` where type="sql";
select "${hello}" as name as output;
在没有执行第一个句子,那么第二条 set
语句在预处理期间执行就会报错,因为此时并没有叫 foo_table
的表。
为了解决这个问题,Byzer-lang 引入了 compile/runtime
两个模式。如果用户希望在 set
语句预处理阶段就可以 evaluate 值,那么添加该参数即可。
set hello=`select 1 as foo ` where type="sql" and mode="compile";
如果希望 set
变量,只在运行时才需要执行,则设置为 runtime
set hello=`select 1 as foo ` where type="sql" and mode="runtime";
此时,Byzer-lang 在预处理阶段不会进行该变量的创建。
Byzer-lang 支持复杂的代码组织结构,这赋予了 Byzer-lang 强大的代码复用能力。
例如,lib-core
是 @allwefantasy 维护的一个 Byzer-lang Lib 库,里面有很多用 Byzer-lang 写成的一些功能。Byzer-lang 使用 Github 来作为 Lib 管理工具。
如果需要引入 lib-core
,可以通过如下方式:
include lib.`github.com/allwefantasy/lib-core`
where
-- libMirror="gitee.com"
-- commit="xxxxx"
-- force="true"
alias="libCore";
在上面的代码示例中,通过 include
引入了 lib-core
库,为了方便使用它,用户可以给其取了一个别名叫 libCore
。
除了 alias
参数以外,还有其他三个可选参数:
libMirror
:可以配置库的镜像地址。比如如果 gitee 也同步了该库,那么可以通过该配置使得国内下载速度加快。commit
:可以指定库的版本force
:决定每次执行的时候,都强制删除上次下载的代码,然后重新下载。如果你每次都希望获得最新的代码,那么可以开启该选项。引入该库后,就可以使用库里的包。假设用户希望在自己的项目中使用 lib-core
里一个叫 hello
的函数, 那么可以通过如下语法引入该函数:
include local.`libCore.udf.hello`;
引入后,就可以在 select
句式中使用该函数了:
select hello() as name as output;
include local.`github.com/allwefantasy/lib-core.udf.hello`;
如果是 Web 端的,请参考 Byzer Notebook 的使用功能手册 。如果是桌面版的,则使用 project
关键字,比如希望在 src/algs/a.byzer
中引入 src/algs/b.byzer
,那么可以在 a.byzer
中使用如下方式对 b.byzer
进行引用:
include project.`src/algs/b.byzer`;
如果你是在开发一个 Lib 库,那么必须使用 local + 全路径:
!pyInclude local 'github.com/allwefantasy/lib-core.alg.xgboost.py' named rawXgboost;
如果仅仅是在自己项目中互相引用,则使用 project + 全路径或者相对路径就可以:
!pyInclude project 'src/algs/xgboost.py' named rawXgboost;
set a = "wow,jack";
!if ''' split(:a,",")[0] == "jack" ''';
select 1 as a as b;
!else;
select 2 as a as b;
!fi;
select * from b as output;
set a="jack,2";
!if ''' select split(:a,",")[0] as :name, split(:a,",")[1] as :num;
:name == "jack" and :num == 3
''';
select 0 as a as b;
!elif ''' select split(:a,",")[1] as :num; :num==2 ''';
!if ''' 2==1 ''';
select 1.1 as a as b;
!else;
select 1.2 as a as b;
!fi;
!else;
select 2 as a as b;
!fi;
select * from b as output;
以上为 Byzer 语言基础语法的使用案例介绍,更多应用方式请参考 Byzer-lang 手册。
欢迎加入 Byzer 社区,也欢迎更多的小伙伴加入我们的Slack 社区讨论组,共同参与 Data & AI 的前沿话题讨论。