Elasticsearch 技术分析(五):如何经过SQL查询Elasticsearch

前言

这篇博文原本是想放在全系列的大概第5、六篇的时候再讲的,毕竟查询是在索引建立、索引文档数据生成和一些基本概念介绍完以后才须要的。当前面的一些知识概念全都讲解完以后再讲解查询是最好的,可是最近公司项目忙常常加班,毕竟年末了。可是不写的话我怕会越拖越久,最后会不了了之了,因此恰好上海周末下雪,天冷没法出门,就坐在电脑前敲下了这篇博文。由于公司的查询这块是我负责的因此我研究了比较多点,写起来也顺手些。那么进入正文。javascript

为何用SQL查询

前面的文章介绍过,Elasticsearch 的官方查询语言是 Query DSL,既然是官方指定的,说明最吻合 ES 的强大功能,为ES作支撑。那么咱们为何还用 SQL 查询?这是不是画蛇添足了呢?html

其实,存在毕竟有存在的道理,存在即合理。SQL 做为一个数据库查询语言,它语法简洁,书写方便并且大部分服务端程序员都清楚了解和熟知它的写法。可是做为一个 ES 萌新来讲,就算他已是一位编程界的老江湖,可是若是他不熟悉 ES ,那么他若是要使用公司已经搭好的 ES 服务,他必需要先学习 Query DSL,学习成本也是一项影响技术开发进度的因素并且不稳定性高。可是若是 ES 查询支持 SQL的话,那么也许就算他是工做一两年的同窗,他虽然不懂 ES的复杂概念,他也能很好的使用 ES 并且顺利的参加到开发的队伍中,毕竟SQL 谁不会写呢?前端

Elasticsearch-SQL

咱们正式介绍下咱们的主角 - Elasticsearch-SQL,Elasticsearch-SQL不属于 Elasticsearch 官方的,它是 NLPChina(中国天然语言处理开源组织)开源的一个 ES 插件,主要功能是经过 SQL 来查询 ES,其实它的底层是经过解释 SQL,将SQL 转换为 DSL 语法,再经过DSL 查询。java

Elasticsearch-SQL目前已经支持大概全部版本的 ES,并且最近的6.5.x的也在支持的范围了,因此能够看得出来维护的仍是蛮频繁的。node

安装插件

因为 ES 2.x 和 5.x 的版本区别(详细参考:版本选择),咱们安装 ES 插件是有点区别的,git

在 5.0以前的安装方式为:plugin install程序员

./bin/plugin install https://github.com/NLPchina/elasticsearch-sql/releases/download/2.4.6.0/elasticsearch-sql-2.4.6.0.zip

在5.0以后(包括6.x)的安装方式为:elasticsearch-plugin installgithub

./bin/elasticsearch-plugin install https://github.com/NLPchina/elasticsearch-sql/releases/download/5.0.1/elasticsearch-sql-5.0.1.0.zip

若是咱们安装不成功,咱们能够直接下载 Elasticsearch-SQL 插件的压缩包,而后解压,完成以后重命名文件夹为 sql ,放到 ES 的安装路径的 plugins目录中,例如:..\elasticsearch-6.4.0\plugins\sqlweb

完成此操做后,须要从新启动Elasticsearch服务器,不然会报错:Invalid index name [sql], must not start with '']; ","status":400}spring

前端可视化界面

Elasticsearch-SQL 插件提供了可视化的界面,方便你执行SQL查询,界面以下:

在 elasticsearch 1.x / 2.x,你能够直接访问以下地址:

http://localhost:9200/_plugin/sql/

而在 elasticsearch 5.x/6.x,这须要安装 node.js 和下载及解压site,而后像这样启动web前端:

cd site-server
npm install express --save
node node-server.js

查询语法

通过以上的操做以后,若是没出问题,如今就可使用 SQL 查询 ES 了,其中有些是正常的 SQL 语法,还有些是超越SQL 语法的,至关因而对 SQL 语法的加强,ES 的查询格式是:

http://localhost:9200/_sql?sql=select * from indexName limit 10

简单查询

先上个简单的查询语法:

SELECT fields from indexName WHERE conditions

能够看到,咱们之前的查询语句中,表名 tableName 的地方如今改成了索引名 indexName,若是有索引Type ,还能够这样写:

SELECT fields from indexName/type WHERE conditions

也能够同时查询索引的多个类型,语法以下:

SELECT fields from indexName/type1,indexName/type2 WHERE conditions

若是想知道当前SQL是如何将SQL解释为Elasticsearch 的Query DSL,能够这样经过关键字explain

http://localhost:9200/_sql/_explain?sql=select * from indexName limit 10

聚合类函数查询

select COUNT(*),SUM(age),MIN(age) as m, MAX(age),AVG(age)
  FROM bank GROUP BY gender ORDER BY SUM(age), m DESC

额外加强查询

Search

SELECT address FROM bank WHERE address = matchQuery('880 Holmes Lane') ORDER BY _score DESC LIMIT 3

Aggregations

  • range age group 20-25,25-30,30-35,35-40
SELECT COUNT(age) FROM bank GROUP BY range(age, 20,25,30,35,40)
  • range date group by day
SELECT online FROM online GROUP BY date_histogram(field='insert_time','interval'='1d')
  • range date group by your config
SELECT online FROM online GROUP BY date_range(field='insert_time','format'='yyyy-MM-dd' ,'2014-08-18','2014-08-17','now-8d','now-7d','now-6d','now')

地理查询

Elasticsearch 能够把地理位置、全文搜索、结构化搜索和分析结合到一块儿。而Elasticsearch-sql 也基本支持全部地理位置相关的查询,对应 Elasticsearch的章节内容为Geolocation

一、地理坐标盒模型过滤器

地理坐标盒模型过滤器(Geo Bounding Box Filter),指定一个矩形的顶部,底部,左边界和右边界,而后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间。

语法:

GEO_BOUNDING_BOX(fieldName,topLeftLongitude,topLeftLatitude,bottomRightLongitude,bottomRightLatitude)

示例:

SELECT * FROM location WHERE GEO_BOUNDING_BOX(center,100.0,1.0,101,0.0)

二、地理距离过滤器

地理距离过滤器( geo_distance ),以给定位置为圆心画一个圆,来找出那些地理坐标落在指定距离范围的文档。

语法:

GEO_DISTANCE(fieldName,distance,fromLongitude,fromLatitude)

示例:

SELECT * FROM location WHERE GEO_DISTANCE(center,'1km',100.5,0.5)

三、地理距离区间过滤器

范围距离过滤器(Range Distance filter),以给定位置为圆心,分别以两个给定的距离画圆,找出与指定点距离在给定最小距离和最大距离之间的点,和geo_distance filter的惟一差异在于Range Distance filter是一个环状的,它会排除掉落在内圈中的那部分文档。

语法:

GEO_DISTANCE_RANGE(fieldName,distanceFrom,distanceTo,fromLongitude,fromLatitude)

示例:

SELECT * FROM location WHERE GEO_DISTANCE_RANGE(center,'1m','1km',100.5,0.50001)

四、Polygon filter (works on points)

找出落在多边形中的点。 这个过滤器使用代价很大 。当你以为本身须要使用它,最好先看看 geo-shapes

语法:

GEO_POLYGON(fieldName,lon1,lat1,lon2,lat2,lon3,lat3,...)

示例:

SELECT * FROM location WHERE GEO_POLYGON(center,100,0,100.5,2,101.0,0)

五、GeoShape Intersects filter (works on geoshapes)

这里须要使用WKT表示查询时的形状。
语法:

GEO_INTERSECTS(fieldName,'WKT')

示例:

SELECT * FROM location WHERE GEO_INTERSECTS(place,'POLYGON ((102 2, 103 2, 103 3, 102 3, 102 2))

更多关于地理的查询能够参考这里

实战用法

咱们以本系列的第一篇教程中咱们建立的索引 nba来做示例,以下:

一、查询 nba 全部球队信息

http://localhost:9200/_sql?sql=select * from nba limit 10

查询结果:

二、查询当家球星是詹姆斯的球队信息

http://localhost:9200/_sql?sql=select * from nba where topStar  = "勒布朗·詹姆斯"

查询结果:

三、根据建队时间降序排列

http://localhost:9200/_sql?sql=select * from nba order by date desc

查询结果:

四、查询拥有总冠军超过5个的球队信息

http://localhost:9200/_sql?sql=select * from nba where championship  >= 5

查询结果:

五、查询总冠军数量分别在1-5,5-10,10-15,15-20范围之间球队的数量

http://localhost:9200/_sql?sql=SELECT COUNT(championship) FROM nba GROUP BY range(championship, 1,5,10,15,20)

查询结果:

固然还有更多的写法,具体实如今这里就很少诉了,感兴趣的读者能够本身搭建个项目而后尝试下,更多特点SQL写法能够参考这里:

Java实现

上面已经介绍了 Elasticsearch-SQL的安装和使用,那么咱们如何在项目中使用它,Elasticsearch-SQL底层是使用Java语言开发的,经过解析SQL 转换为 DSL 语言,而后得出查询结果,解析结果成key-value的固定格式返回。

引入依赖

使用前咱们须要先引入maven依赖

<dependency>
    <groupId>org.nlpcn</groupId>
    <artifactId>elasticsearch-sql</artifactId>
    <version>x.x.x.0</version>
</dependency>

版本号(x.x.x)须要和 Elasticsearch的版本对应上,具体的对应关系大体能够参考下图:

可是不是全部的版本,咱们均可以从Maven Repository里获取到,咱们若是直接从Maven 仓库里面只能获取以下几个版本的依赖,其中缺乏不少版本:

那若是咱们使用的是其余版本的 ES 如何解决依赖 jar包问题呢?还记得咱们开始下载插件解压后的sql文件夹吗?例如6.5.0版本的插件的解压后文件夹内容以下:

这里面就有咱们须要的 jar包,有了 jar包就好办了,咱们能够直接加入到项目中,固然最好的方式是上传到公司的私有仓库里面,而后经过pom文件依赖进来。

搭建项目

jar包问题解决以后就能够正式进入开发阶段了,新建一个springboot项目,引入各项依赖,一切准备就寻后,如何链接ES呢?

这里有两种方式能够实现咱们的功能,一个是经过JDBC的方式,链接数据库同样链接ES。还有一种就是经过 tansport client 方式。

JDBC的方式

代码示例

public void testJDBC() throws Exception {
        Properties properties = new Properties();
        properties.put("url", "jdbc:elasticsearch://192.168.3.31:9300,192.168.3.32:9300/" + TestsConstants.TEST_INDEX);
        DruidDataSource dds = (DruidDataSource) ElasticSearchDruidDataSourceFactory.createDataSource(properties);
        Connection connection = dds.getConnection();
        PreparedStatement ps = connection.prepareStatement("SELECT  gender,lastname,age from  " + TestsConstants.TEST_INDEX + " where lastname='Heath'");
        ResultSet resultSet = ps.executeQuery();
        List<String> result = new ArrayList<String>();
        while (resultSet.next()) {
              System.out.println(resultSet.getString("lastname") + "," + resultSet.getInt("age") + "," + resultSet.getString("gender"))
        }
        ps.close();
        connection.close();
        dds.close();
    }

这种方式是最直观的,用到了Druid链接池,因此咱们还须要在项目中引入druid依赖,并且须要注意依赖的版本,不然会报错。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.15</version>
</dependency>

这种方式很好理解,并且开发也方便,可是我在项目中应用了发现它有不少不足,因此我最后仍是本身看了下源码,经过API的方式从新封装调用。

API方式

其实 elasticsearch-sql 没有提供开发的 文档,并无介绍如何经过调用 Java API方式开发,咱们须要阅读 elasticsearch-sql 的源代码来发现它的service,而后包装成咱们须要的,经过阅读源码咱们发现了以下一个功能明显的Service类。

public class SearchDao {

    private static final Set<String> END_TABLE_MAP = new HashSet<>();

    static {
        END_TABLE_MAP.add("limit");
        END_TABLE_MAP.add("order");
        END_TABLE_MAP.add("where");
        END_TABLE_MAP.add("group");

    }

    private Client client = null;


    public SearchDao(Client client) {
        this.client = client;
    }

    public Client getClient() {
        return client;
    }

    /**
     * Prepare action And transform sql
     * into ES ActionRequest
     * @param sql SQL query to execute.
     * @return ES request
     * @throws SqlParseException
     */
    public QueryAction explain(String sql) throws SqlParseException, SQLFeatureNotSupportedException {
        return ESActionFactory.create(client, sql);
    }
}

SearchDao 类中有一个explain方法,接收的参数就是一个字符串sql ,返回结果是 QueryAction ,QueryAction 是一个抽象类,它又有以下子类

能够看出,每一个子类对应的就是一个查询的功能,聚合查询,默认查询,删除,哈希链接查询,链接查询,嵌套查询等等。

得到的 QueryAction 咱们能够经过 QueryActionElasticExecutor类的executeAnyAction方法来接受,并内部处理,而后就能得到相应的执行结果。

public static Object executeAnyAction(Client client , QueryAction queryAction) throws SqlParseException, IOException {
        if(queryAction instanceof DefaultQueryAction)
            return executeSearchAction((DefaultQueryAction) queryAction);
        if(queryAction instanceof AggregationQueryAction)
            return executeAggregationAction((AggregationQueryAction) queryAction);
        if(queryAction instanceof ESJoinQueryAction)
            return executeJoinSearchAction(client, (ESJoinQueryAction) queryAction);
        if(queryAction instanceof MultiQueryAction)
            return executeMultiQueryAction(client, (MultiQueryAction) queryAction);
        if(queryAction instanceof DeleteQueryAction )
            return executeDeleteAction((DeleteQueryAction) queryAction);
        return null;
    }

虽然获得了查询结果,可是它是一个Object类型,咱们还须要定制化一下,注意到了一个类:ObjectResultsExtractor,它的构造函数以下,构造函数包含三个布尔类型的参数。它们的做用是在结果集中是否包含score,是否包含type,是否包含ID,咱们能够都设置为 false。

public ObjectResultsExtractor(boolean includeScore, boolean includeType, boolean includeId) {
    this.includeScore = includeScore;
    this.includeType = includeType;
    this.includeId = includeId;
    this.currentLineIndex = 0;
}

ObjectResultsExtractor它仅有一个对外的 pulic 修饰的方法extractResults

public ObjectResult extractResults(Object queryResult, boolean flat) throws ObjectResultsExtractException {
    if (queryResult instanceof SearchHits) {
        SearchHit[] hits = ((SearchHits) queryResult).getHits();
        List<Map<String, Object>> docsAsMap = new ArrayList<>();
        List<String> headers = createHeadersAndFillDocsMap(flat, hits, docsAsMap);
        List<List<Object>> lines = createLinesFromDocs(flat, docsAsMap, headers);
        return new ObjectResult(headers, lines);
    }
    if (queryResult instanceof Aggregations) {
        List<String> headers = new ArrayList<>();
        List<List<Object>> lines = new ArrayList<>();
        lines.add(new ArrayList<Object>());
        handleAggregations((Aggregations) queryResult, headers, lines);
        
        // remove empty line。
        if(lines.get(0).size() == 0) {
            lines.remove(0);
        }
        //todo: need to handle more options for aggregations:
        //Aggregations that inhrit from base
        //ScriptedMetric

        return new ObjectResult(headers, lines);

    }
    return null;
}

至此咱们就大体了解了它的查询API ,而后咱们只须要在咱们项目中作以下的代码调用就能够完成咱们的查询功能了,最后获得的ObjectResult就是咱们的最终查询结果集了。

//1.解释SQL
SearchDao searchDao = new SearchDao(transportClient);
QueryAction queryAction = searchDao.explain(sql);
//2.执行        
Object execution = QueryActionElasticExecutor.executeAnyAction(searchDao.getClient(), queryAction);
//3.格式化查询结果            
ObjectResult result = (new ObjectResultsExtractor(true, false, false)).extractResults(execution, true);

至此,代码开发完成,咱们来测试下运行结果,我对外提供了三个接口,一个是 API方式查询,一个是JDBC方式查询,还有一个解释SQL。

@RestController
@RequestMapping("/es/data")
public class ElasticSearchController {

    @Autowired
    private ElasticSearchSqlService elasticSearchSqlService;

    @PostMapping(value = "/search")
    public CommonResult search(@RequestBody QueryDto queryDto) {
        SearchResultDTO resultDTO = elasticSearchSqlService.search(queryDto.getSql());
        return CommonResult.success(resultDTO.getResult());
    }

    @PostMapping(value = "/query")
    public CommonResult query(@RequestBody QueryDto queryDto) {
        SearchResultDTO resultDTO = elasticSearchSqlService.query(queryDto.getSql(), queryDto.getIndex());
        return CommonResult.success(resultDTO.getResult());
    }

    @PostMapping(value = "/explain")
    public CommonResult explain(@RequestBody QueryDto queryDto) {
        return CommonResult.success(elasticSearchSqlService.explain(queryDto.getSql()));
    }

}

请求示例:

查询结果示例:

总结

SQL 虽然不是 ES 官方推荐的查询语言,可是因为他的便捷性,ES 官方也开始意识到这块。ES 在 6.3.0版本后也开始支持 SQL了,可是他是经过引入 x-pack 的方式,若是咱们能够经过 REST 方式使用,可是咱们引入到开发中仍是有点问题,须要铂金会员才行,不知道之后会不会放开。

另外,SQL 虽然使用起来比较方便,可是毕竟不是官方指定的,因此不免在功能上有缺陷,没有 DSL 功能强大,并且里面的坑比较多,可是基本的查询都支持。因此若是不是无可奈何,我仍是建议使用 DSL,而一些简单的操做能够用SQL来辅助,本篇文章源码都已上传到本人的 Github ,若是感兴趣的读者能够关注个人 Github

相关文章
相关标签/搜索