# 使用 DSL 实现自定义 Filter 查询

# DSL 初识

DSL「Domain Specific Language」,领域专用语言。

# 领域应用

# Form 表单 vs. Filter 过滤

# Form 表单

form表单

由于我们的产品是一个类 NPM 的产品,充斥着网络流量解析的各种参数,由于传统的 Form 表单无法支持 查询,给界面的跳转、下钻分析造成了极大的不便。

# Filter 过滤

在调研业界标杆产品中,发现使用 Filter 自定义组合查询条件十分方便。

  • Kibana

kibana

  • ExtraHop

ExtraHop

# 两者异同

功能点 Form Filter 备注
基本的查询
与查询 a=1 AND b=2
或查询 X a=1 OR a=2
关系操作符查询,例如:大于、小于 a>1
范围性查询,例如:10-20 可通过组合实现
a>=10 AND a=<20
多值查询,例如:80,8080 可通过组合实现
a=80 OR a=8080

# PEG.js

PEG.jsJavaScript 的简单解析器生成器,它会将我们用 PEG.js 语法编写的文件转换为可直接运行的 parser

# 转换思路

# Filter 界面

界面设计可以参考 ExtraHop 的实现。上面有截图。

# JSON 对象

界面操作生成一个 json, 结构定义为 IFilterCondition具体定义见伪代码实现

json 示例为:

const filterArr = [
  { field: "name", operator: "=", operand: "张三" },
  {
    operator: "OR",
    group: [
      {
        field: "province",
        operator: "=",
        operand: "北京"
      },
      {
        field: "province",
        operator: "=",
        operand: "山东"
      }
    ]
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通过一个方法把 json 转成 SPL

const filterCondition2Spl = (filter: IFilter) => {
  // json 转成 spl
};

const filterSpl = filterCondition2Spl(filterArr);
// name = "张三" AND (province = "北京" OR province = "山东")
1
2
3
4
5
6
伪代码实现

TS 定义可以为:

// index.tsx
import React from 'react';
interface IAdvancedFilterProps {
  visible: boolean;
  fields: IField[];
  condition?: IFilterGroup | IFilter;
  onFinish: (filter: IFilterGroup) => void;
  onCancel: () => void;
}

const AdvancedFilter: FC<IAdvancedFilterProps> = ({
  visible,
  fields,
  condition,
  onFinish,
  onCancel,
}) => {
  return (<div> something </div>)
}

export default AdvancedFilter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// typings.ts
/**
 * 字段的类型
 */
export enum EFieldType {
  'IPV4' = 'IPv4',
  'IPV6' = 'IPv6',
  'ARRAY' = 'Array',
  'ARRAY<IPv4>' = 'Array<IPv4>',
  'ARRAY<IPv6>' = 'Array<IPv6>',
}
/**
 * 操作数(字段值)类型
 */
export enum EFieldOperandType {
  /**
   * IP类型不可用在转sql中
   */
  'IP' = 'IP',
  'IPV4' = 'IPv4',
  'IPV6' = 'IPv6',
  'PORT' = 'port',
  'NUMBER' = 'number',
  'STRING' = 'string',
  'ENUM' = 'enum',
}
/**
 * 过滤的基本的等式
 */
export type IFilter = {
  id?: string;
  /**
   * 字段值
   */
  field: string;
  /**
   * 字段的名字
   */
  fieldText: string;
  type?: EFieldType;
  /**
   * 操作符号 
   */
  operator: EFilterOperatorTypes;
  operatorText?: string;
  /**
   * 操作数的值
   */
  operand?: string | number;
  /**
   * 操作数的名字
   * 枚举值可以显示名称
   */
  operandText?: string;
};
/**
 * 过滤组
 */
export type IFilterGroup = {
  id?: string;
  operator: EFilterGroupOperatorTypes;
  group: (IFilterGroup | IFilter)[];
};

export type IFilterCondition = (IFilterGroup | IFilter)[];

export enum EFilterGroupOperatorTypes {
  'AND' = 'AND',
  'OR' = 'OR',
  // 'NOT' = 'NOT',
}

/**
 * 操作符类型
 */
export enum EFilterOperatorTypes {
  EQ = '=',
  NEQ = '!=',
  GT = '>',
  EGT = '>=',
  LT = '<',
  ELT = '<=',
  LIKE = 'like',
  // IN = ' IN ',
  /**
   * 结果不为空
   */
  EXISTS = 'exists',
  /**
   * 结果为空
   */
  NOT_EXISTS = 'not_exists',
}

export interface IEnumObj {
  [propName: string]: string;
}

export interface List {
  label: string;
  value: string;
}

/**
 * 表格里面的枚举值
 */
export interface IEnumValue {
  value: string;
  text: string;
}

/**
 * 过滤条件中的字段
 */
export interface IField {
  title: string;
  dataIndex: string;
  /**
   * 字段的类型
   */
  type?: EFieldType;
  /**
   * 操作数的类型
   */
  operandType?: EFieldOperandType;
  enumValue?: IEnumValue[];
}

export interface ISelectOptionWithLabel {
  label: string;
  value: string;
}

/**
 * 搜索历史
 */
export interface ISearchHistory {
  /**
   * 根据过滤条件计算出来的 hash 值
   */
  id: string;
  /**
   * 搜索历史对外显示的文字
   */
  name: string;
  /**
   * 过滤条件
   */
  filter: (IFilter | IFilterGroup)[];
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

# SPL 解析成 ClickHouse SQL

转换器 spl2sql (opens new window)

由于 ClickHouseIPv4IPv6Array 类型的字段有不同的查询转换方法,所以转换器中做了很多不同类型的硬编码。

try {
  const sql = splToSqlConverter.parse(filterSpl)
} catch (error) {
  console.log(error);
}
1
2
3
4
5

# SPL 解析成 Elasticsearch DSL

转换器 spl2dsl (opens new window)

try {
  const dsl = splToDslConverter.parse(filterSpl)
} catch (error) {
  console.log(error);
}
1
2
3
4
5

结果得到一个 DSL query,转换成 base64,配合 ES Wrapper query (opens new window)即可。

# 参考