ToB企服应用市场:ToB评测及商务社交产业平台

标题: 字节跳动数据质量动态探查及相关前端实现 [打印本页]

作者: 天空闲话    时间: 2022-8-28 21:07
标题: 字节跳动数据质量动态探查及相关前端实现
更多技术交流、求职机会、试用福利,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群
 
需求背景

 
数据探查上线之前,数据验证都是通过写 SQL 方式进行查询的,从编写 SQL,到解析运行出结果,不仅时间长,还会反复消耗计算资源,探查上线后,只需要一次探查,就可以得到整张表的探查报告,但后续我们还发现了一些问题,主要有三点:
 
 
针对这些问题,我们进一步开发了动态探查需求,解决的问题如下:
 
 
本文主要介绍动态探查的应用场景和相关的技术实现。
 
应用场景

 
探查主要应用在元数据管理,数据研发,数仓的开发以及数据治理,可为对数据质量有需求的场景提供数据质量的发现和识别能力。目标用户除了研发同学,也包含不是以 SQL 研发为主的群体,比如算法建模和数据挖掘等领域。
 
探查可以有效的打通三个闭环:
 
 
 
名词解释

 
 
两者的对比示意图
 
 
 
技术实现

除了数据的抽样部分在后端做,其他的都是前端实现的。包括大数据展示,探查计算,卡片联动,操作栈交互,以及未来要做的函数编辑器以及 SQL 生成。
 
技术架构

 

目前 sql generator 有以下几种方式:
 
关键技术及实现

大数据渲染

由于动态探查场景下前端需要支持最大 5000 条数据的展示和交互,所以在渲染这块存在比较大的压力,主要集中在探查卡片和数据预览两个部分。
 
探查卡片包含了特定列的部分关键信息汇总,比如 0 值、Null 值、枚举值等,如下图红框部分:
 
图片探查卡片部分由于存在较多定制化内容,所以采用了虚拟列表方案进行渲染,支持收起状态和展开状态:
 
图片数据预览部分展示的是探查的全部数据集合,可以快速查看原始数据的详细内容,由于内容同质化比较高,所以数据预览采用的是基于团队内部维护的 canvas 版本 Table 方案进行渲染,如下图红框部分:
 
卡片联动

由于卡片和数据预览列的宽度差异较大,并且上下两部分滑动是独立的,造成在选择查看某个具体列的时候,上下对齐位置会比较麻烦,为了解决这个问题,这块增加了自动定位功能,演示效果如下:
 
这部分需要解决的问题有两个:卡片中间点坐标计算和自动定位逻辑。
 
中间点坐标计算逻辑如下:
  1. // 计算卡片中点坐标 index是卡片序号,adsorbSider表示是否吸边
  2. getCardCenter(index: number, adsorbSider?: boolean) {
  3.     ...
  4.     // 获取卡片信息
  5.     const cardBox: IBaseBox = this.cardList[index];
  6.     // 获取列信息
  7.     const colBox: IBaseBox = this.colList[index];
  8.     const clientWidth = getClientWidth();
  9.     if(adsorbSider) {
  10.       // 吸边处理
  11.       if(cardBox.offset < this.cardScroll) {
  12.         return cardBox.offset;
  13.       }
  14.       if(cardBox.offset + cardBox.width - this.cardScroll > clientWidth) {
  15.         return cardBox.offset + cardBox.width - clientWidth;
  16.       }
  17.       return this.cardScroll;
  18.     }
  19.     return getTargetPosition(colBox, this.tableScroll, cardBox);
  20. }
  21. // 获取滚动目标位置
  22. // originBox: 滚动起始对象
  23. // originScroll: 滚动起始左侧scroll
  24. // targetBox: 滚动结束对象
  25. const getTargetPosition = (originBox: IBaseBox, originScroll: number, targetBox: IBaseBox) => {
  26.   const clientWidth = getClientWidth();
  27.   if(!originBox || !targetBox) return 0;
  28.   let offsetLeftSider = Math.max(originBox?.offset - originScroll, 0);
  29.   if(offsetLeftSider + targetBox.width >= clientWidth) {
  30.     if(targetBox.offset + targetBox.width > clientWidth) {
  31.       // 此处容易出现吸边
  32.       return targetBox.offset + targetBox.width - clientWidth;
  33.     } else {
  34.       return 0;
  35.     }
  36.   }
  37.   const scroll = targetBox?.offset - offsetLeftSider + (targetBox.width - originBox.width) / 2;
  38.   return Math.max(
  39.     Math.min(targetBox.offset, scroll),
  40.     0
  41.   );
  42. }
复制代码
获取到中点坐标后,自动定位需要符合如下规则:
 
 
规则中有几种边界情况,参考下图:
 
居中对齐是对于卡片和列宽在 scroll 距离允许情况下的理想对齐方式,贴边对齐是针对卡片在起始和结束位置 scroll 不足以满足居中对齐要求时候的对齐方式,除此之外还有一种是卡片的宽度远大于列宽,并且不是起始或者结束位置的时候所采取的对齐方式,如下如卡片 B 因为无法滚动,卡片 A 的宽度又占据了底部第二列的一部分,所以此时卡片 B 只能高亮和底部的列进行对齐。
 
操作栈

动态探查支持了对于探查结果的基础分析能力,比如列删除、过滤、排序等,如下图红框部分:
 
图片用户对于探查结果的每一次操作都会被记作一次操作,多次操作串联起来形成操作栈,可以自由的修改或者删减操作栈里的操作,并实时查看最新结果,以过滤操作演示效果如下:
 
图片操作栈部分需要处理的问题主要有以下几点:
 
这里把所有操作都抽象成了Input + Logic = Ouput的结构,Input 是输入参数,此处可以是指某一列的数据、上一步操作的结果或者其他计算值,Logic 是操作的具体逻辑,负责根据 Input 转换生成 Output,Output 可以作为最终结果进行渲染,也可以再次进入下一环节参与计算,拿列删除操作举个栗子,下面是大体代码实现:
  1. class ColDelOpt {
  2.   run = (params: IOptEngineMetaInfo) => {
  3.     // 操作Input部分
  4.     const {
  5.       columns = [],
  6.       dataSourceMap = {}
  7.     } = params;
  8.     const {
  9.       fields = []
  10.     } = this.params;
  11.     // 操作Logic部分
  12.     const nextColumns = columns.filter((item) => !fields.includes(item.name));
  13.     // 操作的Output
  14.     return {
  15.       columns: nextColumns,
  16.       dataSourceMap
  17.     }
  18.   }
  19. }
复制代码
 
可以看到 ColDelOpt 内部有一个 run 方法,该方法支持传入一个包含了列信息 columns 和数据集 dataSourceMap 的 params 对象,此处 params 即被抽象的外部输入参数 Input,run 方法内部的逻辑部分即被抽象的 Logic 部分,最后方法返回值包含了最新的 columns 和 dataSourceMap,即为 Output 部分。基于这种结构,用户所有的操作都可以被初始化成不同的 Opt 实例,由操作引擎统一调用实例的 run 方法,并传入所需的参数,最终得到计算结果。
 
操作栈的计算是由计算引擎来完成的,引擎负责根据外部事件,来自动执行现有操作的数据处理工作,引擎执行流程和大体代码如下:
 
 
  1. // 操作引擎
  2. class OptEngine {
  3.   // 操作列表
  4.   private optList: IOptEngineItem[] = [];
  5.   // 原始数据
  6.   private metaData: IOptEngineMetaInfo = {
  7.     columns: [],
  8.     dataSourceMap: {},
  9.   };
  10.   // 执行算子
  11.   optRun = () => {
  12.     let {
  13.       columns = [],
  14.       dataSourceMap = {}
  15.     } = this.metaData;
  16.     if(!this.optList.length) return {
  17.       columns,
  18.       dataSourceMap
  19.     };
  20.     for(let index = 0; index < this.optList.length; index++) {
  21.       // 读取操作算子
  22.       const optItem = this.optList[index];
  23.       let startTime = performance.now();
  24.       try {
  25.         // 执行算子计算
  26.         const result = optItem.run({
  27.           columns,
  28.           dataSourceMap
  29.         });
  30.         // 更新算子结果
  31.         columns = result.columns || [];
  32.         dataSourceMap = result.dataSourceMap || {};
  33.       } catch(e) {
  34.         // 报错后直接直接返回
  35.         return {
  36.           columns,
  37.           dataSourceMap,
  38.           // 装填报错信息
  39.           errorInfo: {
  40.             key: optItem.key || '',
  41.             message: e.message
  42.           }
  43.         }
  44.       }
  45.     }
  46.     return {
  47.       columns,
  48.       dataSourceMap,
  49.     }
  50.   }
  51.   autoRun = (
  52.     metaInfo: IOptEngineMetaInfo,
  53.     optList: IOptItem[],
  54.     callback: (params: IAutoRunResult) => void
  55.   ) => {
  56.     // 装填数据
  57.     this.setupMetaData(metaInfo);
  58.     // 装填操作栈
  59.     this.setupOptList(optList.map((item) => {
  60.       // 行过滤
  61.       if(item.type === OPT_TYPE.FILTER) {
  62.         return new FilterOpt({
  63.           key: item.key,
  64.           params: item.params
  65.         })
  66.       }
  67.       // 其余类型操作
  68.       ...
  69.       // 默认原值返回
  70.       return new IdentityOpt({
  71.         key: item.key,
  72.       })
  73.     }));
  74.     // 执行操作计算
  75.     const result = this.optRun();
  76.     // 返回数据
  77.     return {
  78.       // 计算列
  79.       columns: result.columns,
  80.       // 执行结果
  81.       dataSource: Object.entries(result.dataSourceMap).map(([key, value]) => ({
  82.         field: key,
  83.         value
  84.       })),
  85.       // 操作栈执行异常信息
  86.       errorInfo: result.errorInfo
  87.     };
  88.   }
  89. }
复制代码
 
应用实践

以一个小例子来演示下动态探查的使用。前端开发过程中,有一个真实的场景,我们为了排查一个竖屏显示器的 bug(1080*1920),想找到关联的用户,看其分布情况,就可以很方便的用动态探查去寻找。
 
后续计划

 
关注动态探查的操作丰富性以及之后的数据走向,比如离线数据导出,和生成 SQL 等,技术方向上主要放在以下几个方面:


立即跳转火山引擎大数据研发治理套件官网了解详情!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4