一.前言
之前有个需求,是使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 服务端搜索请求。
1 /** 2 * 3 * @author fred 4 * 5 */ 6 public class SqlParser { 7 private final static String dbType = JdbcConstants.MYSQL; 8 private final static Logger logger = LoggerFactory.getLogger(SqlParser.class); 9 private SearchSourceBuilder builder; 10 11 public SqlParser(SearchSourceBuilder builder) { 12 this.builder = builder; 13 } 14 /** 15 * 将SQL解析为ES查询 16 */ 17 public SearchSourceBuilder parse(String sql) throws Exception { 18 if (Objects.isNull(sql)) { 19 throw new IllegalArgumentException("输入语句不得为空"); 20 } 21 sql = sql.trim().toLowerCase(); 22 List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType); 23 if (Objects.isNull(stmtList) || stmtList.size() != 1) { 24 throw new IllegalArgumentException("必须输入一句查询语句"); 25 } 26 // 使用Parser解析生成AST 27 SQLStatement stmt = stmtList.get(0); 28 if (!(stmt instanceof SQLSelectStatement)) { 29 throw new IllegalArgumentException("输入语句须为Select语句"); 30 } 31 SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) stmt; 32 SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery(); 33 SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery; 34 35 SQLExpr whereExpr = sqlSelectQueryBlock.getWhere(); 36 37 // 生成ES查询条件 38 BoolQueryBuilder bridge = QueryBuilders.boolQuery(); 39 bridge.must(); 40 41 QueryBuilder whereBuilder = whereHelper(whereExpr); // 处理where 42 bridge.must(whereBuilder); 43 SQLOrderBy orderByExpr = sqlSelectQueryBlock.getOrderBy(); // 处理order by 44 if (Objects.nonNull(orderByExpr)) { 45 orderByHelper(orderByExpr, bridge); 46 } 47 builder.query(bridge); 48 return builder; 49 }
主流程很简单,拿到SQL字符串后,直接通过Druid API将其转换为抽象语法树,我们要求输入语句必须为Select语句。接下来是对where语句和order by语句的处理,
目前的难点其实主要在于如何将where语句映射到ES查询中。
先从简单的看起,如何处理order by呢?SQL语句中 order by显然可以允许用户根据多字段排序,所以排序字段肯定是一个List<排序字段>,我们要做的就是将这个List映射到
SearchSourceBuilder对象中。见下面代码:
1 /** 2