Skip to main content

自定义指标 - js function 简介

使用前提:该功能需要实施人员非常熟悉本系统的 Logicform,并且具备 js 开发能力。

js function 能让你非常灵活地定义任何指标的计算。此功能的优势在于能够使用任何 SQL 力所不能及的算法,甚至调用外部 API 来得到任何复杂指标的运算结果。此功能的缺点在于在超大数据量下无法兼顾性能。请在实施时候考虑此点。

在配置 js function 的时候,需要配置runshouldSplitGroupby两个函数,其中shouldSplitGroupby为非必需。

run 函数配置

这个函数的作用是填写指标算法。需要输入一个 js 回调函数,返回计算后的数据结果。函数的案例如下 👇

async (logicform, schema, config, helperFunctions) => {
return [{ _id: '0', 复购率: 0.4 }];
};

其中,回调函数的参数有四个:

参数说明
logicform用户此次问句对应的 Logicform
schemalogicform 对应的 schema
config一些调用的上下文,如果在此函数里面需要执行 helperFunctions.execute 函数的话,需要把这个 config 传进去
helperFunctions是一个字典,暴露给外部的一些帮助函数和库,帮助函数列表见下文

帮助函数和库列表

函数说明
schemas系统内所有 schema
execute执行一个 logicform 并返回结果,需要使用 await 来调用此函数
lf2sql将一个 logicform 的 object 转化成 sqlString
toQueryString将一个 QueryType 的 object 转化成 sqlString
executeSQLCMD直接执行一个 sql 语句,需要使用 await 来调用此函数
momentnodejs 的 moment 库
underscorenodejs 的 underscore 库

js function 如何 debug

目前 js function 的 debug 方式略微有点不太方便,需要实施人员能够接触到服务器,在服务器内打开 docker container 的 log,执行命令如下:

docker logs -f yiask -n 100

打开 log 后,可以在 js function 里面多加点一点 console.log 语句,修改后保存。然后进行问答,此时在服务器控制台就可以看到相应的输出。例如,js function 可以这么写:

async (logicform, schema, config, helperFunctions) => {
console.log(logicform);
return [{ _id: '0', 复购率: 0.4 }];
};

js function 的返回值

返回值永远是一个 array of objects。每一个 object 必须要有_id 属性。如果是返回一个值,例如问题是:今年复购率。那么 array 里面放一行即可,_id 可以写 0。

async (logicform, schema, config, helperFunctions) => {
return [{ _id: '0', 复购率: 0.4 }];
};

如果返回一个列表,例如问题是:今年各产品复购率。那么返回的数据如下

async (logicform, schema, config, helperFunctions) => {
return [
{ _id: '产品1的ID', 复购率: 0.1 },
{ _id: '产品2的ID', 复购率: 0.2 },
{ _id: '产品3的ID', 复购率: 0.3 },
{ _id: '产品4的ID', 复购率: 0.4 },
];
};

shouldSplitGroupby 函数配置

某些情况下,如果问题带 groupby,例如每年复购率,那么 js function 可能没有办法兼顾到 groupby 的逻辑。这个时候,可以填写这个函数,让它返回 true。

返回 true 代表此次执行会被分裂成若干个函数分开调用。如果系统内有 4 年数据,那么此函数会被调用 4 次,每次对应某一年,这样的话就不需要考虑 groupby 的处理逻辑了。

注意:在数据量大的时候此方案特别慢。尽量让 run 函数运算逻辑可以兼顾到 groupby。

(logicform) => {
return false;
};

也可以根据 logicform 来动态判断什么时候需要关闭,什么时候保留开启。

实际案例

假设说,我们现在要计算一个复购率。此复购率的算法是购买次数>1 的用户量 / 总用户量

分析此功能,发现需要先计算购买次数>1 的用户量总用户量,对应的 SQL 是(clickhouse 语法):

SELECT countIf(c > 1) / count() AS `复购率`
FROM (
SELECT
user,
count() AS c
FROM sales
WHERE xxxx
GROUP BY user
)

经过此分析,我们需要通过 logicform 构造出此 SQL,并将 logicform 中的 query 传给 WHERE xxx 里面,具体 run 函数为:

async (logicform, schema, config, helperFunctions) => {
const { schemas, lf2sql, toQueryString, executeSQLCMD } = helperFunctions;

let whereClause = toQueryString(schema, logicform.query, schemas, lf2sql);
if (whereClause.length > 0) {
whereClause = `WHERE ${whereClause}`;
}

const sql = `
SELECT
'0' AS _id,
countIf(c > 1) / count() AS \`复购率\`
FROM (
SELECT
user,
count() AS c
FROM sales
${whereClause}
GROUP BY user
)
`;
console.log(sql); // debug语句

const result = await executeSQLCMD(sql);
console.log(result); // debug语句

return result;
};

实际案例2

场景:

现在有一张表存储库存变动明细,记录了每次出库入库

此时问"2023库存"就是将2023传入query,然后根据明细sum起来,得到最新的库存

但是问"每年库存"时,不做处理情况下,js代码会group by year,每年的明细加起来作为当年的库存,

并不符合我们的需求。

所以现在需要有一个函数,将group by year->2023、2022、2021、2020...然后分别传入query进行多次计算。

shouldSplitGroupby

(lf) => (lf.groupby || []).find((g) => g._id === "日期");

JS自定义指标

async (lf, _self, config, { execute, _ }) => {
const newLF = {
...lf,
preds: [
{
operator: "$sql",
type: "int",
name: lf.preds[0].name,
pred: "sum(if( category = '出库', -num , num))",
},
],
};
if (newLF.query && newLF.query.日期) {
delete newLF.query.日期.$gte;
}
const r = await execute(newLF, config);
return r;
};