在SQL中使用UDF
操作场景
Elasticsearch中的SQL能力是基于开源OpenDistro中的SQL插件,并且在该插件上进行增强。本文介绍基于OpenDistro的SQL插件和Elasticsearch的Painless实现SQL支持UDF的特性。
这里的UDF功能为将索引中的某个字段的值做一些计算、转换后得到的自定义字段,例如:
- 将firstname字段和lastname字段进行拼接返回一个叫name的自定义字段。
- 将某数值字段的值乘2后返回新字段。
- 将某字符串字段的值转为大写。
前提条件
安全模式下需要根据当前的鉴权模式进行如下权限配置(普通模式请忽略):
- 在基于用户和角色的鉴权模式下,参考基于用户和角色的鉴权进行配置。
- 使用script创建UDF,用户需要在elasticsearch用户组中。
- 在SQL中使用UDF,用户需要在supergroup用户组中。
- 在Ranger鉴权模式下,参考添加Elasticsearch的Ranger访问权限策略进行配置, 并且在“Permissions”中额外勾选“script”权限和“read”权限。
UDF的编写及验证
UDF的编写在Elasticsearch中表现为创建一个stored script。
- 样例1:
# 创建 script calculate_multiplier POST _scripts/calculate_multiplier { "script": { "lang": "painless", "source": "doc[params['field']].value * params['multiplier'] " } } # 删除script calculate_multiplier DELETE _scripts/calculate_multiplier
- 样例2:
# 创建 script to_upper POST _scripts/to_upper { "script": { "lang": "painless", "source": "params['_source'][params['field']].toUpperCase()" } } # 删除script to_upper DELETE _scripts/to_upper
- lang:表示script支持的语言,script作为UDF使用时,lang需要指定为painless。
https://www.elastic.co/guide/en/elasticsearch/painless/7.10/painless-lang-spec.html
- source:用户自定义的UDF中的操作。
上述•样例1:中的source的含义为:获取参数field字段的内容,并且乘以参数multiplier的值。
上述•样例2:中的source的含义为:获取参数field字段的内容,并且将其转为大写。
source中可使用的操作方法可以参考如下链接:
https://www.elastic.co/guide/en/elasticsearch/painless/7.10/painless-api-reference-field-java-lang.html#painless-api-reference-field-String
https://www.elastic.co/guide/en/elasticsearch/painless/7.10/painless-api-reference-shared-java-lang.html#painless-api-reference-shared-Number
- 参数类型支持int、long、string、double四种类型。
- script id:上述实例中“calculate_multiplier”、“to_upper”为script的唯一标识,即script id。
若上述编写的script作为UDF在SQL中使用,script id仅支持字母、数字、中划线和下划线,不支持其他特殊字符。
- doc[]:表示从doc_values中获取指定字段的值,数值类型和keyword类型默认支持doc_values,text类型不支持doc_values。
- 如果需要在script中获取不支持docvalue的字段(例如text类型)的内容需要使用“_ params['_source']”,即在source中获取该字段的值。从_source中获取效率较低,应该尽量避免使用。
- UDF需要在具体的索引上进行操作,若使用了形如doc['my_field']的具体字段,需要确保使用到该UDF的索引中存在该my_field字段,否则在使用时候会遇到异常。
上述脚本是否可以按照预期运行,可以使用类似如下的命令进行验证。
建议在编写完stored script后验证script的有效性。
PUT accounts/_bulk?refresh {"index":{"_id":"1"}} {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"} {"index":{"_id":"2"}} {"account_number":6,"balance":5686,"firstname":"Hattie","lastname":"Bond","age":36,"gender":"M","address":"671 Bristol Street","employer":"Netagy","email":"hattiebond@netagy.com","city":"Dante","state":"TN"} {"index":{"_id":"3"}} {"account_number":13,"balance":32838,"firstname":"Nanette","lastname":"Bates","age":28,"gender":"F","address":"789 Madison Street","employer":"Quility","email":"nanettebates@quility.com","city":"Nogal","state":"VA"} GET accounts/_search { "script_fields": { " script filled": { "script": { "id": "to_upper", "params": { "field": "firstname" } } } } }
- accounts:为索引名称。
- to_upper:为stored script的名称。
- params中的内容为参数列表。
在SQL中使用UDF
本小节介绍如何在SQL中以UDF的形式使用script。
在SQL中使用UDF时,格式为:
UDF_calculate_multiplier(field=age,multiplier=2)
- UDF_ :在SQL中使用UDF的固定开头。
- calculate_multiplier:在Elasticsearch中创建的script的名称。仅支持字母、数字、中划线和下划线。
- 括号中的内容表示参数列表,key和value用等号隔开,多个参数用英文逗号隔开,不论是否包含参数,括号都需要添加。
- 在SQL中使用UDF时“UDF_”会作为标识UDF的关键字来使用,不支持查询以“UDF_”开头的字段名,以“UDF_”开头的字段会引起歧义,导致SQL中词法、语法分析异常。
UDF支持的场景列表
- SELECT中UDF计算结果作为自定义字段
POST _opendistro/_sql/ { "query":"SELECT UDF_calculate_multiplier(field=age,multiplier=2) FROM accounts" } POST _opendistro/_sql/ { "query":"SELECT UDF_to_upper(field=firstname) FROM accounts" }
- SELECT UDF与其他字段一起做查询
POST _opendistro/_sql/ { "query":"SELECT UDF_to_upper(field=firstname),firstname FROM accounts" }
- UDF支持别名
POST _opendistro/_sql/ { "query":"SELECT UDF_calculate_multiplier(field=age,multiplier=2) as doubleAge FROM accounts" }
- UDF支持和where条件组合使用
POST _opendistro/_sql/ { "query":"SELECT firstname, UDF_calculate_multiplier(field=age,multiplier=2) as udfalias FROM accounts WHERE age > 10" }
- UDF支持和limit条件组合使用
POST _opendistro/_sql/ { "query":"SELECT age, UDF_calculate_multiplier(field=age,multiplier=2) as pv FROM accounts limit 1" }
- 支持多个UDF一起使用
POST _opendistro/_sql/ { "query":" SELECT UDF_calculate_multiplier(field=age,multiplier=2),UDF_to_upper(field=firstname) FROM accounts" }
- 支持与group by一起使用
POST _opendistro/_sql/ { "query":" SELECT UDF_calculate_multiplier(field=age,multiplier=2) as pv FROM accounts WHERE age > 0 GROUP BY pv" }
- 支持作为sum()、avg()、count()、min()、max()聚合的参数,前提条件为该UDF的返回值必须为数值类型
POST _opendistro/_sql/ { "query":" SELECT sum(UDF_calculate_multiplier(field=age,multiplier=2)) FROM accounts" }
在SQL子查询中使用UDF
在SQL中使用UDF不支持多层嵌套,仅支持一层子查询,即将内层查询的结果作为外层的UDF的参数来使用。
如果UDF仍然需要作用在索引上,则需要在外层查询中增加一个参数“index=索引名”,用来指定该UDF在哪个索引上进行计算,该参数仅在外层SQL的UDF中生效,若在内层UDF中配置该参数将会被忽略。
“index=索引名”默认为所有索引,若SQL为嵌套查询时,必须配置该参数,如果没有配置则可能存在某个索引中不存在UDF中定义的字段而导致异常。
嵌套UDF的使用示例:
POST _opendistro/_sql/ { "query" : "SELECT UDF_calculate_multiplier(field=age,multiplier='num',index=accounts) FROM (SELECT account_number as num from accounts limit 3) as a" }
- 外层查询中仅支持SELECT和FROM,不支持与其他关键字组合使用。
- 内层查询的结果和在外层UDF中使用的内层查询的结果需要有别名,否则语法会有异常。
- 在外层UDF中使用的内层查询结果的别名应该唯一表示,避免歧义。
- 内层查询的结果作为外层查询的参数时,在外层的UDF中直接使用子查询的字段的别名即可。即上述例子中的“num”,而不是使用“a.num”。
在包含子查询的SQL中使用UDF时,由于UDF是将子查询的结果作为UDF的参数来使用,无法下推给Elasticsearch,需要将子查询的每一条结果都与Elasticsearch再交互一次,即在子查询中查询会放大,一条SQL会转换为内层查询获取的size条查询,为了保证查询性能,需要尽可能减少查询返回的size,该参数使用“opendistro.query.size_limit”来控制,默认值为“200”。