回覆 PDF 領取資料 html
這是悟空的第 92 篇原創文章前端
做者 | 悟空聊架構java
來源 | 悟空聊架構(ID:PassJava666)mysql
轉載請聯繫受權(微信ID:PassJava)git
前言
上篇咱們講到了 Elasticsearch 全文檢索的原理《別隻會搜日誌了,求你懂點原理吧》,經過在本地搭建一套 ES 服務,以多個案例來分析了 ES 的原理以及基礎使用。此次咱們來說下 Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服務項目中使用 ES 來實現全文檢索,來達到搜索題庫的功能。github
並且題庫的數據量是很是大的,題目的答案也是很是長的,經過 ES 正好能夠解決 mysql 模糊搜索的低效性。web
經過本實戰您能夠學到以下知識點:spring
-
Spring Boot 如何整合 ES。sql
-
微服務中 ES 的 API 使用。數據庫
-
項目中如何使用 ES 來達到全文檢索。
本篇主要內容以下:
主要內容
本文案例都是基於 PassJava 實戰項目來演示的。
Github 地址:https://github.com/Jackson0714/PassJava-Platform
爲了讓你們更清晰地理解 PassJava 項目中 ES 是如何使用的,我畫了三個流程圖:
-
第一步:建立 question 索引。
首先定義 question 索引,而後在 ES 中建立索引。
-
第二步:存 question 數據進 ES 。
前端保存數據時,保存的 API 請求先通過網關,而後轉發到 passjava-question 微服務,而後遠程調用 passjava-search 微服務,將數據保存進 ES 中。
-
第三步:從 ES 中查數據。
前端查詢數據時,先通過網關,而後將請求轉發給 passjava-search 微服務,而後從 ES 中查詢數據。
1、Elasticsearch 組件庫介紹
在講解以前,我在這裏再次提下全文檢索是什麼:
全文檢索: 指以所有文本信息做爲檢索對象的一種信息檢索技術。而咱們使用的數據庫,如 Mysql,MongoDB 對文本信息檢索能力特別是中文檢索並無 ES 強大。因此咱們來看下 ES 在項目中是如何來代替 SQL 來工做的。
我使用的 Elasticsearch 服務是 7.4.2 的版本,而後採用官方提供的 Elastiscsearch-Rest-Client 庫來操做 ES,並且官方庫的 API 上手簡單。
該組件庫的官方文檔地址:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
另外這個組件庫是支持多種語言的:
支持多語言
注意:Elasticsearch Clients
就是指如何用 API 操做 ES 服務的組件庫。
可能有同窗會提問,Elasticsearch 的組件庫中寫着 JavaScript API,是否是能夠直接在前端訪問 ES 服務?能夠是能夠,可是會暴露 ES 服務的端口和 IP 地址,會很是不安全。因此咱們仍是用後端服務來訪問 ES 服務。
咱們這個項目是 Java 項目,天然就是用上面的兩種:Java Rest Client
或者 Java API
。咱們先看下 Java API,可是會發現已經廢棄了。以下圖所示:
Java API 已經廢棄了
因此咱們只能用 Java REST Client 了。而它又分紅兩種:高級和低級的。高級包含更多的功能,若是把高級比做MyBatis的話,那麼低級就至關於JDBC。因此咱們用高級的 Client。
高級和低級 Client
2、整合檢索服務
咱們把檢索服務單獨做爲一個服務。就稱做 passjava-search 模塊吧。
1.1 添加搜索服務模塊
-
建立 passjava-search 模塊。
首先咱們在 PassJava-Platform 模塊建立一個 搜索服務模塊 passjava-search。而後勾選 spring web 服務。以下圖所示。
第一步:選擇 Spring Initializr,而後點擊 Next。
選擇 Spring Initializr
第二步:填寫模塊信息,而後點擊 Next。
passjava-search 服務模塊
第三步:選擇 Web->Spring Web 依賴,而後點擊 Next。
1.2 配置 Maven 依賴
-
參照 ES 官網配置。
進入到 ES 官方網站,能夠看到有低級和高級的 Rest Client,咱們選擇高階的(High Level Rest Client)。而後進入到高階 Rest Client 的 Maven 倉庫。官網地址以下所示:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/index.html
Rest Client 官方文檔
-
加上 Maven 依賴。
對應文件路徑:\passjava-search\pom.xml
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.4.2</version> </dependency>
-
配置 elasticsearch 的版本爲7.4.2
因加上 Maven 依賴後,elasticsearch 版本爲 7.6.2,因此遇到這種版本不一致的狀況時,須要手動改掉。
對應文件路徑:\passjava-search\pom.xml
<properties> <elasticsearch.version>7.4.2</elasticsearch.version> </properties>
刷新 Maven Project 後,能夠看到引入的 elasticsearch 都是 7.4.2 版本了,以下圖所示:
設置版本爲 7.4.2
-
引入 PassJava 的 Common 模塊依賴。
Common 模塊是 PassJava 項目獨立的出來的公共模塊,引入了不少公共組件依賴,其餘模塊引入 Common 模塊依賴後,就不須要單獨引入這些公共組件了,很是方便。
對應文件路徑:\passjava-search\pom.xml
<dependency> <groupId>com.jackson0714.passjava</groupId> <artifactId>passjava-common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
添加完依賴後,咱們就能夠將搜索服務註冊到 Nacos
註冊中心了。Nacos 註冊中心的用法在前面幾篇文章中也詳細講解過,這裏須要注意的是要先啓動 Nacos 註冊中心,才能正常註冊 passjava-search 服務。
1.3 註冊搜索服務到註冊中心
修改配置文件:src/main/resources/application.properties。配置應用程序名、註冊中心地址、註冊中心的命名中間。
spring.application.name=passjava-search spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=passjava-search
給啓動類
添加服務發現註解:@EnableDiscoveryClient
。這樣 passjava-search 服務就能夠被註冊中心發現了。
因 Common 模塊依賴數據源,但 search 模塊不依賴數據源,因此 search 模塊須要移除數據源依賴:
exclude = DataSourceAutoConfiguration.class
以上的兩個註解以下所示:
@EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class PassjavaSearchApplication { public static void main(String[] args) { SpringApplication.run(PassjavaSearchApplication.class, args); } }
接下來咱們添加一個 ES 服務的專屬配置類,主要目的是自動加載一個 ES Client 來供後續 ES API 使用,不用每次都 new 一個 ES Client。
1.4 添加 ES 配置類
配置類:PassJavaElasticsearchConfig.java
核心方法就是 RestClient.builder 方法,設置好 ES 服務的 IP 地址、端口號、傳輸協議就能夠了。最後自動加載了 RestHighLevelClient。
package com.jackson0714.passjava.search.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Author: 公衆號 | 悟空聊架構 * @Date: 2020/10/8 17:02 * @Site: www.passjava.cn * @Github: https://github.com/Jackson0714/PassJava-Platform */ @Configuration public class PassJavaElasticsearchConfig { @Bean // 給容器註冊一個 RestHighLevelClient,用來操做 ES // 參考官方文檔:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-getting-started-initialization.html public RestHighLevelClient restHighLevelClient() { return new RestHighLevelClient( RestClient.builder( new HttpHost("192.168.56.10", 9200, "http"))); } }
接下來咱們測試下 ES Client 是否自動加載成功。
1.5 測試 ES Client 自動加載
在測試類 PassjavaSearchApplicationTests 中編寫測試方法,打印出自動加載的 ES Client。指望結果是一個 RestHighLevelClient 對象。
package com.jackson0714.passjava.search; import org.elasticsearch.client.RestHighLevelClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class PassjavaSearchApplicationTests { @Qualifier("restHighLevelClient") @Autowired private RestHighLevelClient client; @Test public void contextLoads() { System.out.println(client); } }
運行結果以下所示,打印出了 RestHighLevelClient。說明自定義的 ES Client 自動裝載成功。
ES 測試結果
1.6 測試 ES 簡單插入數據
測試方法 testIndexData,省略 User 類。users 索引在個人 ES 中是沒有記錄的,因此指望結果是 ES 中新增了一條 users 數據。
/** * 測試存儲數據到 ES。 * */ @Test public void testIndexData() throws IOException { IndexRequest request = new IndexRequest("users"); request.id("1"); // 文檔的 id //構造 User 對象 User user = new User(); user.setUserName("PassJava"); user.setAge("18"); user.setGender("Man"); //User 對象轉爲 JSON 數據 String jsonString = JSON.toJSONString(user); // JSON 數據放入 request 中 request.source(jsonString, XContentType.JSON); // 執行插入操做 IndexResponse response = client.index(request, RequestOptions.DEFAULT); System.out.println(response); }
執行 test 方法,咱們能夠看到控制檯輸出如下結果,說明數據插入到 ES 成功。另外須要注意的是結果中的 result 字段爲 updated,是由於我本地爲了截圖,多執行了幾回插入操做,但由於 id = 1,因此作的都是 updated 操做,而不是 created 操做。
控制檯輸出結果
咱們再來到 ES 中看下 users 索引中數據。查詢 users 索引:
GET users/_search
結果以下所示:
查詢 users 索引結果
能夠從圖中看到有一條記錄被查詢出來,查詢出來的數據的 _id = 1,和插入的文檔 id 一致。另外幾個字段的值也是一致的。說明插入的數據沒有問題。
"age" : "18", "gender" : "Man", "userName" : "PassJava"
1.7 測試 ES 查詢複雜語句
示例:搜索 bank 索引,address 字段中包含 big 的全部人的年齡分佈 ( 前 10 條 ) 以及平均年齡,以及平均薪資。
1.7.1 構造檢索條件
咱們能夠參照官方文檔給出的示例來建立一個 SearchRequest 對象,指定要查詢的索引爲 bank,而後建立一個 SearchSourceBuilder 來組裝查詢條件。總共有三種條件須要組裝:
-
address 中包含 road 的全部人。
-
按照年齡分佈進行聚合。
-
計算平均薪資。
代碼以下所示,須要源碼請到個人 Github/PassJava 上下載。
查詢複雜語句示例
將打印出來的檢索參數複製出來,而後放到 JSON 格式化工具中格式化一下,再粘貼到 ES 控制檯執行,發現執行結果是正確的。
打印出檢索參數
用在線工具格式化 JSON 字符串,結果以下所示:
而後咱們去掉其中的一些默認參數,最後簡化後的檢索參數放到 Kibana 中執行。
Kibana Dev Tools 控制檯中執行檢索語句以下圖所示,檢索結果以下圖所示:
控制檯中執行檢索語句
找到總記錄數:29 條。
第一條命中記錄的詳情以下:
平均 balance:13136。
平均年齡:26。
地址中包含 Road 的:263 Aviation Road。
和 IDEA 中執行的測試結果一致,說明覆雜檢索的功能已經成功實現。
17.2 獲取命中記錄的詳情
而獲取命中記錄的詳情數據,則須要經過兩次 getHists() 方法拿到,以下所示:
// 3.1)獲取查到的數據。 SearchHits hits = response.getHits(); // 3.2)獲取真正命中的結果 SearchHit[] searchHits = hits.getHits();
咱們能夠經過遍歷 searchHits 的方式打印出全部命中結果的詳情。
// 3.3)、遍歷命中結果 for (SearchHit hit: searchHits) { String hitStr = hit.getSourceAsString(); BankMember bankMember = JSON.parseObject(hitStr, BankMember.class); }
拿到每條記錄的 hitStr 是個 JSON 數據,以下所示:
{ "account_number": 431, "balance": 13136, "firstname": "Laurie", "lastname": "Shaw", "age": 26, "gender": "F", "address": "263 Aviation Road", "employer": "Zillanet", "email": "laurieshaw@zillanet.com", "city": "Harmon", "state": "WV" }
而 BankMember 是根據返回的結果詳情定義的的 JavaBean。能夠經過工具自動生成。在線生成 JavaBean 的網站以下:
https://www.bejson.com/json2javapojo/new/
把這個 JavaBean 加到 PassjavaSearchApplicationTests 類中:
@ToString @Data static class BankMember { private int account_number; private int balance; private String firstname; private String lastname; private int age; private String gender; private String address; private String employer; private String email; private String city; private String state; }
而後將 bankMember 打印出來:
System.out.println(bankMember);
bankMember
獲得的結果確實是咱們封裝的 BankMember 對象,並且裏面的屬性值也都拿到了。
1.7.3 獲取年齡分佈聚合信息
ES 返回的 response 中,年齡分佈的數據是按照 ES 的格式返回的,若是想按照咱們本身的格式來返回,就須要將 response 進行處理。
以下圖所示,這個是查詢到的年齡分佈結果,咱們須要將其中某些字段取出來,好比 buckets,它表明了分佈在 21 歲的有 4 個。
ES 返回的年齡分佈信息
下面是代碼實現:
Aggregations aggregations = response.getAggregations(); Terms ageAgg1 = aggregations.get("ageAgg"); for (Terms.Bucket bucket : ageAgg1.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println("用戶年齡: " + keyAsString + " 人數:" + bucket.getDocCount()); }
最後打印的結果以下,21 歲的有 4 人,26 歲的有 4 人,等等。
打印結果:用戶年齡分佈
1.7.4 獲取平均薪資聚合信息
如今來看看平均薪資如何按照所需的格式返回,ES 返回的結果以下圖所示,咱們須要獲取 balanceAvg 字段的 value 值。
ES 返回的平均薪資信息
代碼實現:
Avg balanceAvg1 = aggregations.get("balanceAvg"); System.out.println("平均薪資:" + balanceAvg1.getValue());
打印結果以下,平均薪資 28578 元。
打印結果:平均薪資
3、實戰:同步 ES 數據
3.1 定義檢索模型
PassJava 這個項目能夠用來配置題庫,若是咱們想經過關鍵字來搜索題庫,該怎麼作呢?
相似於百度搜索,輸入幾個關鍵字就能夠搜到關聯的結果,咱們這個功能也是相似,經過 Elasticsearch 作檢索引擎,後臺管理界面和小程序做爲搜索入口,只須要在小程序上輸入關鍵字,就能夠檢索相關的題目和答案。
首先咱們須要把題目和答案保存到 ES 中,在存以前,第一步是定義索引的模型,以下所示,模型中有 title
和 answer
字段,表示題目和答案。
"id": { "type": "long" }, "title": { "type": "text", "analyzer": "ik_smart" }, "answer": { "type": "text", "analyzer": "ik_smart" }, "typeName": { "type": "keyword" }
3.2 在 ES 中建立索引
上面咱們已經定義了索引結構,接着就是在 ES 中建立索引。
在 Kibana 控制檯中執行如下語句:
PUT question { "mappings" : { "properties": { "id": { "type": "long" }, "title": { "type": "text", "analyzer": "ik_smart" }, "answer": { "type": "text", "analyzer": "ik_smart" }, "typeName": { "type": "keyword" } } } }
執行結果以下所示:
建立 question 索引
咱們能夠經過如下命令來查看 question 索引是否在 ES 中:
GET _cat/indices
執行結果以下圖所示:
查看 ES 中全部的索引
3.3 定義 ES model
上面咱們定義 ES 的索引,接着就是定義索引對應的模型,將數據存到這個模型中,而後再存到 ES 中。
ES 模型以下,共四個字段:id、title、answer、typeName。和 ES 索引是相互對應的。
@Data public class QuestionEsModel { private Long id; private String title; private String answer; private String typeName; }
3.4 觸發保存的時機
當咱們在後臺建立題目或保存題目時,先將數據保存到 mysql 數據庫,而後再保存到 ES 中。
以下圖所示,在管理後臺建立題目時,觸發保存數據到 ES 。
第一步,保存數據到 mysql 中,項目中已經包含此功能,就再也不講解了,直接進入第二步:保存數據到 ES 中。
而保存數據到 ES 中,須要將數據組裝成 ES 索引對應的數據,因此我用了一個 ES model,先將數據保存到 ES model 中。
3.5 用 model 來組裝數據
這裏的關鍵代碼時 copyProperties
,能夠將 question
對象的數據取出,而後賦值到 ES model 中。不過 ES model 中還有些字段是 question 中沒有的,因此須要單獨拎出來賦值,好比 typeName 字段,question 對象中沒有這個字段,它對應的字段是 question.type,因此咱們把 type 取出來賦值到 ES model 的 typeName 字段上。以下圖所示:
用 model 來組裝數據
3.6 保存數據到 ES
我在 passjava-search 微服務中寫了一個保存題目的 api 用來保存數據到 ES 中。
保存數據到 ES
而後在 passjava-question 微服務中調用 search 微服務的保存 ES 的方法就能夠了。
// 調用 passjava-search 服務,將數據發送到 ES 中保存。 searchFeignService.saveQuestion(esModel);
3.7 檢驗 ES 中是否建立成功
咱們能夠經過 kibana 的控制檯來查看 question 索引中的文檔。經過如下命令來查看:
GET question/_search
執行結果以下圖所示,有一條記錄:
另外你們有沒有疑問:能夠重複更新題目嗎?
答案是能夠的,保存到 ES 的數據是冪等的,由於保存的時候帶了一個相似數據庫主鍵的 id。
4、實戰:查詢 ES 數據
咱們已經將數據同步到了 ES 中,如今就是前端怎麼去查詢 ES 數據中,這裏咱們仍是使用 Postman 來模擬前端查詢請求。
4.1 定義請求參數
請求參數我定義了三個:
-
keyword:用來匹配問題或者答案。
-
id:用來匹配題目 id。
-
pageNum:用來分頁查詢數據。
這裏我將這三個參數定義爲一個類:
@Data public class SearchParam { private String keyword; // 全文匹配的關鍵字 private String id; // 題目 id private Integer pageNum; // 查詢第幾頁數據 }
4.2 定義返回參數
返回的 response 我也定義了四個字段:
-
questionList:查詢到的題目列表。
-
pageNum:第幾頁數據。
-
total:查詢到的總條數。
-
totalPages:總頁數。
定義的類以下所示:
@Data public class SearchQuestionResponse { private List<QuestionEsModel> questionList; // 題目列表 private Integer pageNum; // 查詢第幾頁數據 private Long total; // 總條數 private Integer totalPages; // 總頁數 }
4.3 組裝 ES 查詢參數
調用 ES 的查詢 API 時,須要構建查詢參數。
組裝查詢參數的核心代碼以下所示:
組裝查詢參數
-
第一步:建立檢索請求。
-
第二步:設置哪些字段須要模糊匹配。這裏有三個字段:title,answer,typeName。
-
第三步:設置如何分頁。這裏分頁大小是 5 個。
-
第四步:調用查詢 api。
4.4 格式化 ES 返回結果
ES 返回的數據是 ES 定義的格式,真正的數據被嵌套在 ES 的 response 中,因此須要格式化返回的數據。
核心代碼以下圖所示:
格式化 ES 返回結果
-
第一步:獲取查到的數據。
-
第二步:獲取真正命中的結果。
-
第三步:格式化返回的數據。
-
第四步:組裝分頁參數。
4.5 測試 ES 查詢
4.5.1 實驗一:測試 title 匹配
咱們如今想要驗證 title 字段是否能匹配到,傳的請求參數 keyword = 111,匹配到了 title = 111 的數據,且只有一條。頁碼 pageNum 我傳的 1,表示返回第一頁數據。以下圖所示:
測試匹配 title
4.5.2 實驗二:測試 answer 匹配
咱們如今想要驗證 answer 字段是否能匹配到,傳的請求參數 keyword = 測試答案,匹配到了 title = 測試答案的數據,且只有一條,說明查詢成功。以下圖所示:
測試匹配 answer
4.5.2 實驗三:測試 id 匹配
咱們如今想要匹配題目 id 的話,須要傳請求參數 id,並且 id 是精確匹配。另外 id 和 keyword 是取並集,因此不能傳 keyword 字段。
請求參數 id = 5,返回結果也是 id =5 的數據,說明查詢成功。以下圖所示:
測試 id 匹配
5、總結
本文經過個人開源項目 passjava 來說解 ES 的整合,ES 的 API 使用以及測試。很是詳細地講解了每一步該如何作,相信經過閱讀本篇後,再加上本身的實踐,必定能掌握先後端該如何使用 ES 來達到高效搜索的目的。
固然,ES API 還有不少功能未在本文實踐,有興趣的同窗能夠到 ES 官網進行查閱和學習。
再次強調:本文的代碼都是辛苦調試出來的,請不要忘記點贊和轉發哦~
- END -
寫了兩本 PDF, 回覆 分佈式 或 PDF 下 載。
個人 JVM 專欄已上架,回覆 JVM 領取
我是悟空,努力變強,變身超級賽亞人!
本文分享自微信公衆號 - 悟空聊架構(PassJava666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。