一、背景
在数据治理时,经常会遇到个性化统计分析的场景:基于数据的某些属性进行组合筛选,只有符合条件的数据才进行统计分析。
传统的实现方式是:业务人员提供筛选条件,数据开发人员在ETL任务直接开发。这种方式主要有两个痛点:
- 需求上线周期长
开发人员须对ETL任务进行开发调试、发布上线,按天、周、月排期都有可能。 - 统计口径不直观
筛选条件都在ETL任务中,业务人员无法直观判断数据是否符合预期,往往在数据量波动比较明显的时候才会发现异常。
为了解决这些问题,数据治理平台提供了给数据打标签的功能:业务人员在数据治理平台上快速批量对数据打上个性化标签,数据开发人员只须筛选打上特定标签的数据,而无须直接对数据进行过滤筛选。通过这种方式,可以解决需求上线周期长、统计口径不直观的问题。
- 数据开发人员只须一次性开发ETL任务,基于标签进行数据筛选即可,具体的数据筛选交由业务人员在数据治理平台上操作。
- 业务人员在数据治理平台上面可以直观查看须统计的数据,可视化管理数据统计口径。
随着数据量的不断增长,单靠业务人员人工管理标签的工作会变得越来越繁琐:给10条数据打标签业务人员还可以接受,但要给成千上万条数据打标签时,正常人都会抗拒这种操作了。
所以基于规则自动给数据打标签的需求,自然就会提上议程。当业务人员需要的筛选条件简单时,数据治理平台可以快速开发功能进行支持,但随着业务的发展,筛选条件会变得越来越复杂。
为了支持这些条件,平台功能会变得越来越臃肿,而且还需要投入开发人力排期开发,无法快速满足日新月异的业务需求。
因此,经过调研之后,决定引入规则引擎Aviator
,平台基于规则引擎提供可视化配置功能,由业务人员进行自定义配置,平台根据业务配置自动匹配符合条件的数据,打上对应的标签。
本文不涉及规则引擎的调研过程,相关规则引擎的优劣对比不在本文讨论范围。本文主要介绍规则引擎Aviator
的基础语法,以及在数据治理中的使用实践。
二、规则引擎Aviator
2.1 简介
Aviator是一个高性能、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值。
官方地址:https://github.com/killme2008/aviatorscript
用户手册:https://www.yuque.com/boyan-avfmj/aviatorscript/cpow90
2.2 关键函数介绍
这里主要介绍关键函数:
字符串函数
函数名称 | 说明 |
---|---|
string.contains(s1,s2) | 判断 s1 是否包含 s2,返回 Boolean |
Sequence 函数(集合处理)
函数名称 | 说明 |
---|---|
seq.set(p1, p2, p3, …) | 创建一个 java.util.HashSet 实例,添加参数到这个集合并返回。 |
seq.contains_key(map, key) | 当 map 中存在 key 的时候(可能为 null),返回 true。对于数组和链表,key 可以是 index,当 index 在有效范围[0…len-1],返回 true,否则返回 false |
include(seq,element) | 判断 element 是否在集合 seq 中,返回 boolean 值,对于 java.uitl.Set 是 O(1) 时间复杂度,其他为 O(n) |
seq.every(seq, fun) | fun 接收集合的每个元素作为唯一参数,返回 true 或 false。当集合里的每个元素调用 fun 后都返回 true 的时候,整个调用结果为 true,否则为 false。 |
seq.not_any(seq, fun) | fun 接收集合的每个元素作为唯一参数,返回 true 或 false。当集合里的每个元素调用 fun 后都返回 false 的时候,整个调用结果为 true,否则为 false。 |
seq.some(seq, fun) | fun 接收集合的每个元素作为唯一参数,返回 true 或 false。当集合里的只要有一个元素调用 fun 后返回 true 的时候,整个调用结果立即为该元素,否则为 nil。 |
示例:
// 数据字段名与字段值的映射,字段值有可能为多个,所以统一为数组形式,方便处理
Map<String, List<String>> propNameValueMap = new HashMap<>(1);
tag.put("tagName", Arrays.asList("选项1", "选项4"));
// Aviator输入参数
Map<String, Object> env = new HashMap<>(1);
env.put("tag", propNameValueMap);
// Aviator表达式,判断Aviator输入参数中是否有指定属性
String checkPropExpression = "seq.contains_key( tag, 'tagName')";
// Aviator表达式,生成符合预期的选项值集合
String expectedValueExpression = "seq.set('选项1','选项2','选项3')";
// Aviator表达式,生成一个匿名函数,判断字段取值X是否在选项值集合中
String includeFunc = "lambda(x) -> include( seq.set('选项1','选项2','选项3'), x )";
// 数据字段值任意一个符合筛选条件即可:通过 seq.some 函数遍历字段取值,只要任意一个字段取值在选项值集合中即返回true
// 结果:true
System.out.println(AviatorEvaluator.execute("( seq.contains_key( tag, 'tagName') && seq.some( tag.tagNameCn, lambda(x) -> include( seq.set('选项1','选项2','选项3'), x ) end ) != nil )", env));
// 数据字段值全部符合筛选条件:通过 seq.every 函数,遍历字段取值,当且仅当所有字段取值都在选项值集合中才会返回true
// 结果:false
System.out.println(AviatorEvaluator.execute("( seq.contains_key( tag, 'tagName') && seq.every( tag.tagName, lambda(x) -> include( seq.set('选项1','选项2','选项3'), x ) end ) )", env));
// 数据字段值完全不符合筛选条件:通过 seq.not_any 函数,遍历字段取值,当且仅当所有字段取值都不在选项值集合中才会返回true
// 结果:false
System.out.println(AviatorEvaluator.execute("( seq.contains_key( tag, 'tagName') && seq.not_any( tag.tagName, lambda(x) -> include( seq.set('选项1','选项2','选项3'), x ) end ) )", env));
// Aviator表达式,生成一个匿名函数,判断数据字段值是否包含特定的选项值。注意:若选项值有多个,则字段值当中必须包含所有的选项值。
// 比如特定的选项值为两个:“人杰”、“鬼雄”,则字符串“生当作人杰,死亦为鬼雄”是符合要求的;若选项值变成三个“人杰”,“鬼雄”,“英雄”时,则字符串“生当作人杰,死亦为鬼雄”不再符合要求,因为少了“英雄”
String containFunc = "lambda(x) -> seq.every( seq.set('选项1'), lambda(v) -> string.contains(x, v) end )";
// 数据字段值全部包含特定选项值
// 结果:false。因为字段的第二个取值“选项2”不包含字符串“选项1”
System.out.println(AviatorEvaluator.execute("( seq.contains_key( tag, 'tagName') && seq.every( tag.tagName, lambda(x) -> seq.every( seq.set('选项1'), lambda(v) -> string.contains(x, v) end ) end ) )", env));
三、规则定义
3.1 数据匹配
目前业务侧暂只需要对数据进行匹配,不涉及数据计算。
因此基于常见的用户场景,数据治理平台抽象出两种运算符:比较运算符、范围运算符。
- 比较运算符
定义如何比较字段取值与预期值。平台目前提供了三种比较方式:等于
,不等于
,包含
。
- 范围运算符
由于字段取值与预期值都可能是多个,统一以数组形式存储数据,通过比较运算符
得到每个字段值与预期值的比较结果后,须通过范围运算符
定义多个比较结果如何进行组合运算,得到最终结果。平台目前提供了三个运算符:任意符合
,全部符合
,完全不符合
。
示例:
字段取值 | 比较运算符 | 范围运算符 | 预期值 | 匹配结果 | 说明 |
---|---|---|---|---|---|
[选项1, 选项4] | 等于 | 任意符合 | [选项1, 选项2, 选项3] | true | 只要字段取值任意一个在预期值当中,即为true。选项1 在预期值中,所以为true。 |
[选项1, 选项4] | 等于 | 全部符合 | [选项1, 选项2, 选项3] | false | 当且仅当字段取值全部在预期值当中,才为true。选项4 不在预期值中,所以为false。 |
[选项1, 选项4] | 等于 | 完全不符合 | [选项1, 选项2, 选项3] | false | 当且仅当字段取值全部不在预期值当中,才为true。选项1 在预期值中,所以为false。 |
[选项1, 选项4] | 不等于 | 任意符合 | [选项1, 选项2, 选项3] | true | 只要字段取值任意一个不在预期值当中,即为true。选项4 不在预期值中,所以为true。 |
[选项1, 选项4] | 不等于 | 全部符合 | [选项1, 选项2, 选项3] | false | 当且仅当字段取值全部不在预期值当中,才为true。选项1 在预期值中,所以为false。 |
[选项1, 选项4] | 不等于 | 完全不符合 | [选项1, 选项2, 选项3] | false | 当且仅当字段取值全部都在预期值当中,才为true。选项4 不在预期值中,所以为false。 |
["生当作人杰,死亦为鬼雄", "英雄难过美人关"] | 包含 | 任意符合 | [人杰, 鬼雄] | true | 只要字段取值任意一个包含所有的预期值,即为true。第一个字段取值包含了所有预期值,所以为true。 |
["生当作人杰,死亦为鬼雄", "英雄难过美人关"] | 包含 | 全部符合 | [人杰, 鬼雄] | false | 当且仅当所有的字段取值都包含所有的预期值,即为true。第二个字段取值不包含预期值,所以为false。 |
["生当作人杰,死亦为鬼雄", "英雄难过美人关"] | 包含 | 完全不符合 | [人杰, 鬼雄] | false | 当且仅当所有字段取值都不包含所有的预期值,即为true。第一个字段取值包含了所有预期值,所以为false。 |
3.2 业务逻辑
数据匹配只能针对单个字段,业务往往需要针对多个字段进行组合排列,根据多个字段的匹配结果进行取舍。
因此数据治理平台提供了逻辑运算符,对多个字段的匹配结果进行与或运算。
- 逻辑运算符
定义如何将多个字段的匹配结果进行与或运算,得到最终匹配结果。平台提供了两个运算符:与
,或
。
3.3 规则参数
基于数据匹配与业务逻辑,数据治理平台抽象出以下数据结构,方便业务进行个性化配置:
/**
* 标签规则条件,指定各表达式之间的逻辑关系
*
* @author chriscchen
* @createtime 2022-07-30
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@JsonInclude(Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TagRuleCondition {
/**
* 逻辑运算符,定义如何将规则条件与规则表达式进行与或运算 {@link LogicOperator}
*/
private String logic;
/**
* 嵌套的规则条件。当需要对多个字段进行组合排列时,须嵌套规则条件。
*/
private List<TagRuleCondition> conditions;
/**
* 规则表达式,可直接对字段进行数据匹配
*/
private List<TagRuleExpression> expressions;
}
/**
* 标签规则表达式
*
* @author chriscchen
* @createtime 2022-07-30
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@JsonInclude(Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TagRuleExpression {
/**
* 字段名
*/
private String name;
/**
* 比较运算符 {@link CompareOperator}
*/
private String comparator;
/**
* 范围运算符 {@link ScaleOperator}
*/
private String scale;
/**
* 预期值
*/
private List<Object> value;
}
示例:
假设有2条数据如下:
名称 | 状态 | 分类 | 厂商 |
---|---|---|---|
飞天 | 销售中 | 酱香型 | 茅台 |
五粮液 | 销售中 | 浓香型 | 五粮液 |
需要筛选状态为销售中
,并且分类为酱香型
的数据,生成的数据结构如下:
{
"logic": "AND",
"expressions": [
{
"name": "状态",
"comparator": "=",
"scale": "any",
"value": [
"销售中"
]
},
{
"name": "分类",
"comparator": "=",
"scale": "any",
"value": [
"酱香型"
]
}
]
}
3.4 表达式转换
基于3.3节的数据结构,参考2.2节的示例,生成Aviator
表达式
/**
* 根据用户配置的规则条件,生成Aviator的表达式
*
* @param condition 用户配置的规则条件
* @param keyPrefix 有嵌套属性时,需要加上属性前缀
* @return Aviator的表达式
*/
private static String convertToAviatorExpression(TagRuleCondition condition, String keyPrefix)
throws AbstractDataMapException {
if (condition == null) {
throw new CommonParamUnacceptableException("标签的规则条件不能为空");
}
if (StringUtils.isBlank(condition.getLogic())) {
throw new CommonParamMissedException("缺少逻辑运算符,请检查参数:logic");
}
List<String> aviatorExpressions = new ArrayList<>();
// 嵌套的规则条件。当需要对多个字段进行组合排列时,须嵌套规则条件
if (CollectionUtils.isNotEmpty(condition.getConditions())) {
for (TagRuleCondition subCondition : condition.getConditions()) {
aviatorExpressions.add(convertToAviatorExpression(subCondition, keyPrefix));
}
}
// 规则表达式,可直接对字段进行数据匹配
if (CollectionUtils.isNotEmpty(condition.getExpressions())) {
for (TagRuleExpression expression : condition.getExpressions()) {
String valueSet = generateValueSetExpression(expression);
// 检查属性是否存在
String check = "seq.contains_key( " + keyPrefix + expression.getType()
+ ", '" + expression.getName() + "')";
// 属性字段名
String propValue = keyPrefix + expression.getType() + "." + expression.getName();
generateTagExpression(aviatorExpressions, expression, valueSet, check, propValue);
}
}
switch (condition.getLogic()) {
case LogicOperator.AND:
return StringUtils.join(aviatorExpressions, " && ");
case LogicOperator.OR:
return StringUtils.join(aviatorExpressions, " || ");
default:
throw new CommonParamUnacceptableException(
"未知逻辑运算符,请检查参数:" + condition.getLogic());
}
}
/**
* 基于用户配置的规则表达式,生成预期值表达式
*
* @param expression 用户配置的规则表达式
* @return 预期值表达式
*/
private static String generateValueSetExpression(TagRuleExpression expression) {
List<Object> value = expression.getValue();
String valueSet;
if (CollectionUtils.isEmpty(value)) {
valueSet = "seq.set()";
} else {
Object elem = value.get(0);
if (elem instanceof String) {
// 字符串加上引号
valueSet = "seq.set('" + StringUtils.join(value, "','") + "')";
} else {
// 其余类型直接拼接
valueSet = "seq.set(" + StringUtils.join(value, ",") + ")";
}
}
return valueSet;
}
/**
* 生成标签类型的Aviator表达式
*
* @param aviatorExpressions 生成的Aviator表达式,添加到此数组中
* @param expression 用户配置的规则表达式数据结构
* @param valueSet 提前生成的预期值集合表达式
* @param check 提前生成的检测属性存在的表达式
* @param propValue 属性字段名
* @throws AbstractDataMapException exception
*/
private static void generateTagExpression(List<String> aviatorExpressions,
TagRuleExpression expression, String valueSet, String check, String propValue)
throws AbstractDataMapException {
if (StringUtils.isBlank(expression.getComparator())) {
throw new CommonParamMissedException("缺少比较运算符,请检查参数:operation");
}
if (StringUtils.isBlank(expression.getScale())) {
throw new CommonParamMissedException("缺少范围运算符,请检查参数:scale");
}
switch (expression.getComparator()) {
case CompareOperator.E: {
generateTagEqualExpression(aviatorExpressions, expression, valueSet, check, propValue);
break;
}
case CompareOperator.NE: {
generateTagNotEqualExpression(aviatorExpressions, expression, valueSet, check, propValue);
break;
}
case CompareOperator.CONTAIN: {
generateTagContainExpression(aviatorExpressions, expression, valueSet, check, propValue);
break;
}
default: {
throw new CommonParamUnacceptableException(
"未知比较运算符,请检查参数:" + expression.getComparator());
}
}
}
/**
* 生成标签类型 “等于” 的Aviator表达式
*
* @param aviatorExpressions 生成的Aviator表达式,添加到此数组中
* @param expression 用户配置的规则表达式数据结构
* @param valueSet 提前生成的预期值集合表达式
* @param check 提前生成的检测属性存在的表达式
* @param propValue 属性字段名
* @throws AbstractDataMapException exception
*/
private static void generateTagEqualExpression(List<String> aviatorExpressions,
TagRuleExpression expression, String valueSet, String check, String propValue)
throws AbstractDataMapException {
switch (expression.getScale()) {
case ScaleOperator.ANY: {
aviatorExpressions.add("( " + check + " && seq.some( " + propValue
+ ", lambda(x) -> include( " + valueSet + ", x ) end ) != nil )");
break;
}
case ScaleOperator.ALL: {
aviatorExpressions.add("( " + check + " && seq.every( " + propValue
+ ", lambda(x) -> include( " + valueSet + ", x ) end ) )");
break;
}
case ScaleOperator.NONE: {
aviatorExpressions.add("( " + check + " && seq.not_any( " + propValue
+ ", lambda(x) -> include( " + valueSet + ", x ) end ) )");
break;
}
default:
throw new CommonParamUnacceptableException(
"未知范围运算符,请检查参数:" + expression.getScale());
}
}
/**
* 生成标签类型 “不等于” 的Aviator表达式
*
* @param aviatorExpressions 生成的Aviator表达式,添加到此数组中
* @param expression 用户配置的规则表达式数据结构
* @param valueSet 提前生成的预期值集合表达式
* @param check 提前生成的检测属性存在的表达式
* @param propValue 属性字段名
* @throws AbstractDataMapException exception
*/
private static void generateTagNotEqualExpression(List<String> aviatorExpressions,
TagRuleExpression expression, String valueSet, String check, String propValue)
throws AbstractDataMapException {
switch (expression.getScale()) {
case ScaleOperator.ANY: {
aviatorExpressions.add("( " + check + " && seq.some( " + propValue
+ ", lambda(x) -> include( " + valueSet + ", x ) == false end ) != nil )");
break;
}
case ScaleOperator.ALL: {
aviatorExpressions.add("( " + check + " && seq.not_any( " + propValue
+ ", lambda(x) -> include( " + valueSet + ", x ) end ) )");
break;
}
case ScaleOperator.NONE: {
aviatorExpressions.add("( " + check + " && seq.every( " + propValue
+ ", lambda(x) -> include( " + valueSet + ", x ) end ) )");
break;
}
default:
throw new CommonParamUnacceptableException(
"未知范围运算符,请检查参数:" + expression.getScale());
}
}
/**
* 生成标签类型 “包含” 的Aviator表达式
*
* @param aviatorExpressions 生成的Aviator表达式,添加到此数组中
* @param expression 用户配置的规则表达式数据结构
* @param valueSet 提前生成的预期值集合表达式
* @param check 提前生成的检测属性存在的表达式
* @param propValue 属性字段名
* @throws AbstractDataMapException exception
*/
private static void generateTagContainExpression(List<String> aviatorExpressions,
TagRuleExpression expression, String valueSet, String check, String propValue)
throws AbstractDataMapException {
switch (expression.getScale()) {
case ScaleOperator.ANY: {
aviatorExpressions.add("( " + check + " && seq.some( " + propValue
+ ", lambda(x) -> seq.every( " + valueSet
+ ", lambda(v) -> string.contains(x, v) end ) end ) != nil )");
break;
}
case ScaleOperator.ALL: {
aviatorExpressions.add("( " + check + " && seq.every( " + propValue
+ ", lambda(x) -> seq.every( " + valueSet
+ ", lambda(v) -> string.contains(x, v) end ) end ) )");
break;
}
case ScaleOperator.NONE: {
aviatorExpressions.add("( " + check + " && seq.not_any( " + propValue
+ ", lambda(x) -> seq.every( " + valueSet
+ ", lambda(v) -> string.contains(x, v) end ) end ) )");
break;
}
default:
throw new CommonParamUnacceptableException(
"未知范围运算符,请检查参数:" + expression.getScale());
}
}
3.5 数据转换
规则引擎Aviator
需要将数据转换为Map<String, Object>
类型,作为输入参数。
// 生成原始数据
Map<String, Object> rawData = new ObjectMapper().convertValue(record, new TypeReference<Map<String, Object>>() {});
// 字段名与字段取值的映射 { propName -> [ propValue ] },字段取值统一以数组形式
Map<String, Object> nameValueMap = new HashMap<>(rawData.size()));
rawData.forEach((key, value) -> tagNameValueMap.put(key, Collections.singletonList(value)));
3.6 规则匹配
// 预编译表达式,提高效率
Expression aviatorExpression;
try {
aviatorExpression = AviatorEvaluator.compile("...");
} catch (Exception e) {
throw new CommonParamUnacceptableException("规则异常,请检查:" + e.getMessage(), e);
}
// 匹配数据
Boolean result = (Boolean) aviatorExpression.execute(nameValueMap);
四、总结
基于规则引擎Aviator
开发的自定义规则功能,既能让业务直接进行个性化规则配置,快速对匹配的数据打标签,也极大解放了平台开发的人力,从重复乏味的定制化功能开发抽身出来,为业务实现更有价值的功能。
本文只介绍自定义规则功能中的核心处理模块,还须基于数据结构进行前端开发,支持可视化配置,不再一一展开。