一.前言

之前有个需求,是使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转换器(一)的更多相关文章

  1. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  2. 手写一个简单的starter组件

    spring-boot中有很多第三方包,都封装成starter组件,在maven中引用后,启动springBoot项目时会自动装配到spring ioc容器中. 思考: 为什么我们springBoot ...

  3. 手写一个简单的HashMap

    HashMap简介 HashMap是Java中一中非常常用的数据结构,也基本是面试中的"必考题".它实现了基于"K-V"形式的键值对的高效存取.JDK1.7之前 ...

  4. 手写一个简单到SpirngMVC框架

    spring对于java程序员来说,无疑就是吃饭到筷子.在每次编程工作到时候,我们几乎都离不开它,相信无论过去,还是现在或是未来到一段时间,它仍会扮演着重要到角色.自己对spring有一定的自我见解, ...

  5. jquery 手写一个简单浮窗的反面教材

    前言 初学jquery写的代码,陈年往事回忆一下. 正文 介绍一下大体思路 思路: 1.需要控制一块区域,这块区域一开始是隐藏的. 2.这个区域需要关闭按钮,同时我需要写绑定事件,关闭的时候让这块区域 ...

  6. 手写一个简单版的SpringMVC

    一 写在前面 这是自己实现一个简单的具有SpringMVC功能的小Demo,主要实现效果是; 自己定义的实现效果是通过浏览器地址传一个name参数,打印“my name is”+name参数.不使用S ...

  7. socket手写一个简单的web服务端

    直接进入正题吧,下面的代码都是我在pycharm中写好,再粘贴上来的 import socket server = socket.socket() server.bind(('127.0.0.1', ...

  8. 如何手写一个简单的LinkedList

    这是我写的第三个集合类了,也是简单的实现了一下基本功能,这次带来的是LinkedList的写法,需要注意的内容有以下几点: 1.LinkedList是由链表构成的,链表的核心即使data,前驱,后继 ...

  9. JQuery手写一个简单的轮播图

    做出来的样式: 没有切图,就随便找了一些图片来实现效果,那几个小星星萌不萌. 这个轮播图最主要的部分是animate(),可以先熟悉下这个方法. 代码我放到了github上,链接:https://gi ...

随机推荐

  1. vue知识点整理

    1.对于mvvm的理解 mvvm是model-view-viewModel vue是以数据为驱动的,vue自身将dom和数据进行绑定,一旦创建绑定,dom和数据将保持同步,每当数据发生变化,dom也会 ...

  2. 链表常见的题型(java实现)

    链表是面试中最常见的一种题型,因为他的每个题的代码短,短短的几行代码就可以体现出应聘者的编码能力,所以它也就成为了面试的重点. 链表常见的操作有1.打印链表的公共部分,2.删除链表的倒数第K个节点,3 ...

  3. Angular 表单嵌套、动态表单

    说明: 组件使用了ng-zorro (https://ng.ant.design/docs/introduce/zh) 第一类:嵌套表单 1. 静态表单嵌套 demo.component.html & ...

  4. MongoDB的基本操作(增删改查)

    ​ 目录 概念整理 数据库:一个MongoDB中可以建立多个数据库. 集合:MongoDB的文档组. 文档:实际存放数据的地方. 常见的操作 数据库(新增,删除) 集合(新增,编辑,删除) 文档(增删 ...

  5. 网关我选 Spring Cloud Gateway

    网关可提供请求路由与组合.协议转换.安全认证.服务鉴权.流量控制与日志监控等服务.可选的网关有不少,比如 Nginx.高性能网关 OpenResty.Linkerd 以及 Spring Cloud G ...

  6. 一款功能强大的TCP/UDP工具---flynet

    前言 前段时间做某个项目,由于涉及到tcp/udp方面的知识比较多,于是就索性趁热打铁,写个工具来强化相关知识.另外由于并非十分擅长Golang,所以也顺便再了解下Golang吧. 简介 flynet ...

  7. 用OllyDbg爆破一个小程序

    用OllyDbg爆破一个小程序 一.TraceMe小程序 TraceMe是对用户名.序列号判断是否合法的一个小程序.我们任意输入一组用户名.序列号进行check判断,结果如下: 二.用OllyDbg对 ...

  8. oracle表空间不足:ORA-01653: unable to extend table

    问题背景: oracle表空间不足报错是比较常见的故障,尤其是没有对剩余表空间做定期巡检的系统: 报错代码如下: oracle表空间不足错误代码:ORA-01653: unable to extend ...

  9. python requests-toolbelt 生成上传multipart/form-data格式数据

    需求背景 想使用requests做一个自动上传的功能,发现这里问题挺多的,就记录一下. 如上图上传功能,一般分为input标签,非input标签.我这里也不管什么标签,直接抓包看数据流. Conten ...

  10. bugku 程序员本地网站

    提示从本地访问,怎样让服务器认为你是从本地进行访问的: 使用burp抓包并在包中进行修改加入X-Forwarded-For: 127.0.0.1 X-Forwarded-For: 简称XFF头,它代表 ...