SpringBoot操作ES進行各種高級查詢
- 2019 年 10 月 3 日
- 筆記
SpringBoot整合ES
創建SpringBoot項目,導入 ES 6.2.1 的 RestClient 依賴和 ES 依賴。在項目中直接引用 es-starter 的話會報容器初始化異常錯誤,導致項目無法啟動。如果有讀者解決了這個問題,歡迎留言交流
<!-- ES 客戶端 --> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>${elasticsearch.version}</version> </dependency> <!-- ES 版本 --> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>${elasticsearch.version}</version> </dependency>
為容器定義 RestClient 對象
/** * 在Spring容器中定義 RestClient 對象 * @Author: keats_coder * @Date: 2019/8/9 * @Version 1.0 * */ @Configuration public class ESConfig { @Value("${yunshangxue.elasticsearch.hostlist}") private String hostlist; // 127.0.0.1:9200 @Bean // 高版本客戶端 public RestHighLevelClient restHighLevelClient() { // 解析 hostlist 配置資訊。假如以後有多個,則需要用 , 分開 String[] split = hostlist.split(","); // 創建 HttpHost 數組,其中存放es主機和埠的配置資訊 HttpHost[] httpHostArray = new HttpHost[split.length]; for (int i = 0; i < split.length; i++) { String item = split[i]; httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http"); } // 創建RestHighLevelClient客戶端 return new RestHighLevelClient(RestClient.builder(httpHostArray)); } // 項目主要使用 RestHighLevelClient,對於低級的客戶端暫時不用 @Bean public RestClient restClient() { // 解析hostlist配置資訊 String[] split = hostlist.split(","); // 創建HttpHost數組,其中存放es主機和埠的配置資訊 HttpHost[] httpHostArray = new HttpHost[split.length]; for (int i = 0; i < split.length; i++) { String item = split[i]; httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http"); } return RestClient.builder(httpHostArray).build(); } }
在 yml 文件中配置 eshost
yunshangxue: elasticsearch: hostlist: ${eshostlist:127.0.0.1:9200}
調用相關 API 執行操作
- 創建操作索引的對象
- 構建操作索引的請求
- 調用對象的相關API發送請求
- 獲取響應消息
/** * 刪除索引庫 */ @Test public void testDelIndex() throws IOException { // 操作索引的對象 IndicesClient indices = client.indices(); // 刪除索引的請求 DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("ysx_course"); // 刪除索引 DeleteIndexResponse response = indices.delete(deleteIndexRequest); // 得到響應 boolean b = response.isAcknowledged(); System.out.println(b); }
創建索引, 步驟和刪除類似,需要注意的是刪除的時候需要指定 ES 庫分片的數量和副本的數量,並且在創建索引的時候可以將映射一起指定了。程式碼如下
public void testAddIndex() throws IOException { // 操作索引的對象 IndicesClient indices = client.indices(); // 創建索引的請求 CreateIndexRequest request = new CreateIndexRequest("ysx_course"); request.settings(Settings.builder().put("number_of_shards", "1").put("number_of_replicas", "0")); // 創建映射 request.mapping("doc", "{n" + " "properties": {n" + " "description": {n" + " "type": "text",n" + " "analyzer": "ik_max_word",n" + " "search_analyzer": "ik_smart"n" + " },n" + " "name": {n" + " "type": "text",n" + " "analyzer": "ik_max_word",n" + " "search_analyzer": "ik_smart"n" + " },n" + ""pic":{ n" + ""type":"text", n" + ""index":false n" + "}, n" + " "price": {n" + " "type": "float"n" + " },n" + " "studymodel": {n" + " "type": "keyword"n" + " },n" + " "timestamp": {n" + " "type": "date",n" + " "format": "yyyy-MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis"n" + " }n" + " }n" + " }", XContentType.JSON); // 執行創建操作 CreateIndexResponse response = indices.create(request); // 得到響應 boolean b = response.isAcknowledged(); System.out.println(b); }
Java API操作ES
準備數據環境
創建索引:ysx_course
創建映射:
PUT http://localhost:9200/ysx_course/doc/_mapping
{ "properties": { "description": { // 課程描述 "type": "text", // String text 類型 "analyzer": "ik_max_word", // 存入的分詞模式:細粒度 "search_analyzer": "ik_smart" // 查詢的分詞模式:粗粒度 }, "name": { // 課程名稱 "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "pic":{ // 圖片地址 "type":"text", "index":false // 地址不用來搜索,因此不為它構建索引 }, "price": { // 價格 "type": "scaled_float", // 有比例浮點 "scaling_factor": 100 // 比例因子 100 }, "studymodel": { "type": "keyword" // 不分詞,全關鍵字匹配 }, "timestamp": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" } } }
加入原始數據:
POST http://localhost:9200/ysx_course/doc/1
{ "name": "Bootstrap開發", "description": "Bootstrap是由Twitter推出的一個前台頁面開發框架,是一個非常流行的開發框架,此框架集成了多種頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。", "studymodel": "201002", "price":38.6, "timestamp":"2018-04-25 19:11:35", "pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" }
DSL搜索
DSL(Domain Specific Language)是ES提出的基於json的搜索方式,在搜索時傳入特定的json格式的數據來完成不
同的搜索需求。DSL比URI搜索方式功能強大,在項目中建議使用DSL方式來完成搜索。
查詢全部
原本我們想要查詢全部的話,需要使用 GET 請求發送 _search 命令,如今使用 DSL 方式搜索,可以使用 POST 請求,並在請求體中設置 JSON 字元串來構建查詢條件
POST http://localhost:9200/ysx_course/doc/_search
請求體 JSON
{ "query": { "match_all": {} // 查詢全部 }, "_source" : ["name","studymodel"] // 查詢結果包括 課程名 + 學習模式兩個映射 }
具體的測試方法如下:過程比較繁瑣,好在條理還比較清晰
// 搜索全部記錄 @Test public void testSearchAll() throws IOException, ParseException { // 搜索請求對象 SearchRequest searchRequest = new SearchRequest("ysx_course"); // 指定類型 searchRequest.types("doc"); // 搜索源構建對象 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 搜索方式 // matchAllQuery搜索全部 searchSourceBuilder.query(QueryBuilders.matchAllQuery()); // 設置源欄位過慮,第一個參數結果集包括哪些欄位,第二個參數表示結果集不包括哪些欄位 searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); // 向搜索請求對象中設置搜索源 searchRequest.source(searchSourceBuilder); // 執行搜索,向ES發起http請求 SearchResponse searchResponse = client.search(searchRequest); // 搜索結果 SearchHits hits = searchResponse.getHits(); // 匹配到的總記錄數 long totalHits = hits.getTotalHits(); // 得到匹配度高的文檔 SearchHit[] searchHits = hits.getHits(); // 日期格式化對象 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for(SearchHit hit:searchHits){ // 文檔的主鍵 String id = hit.getId(); // 源文檔內容 Map<String, Object> sourceAsMap = hit.getSourceAsMap(); String name = (String) sourceAsMap.get("name"); // 由於前邊設置了源文檔欄位過慮,這時description是取不到的 String description = (String) sourceAsMap.get("description"); // 學習模式 String studymodel = (String) sourceAsMap.get("studymodel"); // 價格 Double price = (Double) sourceAsMap.get("price"); // 日期 Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); System.out.println(name); System.out.println(studymodel); System.out.println("你看不見我,看不見我~" + description); System.out.println(price); } }
坑:red>
執行過程中遇到的問題:不能對這個值進行初始化,導致 Spring 容器無法初始化
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'yunshangxue.elasticsearch.hostlist' in value "${yunshangxue.elasticsearch.hostlist}"
通過檢查 target 目錄發現,生成的 target 文件包中沒有將 yml 配置文件帶過來… 仔細對比發現,我的項目竟然變成了一個不是 Maven 的項目。重新使用 IDEA 導入 Mavaen 工程之後便能正常運行了
分頁查詢
我們來 look 一下 ES 的分頁查詢參數:
{ // from 起始索引 // size 每頁顯示的條數 "from" : 0, "size" : 1, "query": { "match_all": {} }, "_source" : ["name","studymodel"] }
通過查詢結果可以發現,我們設置了分頁參數之後, hits.total 仍然是 3,表示它找到了 3 條數據,而按照分頁規則,它只會返回一條數據,因此 hits.hits 裡面只有一條數據。這也符合我們的業務規則,在查詢前端頁面顯示總共的條數和當前的數據。
由此,我們就可以通過 Java API 來構建查詢條件了:對上面查詢全部的程式碼進行如下改造:
// 搜索源構建對象 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); int page = 2; // 頁碼 int size = 1; // 每頁顯示的條數 int index = (page - 1) * size; searchSourceBuilder.from(index); searchSourceBuilder.size(1); // 搜索方式 // matchAllQuery搜索全部 searchSourceBuilder.query(QueryBuilders.matchAllQuery());
精確查詢 TermQuery
Term Query為精確查詢,在搜索時會整體匹配關鍵字,不再將關鍵字分詞
例如:
{ "query": { "term": { // 查詢的方式為 term 精確查詢 "name": "spring" // 查詢的欄位為 name 關鍵字是 spring } }, "_source": [ "name", "studymodel" ] }
此時查詢的結果是:
"hits": [ { "_index": "ysx_course", "_type": "doc", "_id": "3", "_score": 0.9331132, "_source": { "studymodel": "201001", "name": "spring開發基礎" } } ]
查詢到了上面這條數據,因為 spring開發基礎 分完詞後是 spring 開發 基礎 ,而查詢關鍵字是 spring 不分詞,這樣當然可以匹配到這條記錄,但是當我們修改關鍵字為 spring開發,按照往常的查詢方法,也是可以查詢到的。但是 term 不一樣,它不會對關鍵字分詞。結果可想而知是查詢不到的
JavaAPI如下:
// 搜索方式 // termQuery 精確查詢 searchSourceBuilder.query(QueryBuilders.termQuery("studymodel", "201002"));
根據 ID 查詢:
根據 ID 精確查詢和根據其他條件精確查詢是一樣的,不同的是 id 欄位前面有一個下劃線注意寫上
searchSourceBuilder.query(QueryBuilders.termQuery("_id", "1"));
但是,當一次查詢多個 ID 時,相應的 API 也應該改變,使用 termsQuery 而不是 termQuery。多了一個 s
全文檢索 MatchQuery
MatchQuery 即全文檢索,會對關鍵字進行分詞後匹配詞條。
query:搜索的關鍵字,對於英文關鍵字如果有多個單詞則中間要用半形逗號分隔,而對於中文關鍵字中間可以用
逗號分隔也可以不用
operator:設置查詢的結果取交集還是並集,並集用 or, 交集用 and
{ "query": { "match": { "description": { "query": "spring開發", "operator": "or" } } } }
有時,我們需要設定一個量化的表達方式,例如查詢 spring開發基礎,這三個詞條。我們需求是至少匹配兩個詞條,這時 operator 屬性就不能滿足要求了,ES 還提供了另外一個屬性:minimum_should_match 用一個百分數來設定應該有多少個詞條滿足要求。例如查詢:
「spring開發框架」會被分為三個詞:spring、開發、框架
設置"minimum_should_match": "80%"表示,三個詞在文檔的匹配佔比為80%,即3*0.8=2.4,向下取整得2,表
示至少有兩個詞在文檔中要匹配成功。
JavaAPI
通過 matchQuery.minimumShouldMatch 的方式來設置條件
// matchQuery全文檢索 searchSourceBuilder.query(QueryBuilders.matchQuery("description", "Spring開發框架").minimumShouldMatch("70%"));
多欄位聯合搜索 MultiQuery
上面的 MatchQuery 有一個短板,假如用戶輸入了某關鍵字,我們在查找的時候並不知道他輸入的是 name 還是 description,這時我們用什麼都不合適,而 MultiQuery 的出現解決了這個問題,他可以通過 fields 屬性來設置多個域聯合查找:具體用法如下
{ "query": { "multi_match": { "query": "Spring開發", "minimum_should_match": "70%", "fields": ["name", "description"] } } }
JavaAPI
searchSourceBuilder.query(QueryBuilders.multiMatchQuery("Spring開發框架", "name", "description").minimumShouldMatch("70%"));
提升 boost
在多域聯合查詢的時候,可以通過 boost 來設置某個域在計算得分時候的比重,比重越高的域當他符合條件時計算的得分越高,相應的該記錄也更靠前。通過在 fields 中給相應的欄位用 ^權重倍數來實現
"fields": ["name^10", "description"]
上面的程式碼表示給 name 欄位提升十倍權重,查詢到的結果:
{ "_index": "ysx_course", "_type": "doc", "_id": "3", "_score": 13.802518, // 可以清楚的發現,得分竟然是 13 了 "_source": { "name": "spring開發基礎", "description": "spring 在java領域非常流行,java程式設計師都在用。", "studymodel": "201001", "price": 88.6, "timestamp": "2018-02-24 19:11:35", "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" } },
而在 Java 中,仍然可以通過鏈式編程來實現
searchSourceBuilder.query(QueryBuilders.multiMatchQuery("Spring開發框架", "name", "description").field("name", 10)); // 設置 name 10倍權重
布爾查詢 BoolQuery
如果我們既要對一些欄位進行分詞查詢,同時要對另一些欄位進行精確查詢,就需要使用布爾查詢來實現了。布爾查詢對應於Lucene的BooleanQuery查詢,實現將多個查詢組合起來,有三個可選的參數:
must:文檔必須匹配must所包括的查詢條件,相當於 「AND」
should:文檔應該匹配should所包括的查詢條件其中的一個或多個,相當於 "OR"
must_not:文檔不能匹配must_not所包括的該查詢條件,相當於「NOT」
{ "query": { "bool": { // 布爾查詢 "must": [ // 查詢條件 must 表示數組中的查詢方式所規定的條件都必須滿足 { "multi_match": { "query": "spring框架", "minimum_should_match": "50%", "fields": [ "name^10", "description" ] } }, { "term": { "studymodel": "201001" } } ] } } }
JavaAPI
// 搜索方式 // 首先構造多關鍵字查詢條件 MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("Spring開發框架", "name", "description").field("name", 10); // 然後構造精確匹配查詢條件 TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("studymodel", "201002"); // 組合兩個條件,組合方式為 must 全滿足 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(matchQueryBuilder); boolQueryBuilder.must(termQueryBuilder); // 將查詢條件封裝給查詢對象 searchSourceBuilder.query(boolQueryBuilder);
過濾器
定義過濾器查詢,是在原本查詢結果的基礎上對數據進行篩選,因此省略了重新計算的分的步驟,效率更高。並且方便快取。推薦盡量使用過慮器去實現查詢或者過慮器和查詢共同使用,過濾器在布爾查詢中使用,下邊是在搜索結果的基礎上進行過濾:
{ "query": { "bool": { "must": [ { "multi_match": { "query": "spring框架", "minimum_should_match": "50%", "fields": [ "name^10", "description" ] } } ], "filter": [ { // 過濾條件:studymodel 必須是 201001 "term": {"studymodel": "201001"} }, { // 過濾條件:價格 >=60 <=100 "range": {"price": {"gte": 60,"lte": 100}} } ] } } }
注意:range和term一次只能對一個Field設置範圍過慮。
JavaAPI
// 首先構造多關鍵字查詢條件 MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("Spring框架", "name", "description").field("name", 10); // 添加條件到布爾查詢 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(matchQueryBuilder); // 通過布爾查詢來構造過濾查詢 boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel", "201001")); boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(60).lte(100)); // 將查詢條件封裝給查詢對象 searchSourceBuilder.query(boolQueryBuilder);
排序
我們可以在查詢的結果上進行二次排序,支援對 keyword、date、float 等類型添加排序,text類型的欄位不允許排序。排序使用的 JSON 格式如下:
{ "query": { "bool": { "filter": [ { "range": { "price": { "gte": 0, "lte": 100 } } } ] } }, "sort": [ // 注意這裡排序是寫在 query key 的外面的。這就表示它的API也不是布爾查詢提供 { "studymodel": "desc" // 對 studymodel(keyword)降序 }, { "price": "asc" // 對 price(double)升序 } ] }
由上面的 JSON 數據可以發現,排序所屬的 API 是和 query 評級的,因此在調用 API 時也應該選擇對應的 SearchSourceBuilder 對象
// 排序查詢 @Test public void testSort() throws IOException, ParseException { // 搜索請求對象 SearchRequest searchRequest = new SearchRequest("ysx_course"); // 指定類型 searchRequest.types("doc"); // 搜索源構建對象 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 搜索方式 // 添加條件到布爾查詢 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 通過布爾查詢來構造過濾查詢 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100)); // 將查詢條件封裝給查詢對象 searchSourceBuilder.query(boolQueryBuilder); // 向搜索請求對象中設置搜索源 searchRequest.source(searchSourceBuilder); // 設置排序規則 searchSourceBuilder.sort("studymodel", SortOrder.DESC); // 第一排序規則 searchSourceBuilder.sort("price", SortOrder.ASC); // 第二排序規則 // 執行搜索,向ES發起http請求 SearchResponse searchResponse = client.search(searchRequest); // 搜索結果 SearchHits hits = searchResponse.getHits(); // 匹配到的總記錄數 long totalHits = hits.getTotalHits(); // 得到匹配度高的文檔 SearchHit[] searchHits = hits.getHits(); // 日期格式化對象 soutData(searchHits); }
高亮顯示
高亮顯示可以將搜索結果一個或多個字突出顯示,以便向用戶展示匹配關鍵字的位置。
高亮三要素:高亮關鍵字、高亮前綴、高亮後綴
{ "query": { "bool": { "must": [ { "multi_match": { "query": "開發框架", "minimum_should_match": "50%", "fields": [ "name^10", "description" ], "type": "best_fields" } } ] } }, "sort": [ { "price": "asc" } ], "highlight": { "pre_tags": [ "<em>" ], "post_tags": [ "</em>" ], "fields": { "name": {}, "description": {} } } }
查詢結果的數據如下:
Java 程式碼如下,注意到上面的 JSON 數據, highlight 和 sort 和 query 依然是同級的,所以也需要用 SearchSourceBuilder 對象來設置到搜索條件中
// 高亮查詢 @Test public void testHighLight() throws IOException, ParseException { // 搜索請求對象 SearchRequest searchRequest = new SearchRequest("ysx_course"); // 指定類型 searchRequest.types("doc"); // 搜索源構建對象 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 搜索方式 // 首先構造多關鍵字查詢條件 MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("Spring框架", "name", "description").field("name", 10); // 添加條件到布爾查詢 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(matchQueryBuilder); // 通過布爾查詢來構造過濾查詢 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(60).lte(100)); // 將查詢條件封裝給查詢對象 searchSourceBuilder.query(boolQueryBuilder); // *********************** // 高亮查詢 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.preTags("<em>"); // 高亮前綴 highlightBuilder.postTags("</em>"); // 高亮後綴 highlightBuilder.fields().add(new HighlightBuilder.Field("name")); // 高亮欄位 // 添加高亮查詢條件到搜索源 searchSourceBuilder.highlighter(highlightBuilder); // *********************** // 設置源欄位過慮,第一個參數結果集包括哪些欄位,第二個參數表示結果集不包括哪些欄位 searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); // 向搜索請求對象中設置搜索源 searchRequest.source(searchSourceBuilder); // 執行搜索,向ES發起http請求 SearchResponse searchResponse = client.search(searchRequest); // 搜索結果 SearchHits hits = searchResponse.getHits(); // 匹配到的總記錄數 long totalHits = hits.getTotalHits(); // 得到匹配度高的文檔 SearchHit[] searchHits = hits.getHits(); // 日期格式化對象 soutData(searchHits); }
根據查詢結果的數據結構來獲取高亮的數據,替換原有的數據:
private void soutData(SearchHit[] searchHits) throws ParseException { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for (SearchHit hit : searchHits) { // 文檔的主鍵 String id = hit.getId(); // 源文檔內容 Map<String, Object> sourceAsMap = hit.getSourceAsMap(); String name = (String) sourceAsMap.get("name"); // 獲取高亮查詢的內容。如果存在,則替換原來的name Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if( highlightFields != null ){ HighlightField nameField = highlightFields.get("name"); if(nameField!=null){ Text[] fragments = nameField.getFragments(); StringBuffer stringBuffer = new StringBuffer(); for (Text str : fragments) { stringBuffer.append(str.string()); } name = stringBuffer.toString(); } } // 由於前邊設置了源文檔欄位過慮,這時description是取不到的 String description = (String) sourceAsMap.get("description"); // 學習模式 String studymodel = (String) sourceAsMap.get("studymodel"); // 價格 Double price = (Double) sourceAsMap.get("price"); // 日期 Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); System.out.println(name); System.out.println(id); System.out.println(studymodel); System.out.println("你看不見我,看不見我~" + description); System.out.println(price); } }