手写一个简单的ElasticSearch SQL转换器(一)
一.前言
之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql
插件,之所以决定手写一个,主要有两点原因:
1. 目前用的ES版本较老
2. elasticsearch-sql虽好,但比较复杂,代码也不易维护
3. 练练手
二.技术选型
目前主流软件中通常使用ANTLR做词法语法分析,诸如著名的Hibernate,Spark,Hive等项目,之前因为工作原因也有所接触,不过如果只是解析标准SQL的话,
其实还有更好的选择,如使用Hibernate或阿里巴巴的数据库Druid(Druid采用了手写词法语法分析器的方案,这种方式当然比自动ANTLR生成的解析器性能高得多), 这里
我选择了第二种方案。
开始之前先看下我们可以通过Druid拿到的SQL语言的抽象语法树:
图片:https://www.jianshu.com/p/437aa22ea3ca
三.技术实现
首先我们创建一个SqlParser类,主流程都在parse方法中,该方法负责将一个SQL字符串解析(顺便说一句,Druid支持多种SQL方言,这里我选择了MySQL),
并返回SearchSourceBuilder对象,这是一个ElasticSearch提供的DSL构建器,以该对象作为参数,ES client端即可发起对ES 服务端搜索请求。
- /**
- *
- * @author fred
- *
- */
- public class SqlParser {
- private final static String dbType = JdbcConstants.MYSQL;
- private final static Logger logger = LoggerFactory.getLogger(SqlParser.class);
- private SearchSourceBuilder builder;
- public SqlParser(SearchSourceBuilder builder) {
- this.builder = builder;
- }
- /**
- * 将SQL解析为ES查询
- */
- public SearchSourceBuilder parse(String sql) throws Exception {
- if (Objects.isNull(sql)) {
- throw new IllegalArgumentException("输入语句不得为空");
- }
- sql = sql.trim().toLowerCase();
- List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
- if (Objects.isNull(stmtList) || stmtList.size() != 1) {
- throw new IllegalArgumentException("必须输入一句查询语句");
- }
- // 使用Parser解析生成AST
- SQLStatement stmt = stmtList.get(0);
- if (!(stmt instanceof SQLSelectStatement)) {
- throw new IllegalArgumentException("输入语句须为Select语句");
- }
- SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) stmt;
- SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery();
- SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery;
- SQLExpr whereExpr = sqlSelectQueryBlock.getWhere();
- // 生成ES查询条件
- BoolQueryBuilder bridge = QueryBuilders.boolQuery();
- bridge.must();
- QueryBuilder whereBuilder = whereHelper(whereExpr); // 处理where
- bridge.must(whereBuilder);
- SQLOrderBy orderByExpr = sqlSelectQueryBlock.getOrderBy(); // 处理order by
- if (Objects.nonNull(orderByExpr)) {
- orderByHelper(orderByExpr, bridge);
- }
- builder.query(bridge);
- return builder;
- }
主流程很简单,拿到SQL字符串后,直接通过Druid API将其转换为抽象语法树,我们要求输入语句必须为Select语句。接下来是对where语句和order by语句的处理,
目前的难点其实主要在于如何将where语句映射到ES查询中。
先从简单的看起,如何处理order by呢?SQL语句中 order by显然可以允许用户根据多字段排序,所以排序字段肯定是一个List<排序字段>,我们要做的就是将这个List映射到
SearchSourceBuilder对象中。见下面代码:
- /**
- * 处理所有order by字段
- *
- * @param orderByExpr
- */
- private void orderByHelper(SQLOrderBy orderByExpr, BoolQueryBuilder bridge) {
- List<SQLSelectOrderByItem> orderByList = orderByExpr.getItems(); // 待排序的列
- for (SQLSelectOrderByItem sqlSelectOrderByItem : orderByList) {
- if (sqlSelectOrderByItem.getType() == null) {
- sqlSelectOrderByItem.setType(SQLOrderingSpecification.ASC); // 默认升序
- }
- String orderByColumn = sqlSelectOrderByItem.getExpr().toString();
- builder.sort(orderByColumn,
- sqlSelectOrderByItem.getType().equals(SQLOrderingSpecification.ASC) ? SortOrder.ASC
- : SortOrder.DESC);
- }
- }
通过Druid的API,我们很容易拿到了SQL语句中所有的排序字段,我们逐个遍历这些字段,拿到排序的列名字面量和顺序,传递给SearchSourceBuilder的sort方法,需注意的
是, 如果原始SQL中没有指定字段是顺序,我们默认升序。
接下来我们处理稍微有点麻烦的where语句,因为SQL语句被解析成了语法树,很自然的我们想到使用递归方式进行处理。 而通常在处理递归问题的时候,
我习惯于从递归的base case开始考虑,where语句中的运算符根据Druid API中的定义主要分为以下三种:
1. 简单二元运算符:包括逻辑处理,如and, or 和大部分关系运算(后续会详细讲)
2. between或not between运算符:我们可以简单的将其映射成ES中的Range Query
3. in, not in 运算符: 可以简单的映射成ES中的Term Query
通过Druid,我们可以很方便的获取每种运算中的运算符与操作数
- /**
- * 递归遍历“where”子树
- *
- * @return
- */
- private QueryBuilder whereHelper(SQLExpr expr) throws Exception {
- if (Objects.isNull(expr)) {
- throw new NullPointerException("节点不能为空!");
- }
- BoolQueryBuilder bridge = QueryBuilders.boolQuery();
- if (expr instanceof SQLBinaryOpExpr) { // 二元运算
- SQLBinaryOperator operator = ((SQLBinaryOpExpr) expr).getOperator(); // 获取运算符
- if (operator.isLogical()) { // and,or,xor
- return handleLogicalExpr(expr);
- } else if (operator.isRelational()) { // 具体的运算,位于叶子节点
- return handleRelationalExpr(expr);
- }
- } else if (expr instanceof SQLBetweenExpr) { // between运算
- SQLBetweenExpr between = ((SQLBetweenExpr) expr);
- boolean isNotBetween = between.isNot(); // between or not between ?
- String testExpr = between.testExpr.toString();
- String fromStr = formatSQLValue(between.beginExpr.toString());
- String toStr = formatSQLValue(between.endExpr.toString());
- if (isNotBetween) {
- bridge.must(QueryBuilders.rangeQuery(testExpr).lt(fromStr).gt(toStr));
- } else {
- bridge.must(QueryBuilders.rangeQuery(testExpr).gte(fromStr).lte(toStr));
- }
- return bridge;
- } else if (expr instanceof SQLInListExpr) { // SQL的 in语句,ES中对应的是terms
- SQLInListExpr siExpr = (SQLInListExpr) expr;
- boolean isNotIn = siExpr.isNot(); // in or not in?
- String leftSide = siExpr.getExpr().toString();
- List<SQLExpr> inSQLList = siExpr.getTargetList();
- List<String> inList = new ArrayList<>();
- for (SQLExpr in : inSQLList) {
- String str = formatSQLValue(in.toString());
- inList.add(str);
- }
- if (isNotIn) {
- bridge.mustNot(QueryBuilders.termsQuery(leftSide, inList));
- } else {
- bridge.must(QueryBuilders.termsQuery(leftSide, inList));
- }
- return bridge;
- }
- return bridge;
- }
上述第一种情况比较复杂,首先我们先看看运算符是逻辑运算的情况:
如下面的代码所示,如果运算符是逻辑运算符,我们需要对左右操作数分别递归,然后根据运算符类型归并结果:or可以映射成ES 中的Should,而and则映射成Must.
- /**
- * 逻辑运算符,目前支持and,or
- *
- * @return
- * @throws Exception
- */
- private QueryBuilder handleLogicalExpr(SQLExpr expr) throws Exception {
- BoolQueryBuilder bridge = QueryBuilders.boolQuery();
- SQLBinaryOperator operator = ((SQLBinaryOpExpr) expr).getOperator(); // 获取运算符
- SQLExpr leftExpr = ((SQLBinaryOpExpr) expr).getLeft();
- SQLExpr rightExpr = ((SQLBinaryOpExpr) expr).getRight();
- // 分别递归左右子树,再根据逻辑运算符将结果归并
- QueryBuilder leftBridge = whereHelper(leftExpr);
- QueryBuilder rightBridge = whereHelper(rightExpr);
- if (operator.equals(SQLBinaryOperator.BooleanAnd)) {
- bridge.must(leftBridge).must(rightBridge);
- } else if (operator.equals(SQLBinaryOperator.BooleanOr)) {
- bridge.should(leftBridge).should(rightBridge);
- }
- return bridge;
- }
下面来讨论下第一种情况中,如果运算符是关系运算符的情况,我们知道,SQL中的关系运算主要就是一些比较运算符,诸如大于,小于,等于,Like等,这里我还加上了
正则搜索(不过貌似性能比较差,ES对正则搜索的限制颇多,不太建议使用)。
- /**
- * 大于小于等于正则
- *
- * @param expr
- * @return
- */
- private QueryBuilder handleRelationalExpr(SQLExpr expr) {
- SQLExpr leftExpr = ((SQLBinaryOpExpr) expr).getLeft();
- if (Objects.isNull(leftExpr)) {
- throw new NullPointerException("表达式左侧不得为空");
- }
- String leftExprStr = leftExpr.toString();
- String rightExprStr = formatSQLValue(((SQLBinaryOpExpr) expr).getRight().toString()); // TODO:表达式右侧可以后续支持方法调用
- SQLBinaryOperator operator = ((SQLBinaryOpExpr) expr).getOperator(); // 获取运算符
- QueryBuilder queryBuilder;
- switch (operator) {
- case GreaterThanOrEqual:
- queryBuilder = QueryBuilders.rangeQuery(leftExprStr).gte(rightExprStr);
- break;
- case LessThanOrEqual:
- queryBuilder = QueryBuilders.rangeQuery(leftExprStr).lte(rightExprStr);
- break;
- case Equality:
- queryBuilder = QueryBuilders.boolQuery();
- TermQueryBuilder eqCond = QueryBuilders.termQuery(leftExprStr, rightExprStr);
- ((BoolQueryBuilder) queryBuilder).must(eqCond);
- break;
- case GreaterThan:
- queryBuilder = QueryBuilders.rangeQuery(leftExprStr).gt(rightExprStr);
- break;
- case LessThan:
- queryBuilder = QueryBuilders.rangeQuery(leftExprStr).lt(rightExprStr);
- break;
- case NotEqual:
- queryBuilder = QueryBuilders.boolQuery();
- TermQueryBuilder notEqCond = QueryBuilders.termQuery(leftExprStr, rightExprStr);
- ((BoolQueryBuilder) queryBuilder).mustNot(notEqCond);
- break;
- case RegExp: // 对应到ES中的正则查询
- queryBuilder = QueryBuilders.boolQuery();
- RegexpQueryBuilder regCond = QueryBuilders.regexpQuery(leftExprStr, rightExprStr);
- ((BoolQueryBuilder) queryBuilder).mustNot(regCond);
- break;
- case NotRegExp:
- queryBuilder = QueryBuilders.boolQuery();
- RegexpQueryBuilder notRegCond = QueryBuilders.regexpQuery(leftExprStr, rightExprStr);
- ((BoolQueryBuilder) queryBuilder).mustNot(notRegCond);
- break;
- case Like:
- queryBuilder = QueryBuilders.boolQuery();
- MatchPhraseQueryBuilder likeCond = QueryBuilders.matchPhraseQuery(leftExprStr,
- rightExprStr.replace("%", ""));
- ((BoolQueryBuilder) queryBuilder).must(likeCond);
- break;
- case NotLike:
- queryBuilder = QueryBuilders.boolQuery();
- MatchPhraseQueryBuilder notLikeCond = QueryBuilders.matchPhraseQuery(leftExprStr,
- rightExprStr.replace("%", ""));
- ((BoolQueryBuilder) queryBuilder).mustNot(notLikeCond);
- break;
- default:
- throw new IllegalArgumentException("暂不支持该运算符!" + operator.toString());
- }
- return queryBuilder;
- }
到这里我们就完成了SQL转ES DSL的功能了(其实只是简单查询的转换),下面我们写几个Junit测试一下吧:
首先是简单的比较运算:
- public void normalSQLTest() {
- String sql = "select * from test where time>= 1";
- SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
- try {
- searchSourceBuilder = new SqlParser(searchSourceBuilder).parse(sql);
- } catch (Exception e) {
- e.printStackTrace();
- }
- System.out.println(searchSourceBuilder);
- SearchSourceBuilder builderToCompare = new SearchSourceBuilder();
- QueryBuilder whereBuilder = QueryBuilders.rangeQuery("time").gte("");
- BoolQueryBuilder briage = QueryBuilders.boolQuery();
- briage.must();
- briage.must(whereBuilder);
- builderToCompare.query(briage);
- assertEquals(searchSourceBuilder,builderToCompare);
- }
下面是输出的ES 查询语句:
- {
- "query" : {
- "bool" : {
- "must" : [
- {
- "range" : {
- "time" : {
- "from" : "",
- "to" : null,
- "include_lower" : true,
- "include_upper" : true,
- "boost" : 1.0
- }
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- }
- }
再来个带排序的:
- @Test
- public void normalSQLWithOrderByTest() {
- String sql = "select * from test where time>= 1 order by time desc";
- SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
- try {
- searchSourceBuilder = new SqlParser(searchSourceBuilder).parse(sql);
- } catch (Exception e) {
- e.printStackTrace();
- }
- System.out.println(searchSourceBuilder);
- SearchSourceBuilder builderToCompare = new SearchSourceBuilder();
- QueryBuilder whereBuilder = QueryBuilders.rangeQuery("time").gte("1");
- BoolQueryBuilder briage = QueryBuilders.boolQuery();
- briage.must();
- briage.must(whereBuilder);
- builderToCompare.sort("time",SortOrder.DESC);
- builderToCompare.query(briage);
- assertEquals(searchSourceBuilder,builderToCompare);
- }
between, in这些没什么区别,就不贴代码了,最后看看稍微复杂点儿,带逻辑运算的查询:
- @Test
- public void sqlLogicTest() {
- String sql = "select * from test where raw_log not like"+"'%aaa' && b=1 or c=0";
- SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
- try {
- searchSourceBuilder = new SqlParser(searchSourceBuilder).parse(sql);
- } catch (Exception e) {
- e.printStackTrace();
- }
- System.out.println(searchSourceBuilder);
- SearchSourceBuilder builderToCompare = new SearchSourceBuilder();
- QueryBuilder builder =QueryBuilders.matchPhraseQuery("raw_log","aaa");
- BoolQueryBuilder briage1 = QueryBuilders.boolQuery();//raw log not like
- briage1.mustNot(builder);
- BoolQueryBuilder briage2 = QueryBuilders.boolQuery(); //b=1
- briage2.must(QueryBuilders.termQuery("b","1"));
- BoolQueryBuilder briage3 = QueryBuilders.boolQuery(); // not like and b=1
- briage3.must(briage1).must(briage2);
- BoolQueryBuilder briage4 = QueryBuilders.boolQuery(); //c =0
- briage4.must(QueryBuilders.termQuery("c","0"));
- BoolQueryBuilder briage5 = QueryBuilders.boolQuery(); // not like and b =1 or c =0
- briage5.should(briage3).should(briage4);
- BoolQueryBuilder briage6 = QueryBuilders.boolQuery();
- briage6.must();
- briage6.must(briage5);
- builderToCompare.query(briage6);
- assertEquals(searchSourceBuilder,builderToCompare);
- }
下面是生成的查询语句:
- {
- "query" : {
- "bool" : {
- "must" : [
- {
- "bool" : {
- "should" : [
- {
- "bool" : {
- "must" : [
- {
- "bool" : {
- "must_not" : [
- {
- "match_phrase" : {
- "raw_log" : {
- "query" : "aaa",
- "slop" : 0,
- "boost" : 1.0
- }
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- },
- {
- "bool" : {
- "must" : [
- {
- "term" : {
- "b" : {
- "value" : "1",
- "boost" : 1.0
- }
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- },
- {
- "bool" : {
- "must" : [
- {
- "term" : {
- "c" : {
- "value" : "0",
- "boost" : 1.0
- }
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- }
- ],
- "disable_coord" : false,
- "adjust_pure_negative" : true,
- "boost" : 1.0
- }
- }
- }
四.总结
本篇文章主要讲述了如何使用Druid实现SQL语句转换ES DSL进行搜索的功能,后续文章中会陆续完善这个功能,实现诸如聚合查询,分页查询等功能。
手写一个简单的ElasticSearch SQL转换器(一)的更多相关文章
- 利用SpringBoot+Logback手写一个简单的链路追踪
目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...
- 手写一个简单的starter组件
spring-boot中有很多第三方包,都封装成starter组件,在maven中引用后,启动springBoot项目时会自动装配到spring ioc容器中. 思考: 为什么我们springBoot ...
- 手写一个简单的HashMap
HashMap简介 HashMap是Java中一中非常常用的数据结构,也基本是面试中的"必考题".它实现了基于"K-V"形式的键值对的高效存取.JDK1.7之前 ...
- 手写一个简单到SpirngMVC框架
spring对于java程序员来说,无疑就是吃饭到筷子.在每次编程工作到时候,我们几乎都离不开它,相信无论过去,还是现在或是未来到一段时间,它仍会扮演着重要到角色.自己对spring有一定的自我见解, ...
- jquery 手写一个简单浮窗的反面教材
前言 初学jquery写的代码,陈年往事回忆一下. 正文 介绍一下大体思路 思路: 1.需要控制一块区域,这块区域一开始是隐藏的. 2.这个区域需要关闭按钮,同时我需要写绑定事件,关闭的时候让这块区域 ...
- 手写一个简单版的SpringMVC
一 写在前面 这是自己实现一个简单的具有SpringMVC功能的小Demo,主要实现效果是; 自己定义的实现效果是通过浏览器地址传一个name参数,打印“my name is”+name参数.不使用S ...
- socket手写一个简单的web服务端
直接进入正题吧,下面的代码都是我在pycharm中写好,再粘贴上来的 import socket server = socket.socket() server.bind(('127.0.0.1', ...
- 如何手写一个简单的LinkedList
这是我写的第三个集合类了,也是简单的实现了一下基本功能,这次带来的是LinkedList的写法,需要注意的内容有以下几点: 1.LinkedList是由链表构成的,链表的核心即使data,前驱,后继 ...
- JQuery手写一个简单的轮播图
做出来的样式: 没有切图,就随便找了一些图片来实现效果,那几个小星星萌不萌. 这个轮播图最主要的部分是animate(),可以先熟悉下这个方法. 代码我放到了github上,链接:https://gi ...
随机推荐
- linux环境下Nginx的配置及使用
切换到目录/usr/local/nginx/sbin,/usr/local为nginx的默认安装目录 #启动 ./nginx #查看命令帮助 ./nginx -h 验证配置文件状态 ./nginx - ...
- 使用servlet+jdbc+MD5实现用户加密登录
/** * 分析流程: * 1.前端页面提交登录请求 * 2.被web.xml拦截,进入到LoginServlet(有两种方式:方式一,在web.xml文件中配置servlet拦截器;方式二,不用在w ...
- 十分钟快速学会Matplotlib基本图形操作
在学习Python的各种工具包的时候,看网上的各种教程总是感觉各种方法很多很杂,参数的种类和个数也十分的多,理解起来需要花费不少的时间. 所以我在这里通过几个例子,对方法和每个参数都进行详细的解释,这 ...
- vue 条件渲染方式
1.通过class绑定 <div :class="{'div-class': this.align == 'center'}"></div> 对应的css ...
- MongoDB 基础教程CURD帮助类
最近两天在学习MongoDB,强大的文档数据库.给我最大的感觉就是相比于SQL或者MSQ等传统的关系型数据库,在使用和配置上真的是简化了很多.无论是在集群的配置还是故障转移方面,都省去了许多繁琐的步骤 ...
- python正则表达式贪婪算法与非贪婪算法与正则表达式子模式的简单应用
先引入一下百度百科对于正则表达式的概念: 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符.及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种 ...
- B/S 端 WebGL 3D 游戏机教程
前言 摘要:2D 的俄罗斯方块已经被人玩烂了,突发奇想就做了个 3D 的游戏机,用来玩俄罗斯方块...实现的基本想法是先在 2D 上实现俄罗斯方块小游戏,然后使用 3D 建模功能创建一个 3D 街机模 ...
- js 验证数据类型的4中方法
1.typeof 可以检验基本数据类型 但是引用数据类型(复杂数据类型)无用: 总结 : typeof 无法识别引用数据类型 包括 bull; 2.instanceof是一个二元运算符,左操作数 ...
- LeetCode正则表达式匹配
题目描述 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 ...
- 快学Scala 第十七课 (trait 入门)
trait 入门: trait类似于java的接口,不过比java接口功能更强大,可以有实体成员,抽象成员,实体方法,抽象方法. 如果需要混入的特质不止一个用with关键字. 带有特质的对象:(特质可 ...