如何經過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


我的公衆號:JaJian

歡迎長按下圖關注公衆號:JaJian!

按期爲你奉上分佈式,微服務等一線互聯網公司相關技術的講解和分析。


1557975294786730.png

相關文章
相關標籤/搜索