1. 程式人生 > >Elasticsearch通關教程(五):如何通過SQL查詢Elasticsearch

Elasticsearch通關教程(五):如何通過SQL查詢Elasticsearch

  這篇博文字來是想放在全系列的大概第五、六篇的時候再講的,畢竟查詢是在索引建立、索引文件資料生成和一些基本概念介紹完之後才需要的。當前面的一些知識概念全都講解完之後再講解查詢是最好的,但是最近公司專案忙經常加班,畢竟年底了。但是不寫的話我怕會越拖越久,最後會不了了之了,所以剛好上海週末下雪,天冷無法出門,就坐在電腦前敲下了這篇博文。因為公司的查詢這塊是我負責的所以我研究了比較多點,寫起來也順手些。那麼進入正文。
  
  2|0為什麼用SQL查詢
  
  前面的文章介紹過,Elasticsearch 的官方查詢語言是 Query DSL,既然是官方指定的,說明最吻合 ES 的強大功能,為ES做支撐。那麼我們為什麼還用 SQL 查詢?這是否是多此一舉了呢?
  
  其實,存在畢竟有存在的道理,存在即合理。SQL 作為一個數據庫查詢語言,它語法簡潔,書寫方便而且大部分服務端程式設計師都清楚瞭解和熟知它的寫法。但是作為一個 ES 萌新來說,就算他已經是一位程式設計界的老江湖,但是如果他不熟悉 ES ,那麼他如果要使用公司已經搭好的 ES 服務,他必須要先學習 Query DSL,學習成本也是一項影響技術開發進度的因素而且不穩定性高。但是如果 ES 查詢支援 SQL的話,那麼也許就算他是工作一兩年的同學,他雖然不懂 ES的複雜概念,他也能很好的使用 ES 而且順利的參加到開發的隊伍中,畢竟SQL 誰不會寫呢?
  
  3|0Elasticsearch-SQL
  
  我們正式介紹下我們的主角 - Elasticsearch-SQL,Elasticsearch-SQL不屬於 Elasticsearch 官方的,它是 NLPChina(中國自然語言處理開源組織)開源的一個 ES 外掛,主要功能是通過 SQL 來查詢 ES,其實它的底層是通過解釋 SQL,將SQL 轉換為 DSL 語法,再通過DSL 查詢。
  
  Elasticsearch-SQL目前已經支援大概所有版本的 ES,而且最近的6.5.x的也在支援的範圍了,所以可以看得出來維護的還是蠻頻繁的。
  
  4|0安裝外掛
  
  由於 ES 2.x 和 5.x 的版本區別(詳細參考:版本選擇),我們安裝 ES 外掛是有點區別的,
  
  在 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 install
  
  ./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\sql。
  
  完成此操作後,需要重新啟動Elasticsearch伺服器,否則會報錯:Invalid index name [sql], must not start with '']; ","status":400}。
  
  5|0前端視覺化介面
  
  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
  
  6|0查詢語法
  
  經過以上的操作之後,如果沒出問題,現在就可以使用 SQL 查詢 ES 了,其中有些是正常的 SQL 語法,還有些是超越SQL 語法的,相當於是對 SQL 語法的增強,ES 的查詢格式是:
  
  http://localhost:9200/_sql?sql=select * from indexName limit 10
  
  6|1簡單查詢
  
  先上個簡單的查詢語法:
  
  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
  
  6|2額外增強查詢
  
  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')
  
  6|3地理查詢
  
  Elasticsearch 可以把地理位置、全文搜尋、結構化搜尋和分析結合到一起。而Elasticsearch-sql 也基本支援所有地理位置相關的查詢,對應 Elasticsearch的章節內容為Geolocation。
  
  1、地理座標盒模型過濾器
  
  地理座標盒模型過濾器(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)
  
  2、地理距離過濾器
  
  地理距離過濾器( geo_distance ),以給定位置為圓心畫一個圓,來找出那些地理座標落在指定距離範圍的文件。
  
  語法:
  
  GEO_DISTANCE(fieldName,distance,fromLongitude,fromLatitude)
  
  示例:
  
  SELECT * FROM location WHERE GEO_DISTANCE(center,'1km',100.5,0.5)
  
  3、地理距離區間過濾器
  
  範圍距離過濾器(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)
  
  4、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)
  
  5、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))
  
  更多關於地理的查詢可以參考這裡。
  
  7|0實戰用法
  
  我們以本系列的第一篇教程中我們建立的索引 nba來作示例,如下:
  
  1、查詢 nba 所有球隊資訊
  
  http://localhost:9200/_sql?sql=select * from nba limit 10
  
  查詢結果:
  
  2、查詢當家球星是詹姆斯的球隊資訊
  
  http://localhost:9200/_sql?sql=select * from nba where topStar  = "勒布朗·詹姆斯"
  
  查詢結果:
  
  3、根據建隊時間降序排列
  
  http://localhost:9200/_sql?sql=select * from nba order by date desc
  
  查詢結果:
  
  4、查詢擁有總冠軍超過5個的球隊資訊
  
  http://localhost:9200/_sql?sql=select * from nba where championship  >= 5
  
  查詢結果:
  
  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寫法可以參考這裡:
  
  基本條件查詢
  
  地理查詢
  
  聚合查詢
  
  額外SQL功能
  
  Scan and scroll
  
  功能有限的連線查詢
  
  Show Commands
  
  Script Fields
  
  NestedTypes support
  
  Union & Minus support
  
  8|0Java實現
  
  上面已經介紹了 Elasticsearch-SQL的安裝和使用,那麼我們如何在專案中使用它,Elasticsearch-SQL底層是使用Java語言開發的,通過解析SQL 轉換為 DSL 語言,然後得出查詢結果,解析結果成key-value的固定格式返回。
  
  8|1引入依賴
  
  使用前我們需要先引入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檔案依賴進來。
  
  8|2搭建專案
  
  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(www.thd540.com"limit");
  
  END_TABLE_MAP.add(www.furggw.com"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(www.mhylpt.com/ ) {
  
  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(www.dasheng178.com/   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(www.mcyllpt.com/ (DefaultQueryAction) queryAction);
  
  if(queryAction instanceof AggregationQueryAction)
  
  return executeAggregationAction((AggregationQueryAction) queryAction);
  
  if(queryAction instanceof ESJoinQueryAction)
  
  return executeJoinSearchAction(client, www.yigouyule2.cn  (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>www.michenggw.com/> 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(www.gcyL157.com) == 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 =www.yongshiyule178.com  (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()));
  
  }
  
  }
  
  請求示例:
  
  查詢結果示例:
  
  9|0總結
  
  SQL 雖然不是 ES 官方推薦的查詢語言,但是由於他的便捷性,ES 官方也開始意識到這塊。ES 在 6.3.0版本後也開始支援 SQL了,但是他是通過引入 x-pack 的方式,如歸我們可以通過 REST 方式使用,但是我們引入到開發中還是有點問題,需要鉑金會員才行,不知道以後會不會放開。
  
  另外,SQL 雖然使用起來比較方便,但是畢竟不是官方指定的,所以難免在功能上有缺陷,沒有 DSL 功能強大,而且裡面的坑比較多,但是基本的查詢都支援。所以如果不是迫不得已,我還是建議使用 DSL,而一些簡單的操作可以用SQL來輔助,本篇文章原始碼都已上傳到本人的 Github ,如果感興趣的讀者可以關注我的 Github。