Spring Boot 教程 - Elasticsearch

1. Elasticsearch簡介

Elasticsearch是一個基於Lucene的搜索服務器。它提供了一個分佈式多用戶能力的全文搜索引擎,基於RESTful web接口。Elasticsearch是用Java語言開發的,並做爲Apache許可條款下的開放源碼發佈,是一種流行的企業級搜索引擎。Elasticsearch用於雲計算中,可以達到實時搜索,穩定,可靠,快速,安裝使用方便。官方客戶端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和許多其餘語言中都是可用的。根據DB-Engines的排名顯示,Elasticsearch是最受歡迎的企業搜索引擎,其次是Apache Solr,也是基於Lucene。之後再給你們詳細介紹solr。html

它能很方便的使大量數據具備搜索、分析和探索的能力。充分利用Elasticsearch的水平伸縮性,能使數據在生產環境變得更有價值。Elasticsearch 的實現原理主要分爲如下幾個步驟,首先用戶將數據提交到Elasticsearch 數據庫中,再經過分詞控制器去將對應的語句分詞,將其權重和分詞結果一併存入數據,當用戶搜索數據時候,再根據權重將結果排名,打分,再將返回結果呈現給用戶。java

Elasticsearch能夠用於搜索各類文檔。它提供可擴展的搜索,具備接近實時的搜索,並支持多租戶。」Elasticsearch是分佈式的,這意味着索引能夠被分紅分片,每一個分片能夠有0個或多個副本。每一個節點託管一個或多個分片,並充當協調器將操做委託給正確的分片。再平衡和路由是自動完成的。「相關數據一般存儲在同一個索引中,該索引由一個或多個主分片和零個或多個複製分片組成。一旦建立了索引,就不能更改主分片的數量。mysql

Elasticsearch使用Lucene,並試圖經過JSON和Java API提供其全部特性。它支持facetting和percolating,若是新文檔與註冊查詢匹配,這對於通知很是有用。另外一個特性稱爲「網關」,處理索引的長期持久性;例如,在服務器崩潰的狀況下,能夠從網關恢復索引。Elasticsearch支持實時GET請求,適合做爲NoSQL數據存儲,但缺乏分佈式事務。git

2. Elasticsearch深刻了解

2.1 Elasticsearch的底層實現

  • 2.1.1 lucenegithub

    Es是一個比較複雜的搜索服務器,自己也是使用Java語言編寫的,在上面的簡介中,說明了ES是一個基於lucene的搜索服務器,lucene是什麼呢?Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎。lucene也是使用Java語言編寫的,Java天下第一😁!web

    Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,可以作全文索引和搜尋。在Java開發環境裏Lucene是一個成熟的免費開源工具。就其自己而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。至於lucene究竟是怎麼實現的,牛牛們可能要本身去百度或者谷歌一下啦。spring

  • 2.1.2 Elasticsearch的基本概念sql

    1. 集羣(Cluster):就是多臺ES服務器在一塊兒構成搜索服務器,如今不少應用基本上都有集羣的概念,提升性能,讓應用具備高可用性,一臺服務器掛掉,能夠很快有另外一臺ES服務器補上。數據庫

    2. 節點(Node):節點就是集羣中的某一臺ES服務器就稱爲一個節點。apache

    3. 索引庫(Index Indices):就是ES服務器上的某一個索引,至關於Mysql數據庫中的數據庫的概念,一個節點能夠有不少個索引庫。

    4. 文檔類型(Type):這個概念就至關於Mysql數據庫中表的概念,一個索引庫能夠有不少個文檔類型,可是這個概念如今慢慢淡化了,由於在ES中一個索引庫直接存數據文檔就挺好的,這個概念如今來講有點多餘了,因此ES官方也在淡化這個概念,在ES8中,這個概念將會完全的消失。

    5. 文檔(Doc):文檔就至關於Mysql是數據庫中某個表的一條數據記錄,如今ES已經到7.7版本了,咱們也就忽略type這個概念,直接在索引庫中存文檔便可。另外須要說一下,咱們通常把數據文檔存到Es服務器的某個索引庫的這個動做稱之爲索引

      最後還有兩個比較重要的概念,可是可能不是那麼直觀的能夠感覺獲得:

      分片(Shards)和副本(Replicas)

      索引可能會存儲大量數據,這些數據可能超過單個節點的硬件限制。例如,十億個文檔的單個索引佔用了1TB的磁盤空間,可能不適合單個節點的磁盤,或者可能太慢而沒法單獨知足來自單個節點的搜索請求。

      爲了解決此問題,Elasticsearch提供了將索引細分爲多個碎片的功能。建立索引時,只需定義所需的分片數量便可。每一個分片自己就是一個功能齊全且獨立的「索引」,能夠託管在羣集中的任何節點上。

      分片很重要,主要有兩個緣由:

      • 它容許您水平分割/縮放內容量
      • 它容許您跨碎片(可能在多個節點上)分佈和並行化操做,從而提升性能/吞吐量

      分片如何分佈以及其文檔如何聚合回到搜索請求中的機制由Elasticsearch徹底管理,而且對您做爲用戶是透明的。

      在隨時可能發生故障的網絡/雲環境中,很是有用,強烈建議您使用故障轉移機制,以防碎片/節點因某種緣由脫機或消失。爲此,Elasticsearch容許您將索引分片的一個或多個副本製做爲所謂的副本分片(簡稱副本)。

      複製很重要,主要有兩個緣由:

      • 若是分片/節點發生故障,它可提供高可用性。所以,重要的是要注意,副本碎片永遠不會與從其複製原始/主要碎片的節點分配在同一節點上。
      • 因爲能夠在全部副本上並行執行搜索,所以它能夠擴展搜索量/吞吐量。

      總而言之,每一個索引能夠分爲多個碎片。索引也能夠複製零(表示沒有副本)或屢次。複製後,每一個索引將具備主碎片(從中進行復制的原始碎片)和副本碎片(主碎片的副本)。能夠在建立索引時爲每一個索引定義分片和副本的數量。建立索引後,您能夠隨時動態更改副本數,但不能過後更改分片數。

      默認狀況下,Elasticsearch中的每一個索引分配有5個主碎片和1個副本,這意味着若是集羣中至少有兩個節點,則索引將具備5個主碎片和另外5個副本碎片(1個完整副本),總共每一個索引10個碎片。

  • 2.1.3 Elasticsearch的索引原理

    Es做爲一個全文檢索服務器,那麼它在搜索方面確定很在行啦!那它是怎麼作到的呢?

    Es官方有這麼一句話:一切設計都是爲了提升搜索的性能!

    Es可以快速的搜索出咱們須要的內容,靠的就是倒排索引的思想,或者說是一種設計!

    在沒有使用倒排索引的狀況下,正常思路是根據搜索關鍵字去查找相應的內容,可是使用了倒排索引以後,ES會先將文檔的全部內容拆分紅多個詞條,建立一個包含全部不重複詞條的排序列表,而後列出每一個詞條出如今哪一個文檔。

    例如,假設咱們有兩個文檔,每一個文檔的 content 域包含以下內容:

    ​ Doc_1:The quick brown fox jumped over the lazy dog

    ​ Doc_2:Quick brown foxes leap over lazy dogs in summer

    ES首先會將這兩個文檔拆分紅多個單獨的詞,或者叫作詞條,而後爲全部的詞條建立一個排序列表,並記錄每一個詞條出現的文檔的信息。就像下面這樣:

    Term      Doc_1  Doc_2
    -------------------------
    Quick   |       |  X                        /*
    The     |   X   |								Term就是詞條,好比第一個Term就是Quick關鍵字,在Doc_1中不存
    brown   |   X   |  X							在,在Doc_2中存在,其餘的以此類推。
    dog     |   X   |							*/
    dogs    |       |  X
    fox     |   X   |
    foxes   |       |  X
    in      |       |  X
    jumped  |   X   |
    lazy    |   X   |  X
    leap    |       |  X
    over    |   X   |  X
    quick   |   X   |
    summer  |       |  X
    the     |   X   |
    ------------------------

    如今,若是咱們想搜索 quickbrown這兩個關鍵字,咱們只須要查找包含每一個詞條的文檔,就至關於咱們查詢的時候,是經過這個索引表找到文檔,在經過文檔去找文檔內容中的搜索關鍵字,與傳統的經過關鍵字去找內容是不一樣的。

    倒排索引究竟是個怎麼實現的,怎麼個思想,我在這裏就不一一說明了,你們能夠看下官方的詳細介紹:倒排索引的原理

    還有es官方的一系列的說明也均可以瞭解一下:什麼是Elasticsearch?

2.2 Elasticsearch的安裝

本演示項目ES版本爲7.0.0版本,其餘版本的ES的maven依賴與其餘的jar包關係請自行查閱官方文檔,保證不衝突。

  • Windows

    Es服務器的安裝很簡單,Windows版本特別的簡單,直接去官網下載,運行 bin/elasticsearch 或者bin\elasticsearch.bat

  • Linux(CentOS7)

    首先咱們去官網下載ES的tar.gz包,而後自建一個文件夾放好,而後解壓tar.zg壓縮包:

    tar -xvf elasticsearch-7.0.0.tar.gz

    而後進入到bin目錄下:

    cd elasticsearch-7.0.0/bin

    而後運行elasticsearch:

    ./elasticsearch

    這個時候確定會報錯的,由於沒有進行配置,因此咱們先對es進行一些簡單的配置,保證能單機運行,進入elasticsearch-7.7.0/config目錄,對es的核心配置文件進行編輯:

    vim elasticsearch.yml

    進入到了elasticsearch.yml文件的編輯頁面:

    首先咱們配置集羣名稱,集羣名稱本身取一個喜歡的名字就好:

    接下來配置節點名稱,就是在這個集羣中,這個es服務器的名稱:

    接下來配置一些必要的參數:

    bootstrap.memory_lock: 是否鎖住內存,避免交換(swapped)帶來的性能損失,默認值是: false。

    bootstrap.system_call_filter: 是否支持過濾掉系統調用。elasticsearch 5.2之後引入的功能,在bootstrap的時候check是否支持seccomp。

    配置network爲全部人均可以訪問,由於咱們通常是使用ssh鏈接工具在其餘的電腦上操做Linux系統,因此咱們須要配置一下:

    到這裏就配置完成了,可是當你從新去運行.elasticsearch的可執行文件的時候,依然會報錯。

    報錯信息中可能包含如下幾個錯誤:

    • max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]

      緣由:沒法建立本地文件問題,用戶最大可建立文件數過小。

      解決方法:切換到root帳戶下,進入Linux系統文件夾,編輯limits.conf文件:

      vim /etc/security/limits.conf

      在文件的末尾加上:

      *                soft    nofile          65536
      *                hard    nofile          65536
      *                soft    nproc           4096
      *                hard    nproc           4096
    • max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

      緣由:最大虛擬內存過小,須要修改系統變量的最大值。

      解決方法:切換到root帳戶下,進入Linux系統文件夾,編輯sysctl.conf文件:

      vim /etc/sysctl.conf

      在文件的末尾加上:

      vm.max_map_count=262144
    • max number of threads [1024] for user [es] likely too low, increase to at least [2048]

      緣由:沒法建立本地線程問題,用戶最大可建立線程數過小。

      解決方法:若是你是CentOS6及如下系統,編輯的文件是90-nproc.conf這個文件,若是你和我同樣使用的是CentOS7的話,編輯的文件是20-nproc.conf文件,其實這兩個文件是同樣的,只是在不一樣CentOS系統中名稱不同而已。

      CentOS7使用這個命令:

      vim /etc/security/limits.d/20-nproc.conf

      CentOS6使用這個命令:

      vim /etc/security/limits.d/90-nproc.conf

      只須要在文件中加上如下配置:

      *          soft    nproc     4096

      這個配置的意思是說賦予其餘用戶的可建立本地線程數爲4096。在這個文件中原本就有一個配置,意思是說賦予root帳戶建立線程數不受限制。咱們就把上面的配置加在原本存在的配置的下面一行就能夠了。

      若是是CentOS7的使用者,還須要配置另外一個文件,不然這個最大線程數是不會生效的。CentOS 7 使用systemd替換了SysV,Systemd目的是要取代Unix時代以來一直在使用的init系統,兼容SysV和LSB的啓動腳本,並且夠在進程啓動過程當中更有效地引導加載服務。在/etc/systemd目錄下有一個系統的默認管理配置,這裏有登錄、日誌、服務、系統等。因此CentOS7的使用者還須要配置下面這個文件:

      vim /etc/systemd/system.conf

      對其中的選項進行配置,在文件的末尾加上:

      DefaultLimitNOFILE=65536
      DefaultLimitNPROC=4096

    上面的因此錯誤解決完畢以後,咱們再運行.elasticsearch可執行文件,es才能夠啓動成功。

2.3 Elasticsearch的使用

首先給你們介紹一個谷歌瀏覽器插件,這個插件是用來可視化展現es的索引庫數據的,這個插件叫作ElasticVue,我的感受挺好用的,展現也比較方便,給你們截個圖看看:

你們可使用這個創建索引庫,而後調用es官方的es專用的語法操做es服務器進行CRUD操做,可是此處我只介紹Java語言如何調用es服務器API,廢話很少說,咱們直接開始下一步。

  • 2.3.1 引入依賴

    搭建工程的過程我就不演示了,直接上pom.xml依賴文件。

    pom.xml

    <!--springboot父工程-->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.2.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <dependencies>
            <!--springboot-web組件-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.2.2.RELEASE</version>
            </dependency>
            <!--elasticsearch-rest-client組件-->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-client</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--elasticsearch-rest-high-level-client組件-->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-high-level-client</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--elasticsearch組件-->
            <dependency>
                <groupId>org.elasticsearch</groupId>
                <artifactId>elasticsearch</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--mybatis整合springboot組件-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>
            <!--mysql數據庫鏈接驅動-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.18</version>
            </dependency>
            <!--lombok組件-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.10</version>
            </dependency>
            <!--json組件gson-->
            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.8.5</version>
            </dependency>
            <!--springboot-test組件-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-test</artifactId>
            </dependency>
            <!--單元測試junit組件-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
            <!--spring-test組件-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>5.2.2.RELEASE</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <!--springboot的maven插件-->
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <compilerArgs>
                            <arg>-parameters</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
  • 2.3.2 Elasticsearch的配置類和Gson配置類和應用配置文件

    application.yml

    butterflytri:
      databaseurl-port: 127.0.0.1:3306 # 數據庫端口
      database-name: student_db # 數據庫名
      host: 192.168.129.100:9200 # es服務端
    server:
      port: 8080 # 應用端口
      servlet:
        context-path: /butterflytri # 應用映射
    spring:
      application:
        name: mybatis # 應用名稱
      datasource:
        url: jdbc:mysql://${butterflytri.databaseurl-port}/${butterflytri.database-name}?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
    mybatis:
      type-aliases-package: com.butterflytri.entity # entity別名
      mapper-locations: classpath:com/butterflytri/mapper/*Mapper.xml # mapper映射包掃描

    注意:yml文件中的192.168.129.100:9200是es對外的端口,使用的http協議進行操做,es服務器還有個9300端口,這個端口是es集羣中各個節點進行交流的端口,使用的是tcp協議。因此咱們鏈接的時候,端口要使用9200端口。

    項目啓動類沒有什麼特別的東西,就不展現了。

    ElasticsearchConfig.java

    package com.butterflytri.config;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.FactoryBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: ElasticSearchConfig
     */
    @Configuration
    public class ElasticSearchConfig implements FactoryBean<RestHighLevelClient>, InitializingBean, DisposableBean {
    
        /**
         * {@link FactoryBean<T>}:FactoryBean<T>是spring對外提供的對接接口,當向spring對象使用getBean("..")方法時,
         *                         spring會使用FactoryBean<T>的getObject 方法返回對象。因此當一個類實現的factoryBean<T>接口時,
         *                         那麼每次向spring要這個類時,spring就返回T對象。
         *
         * {@link InitializingBean}:InitializingBean接口爲bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,
         *                          凡是繼承該接口的類,在初始化bean的時候會執行該方法。在spring初始化bean的時候,若是該bean是
         *                          實現了InitializingBean接口,而且同時在配置文件中指定了init-method,系統則是
         *                          先調用afterPropertiesSet方法,而後在調用init-method中指定的方法。
         *
         * {@link DisposableBean}:DisposableBean接口爲bean提供了銷燬方法destroy-method,會在程序關閉前銷燬對象。
         */
    
        @Value("#{'${butterflytri.host}'.split(':')}")
        private String[] host;
    
        private RestHighLevelClient restHighLevelClient;
    
        private RestHighLevelClient restHighLevelClient() {
            restHighLevelClient = new RestHighLevelClient(
    
                    RestClient.builder(new HttpHost(host[0],Integer.valueOf(host[1]),"http"))
    
            );
            return restHighLevelClient;
        }
    
        @Override
        public void destroy() throws Exception {
            restHighLevelClient.close();
        }
    
        @Override
        public RestHighLevelClient getObject() throws Exception {
            return restHighLevelClient;
        }
    
        @Override
        public Class<?> getObjectType() {
            return RestHighLevelClient.class;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            restHighLevelClient();
        }
    
    }

    ES的配置類,這個配置類實現了三個接口,三個接口的做用我也寫上了註釋,你們能夠看下,須要注意的是FactoryBean這個接口,一但實現了這個接口,每當你須要使用泛型表示的對象T的時候,Spring不會從容器中去拿這個對象,而是會調用這個FactoryBean.getObject()方法去拿對象。其餘的就沒有什麼了。

    Gson.java

    Gson是一個操做json數據的類,它的執行效率可能會慢一點,可是它在解析json數據的時候不會出Bug。

    package com.butterflytri.config;
    
    import com.google.gson.Gson;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: GsonConfig
     */
    @Configuration
    public class GsonConfig {
    
        /**
         * {@link Gson}:一個操做json的對象,有比較好的json操做體驗,相對於Alibaba的FastJson來講速度慢一些,可是FastJson在解析
         *              複雜的的json字符串時有可能會出現bug。
         * @return Gson
         */
    
        @Bean
        public Gson gson() {
            return new Gson();
        }
    
    }

    Constants.java

    這是我寫的常量類,放一些ES使用的常量,直接寫字符串也行,可是我建議這樣作。

    package com.butterflytri.constants;
    
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: Constants
     */
    public class Constants {
    
        /**
         * es搜索關鍵字
         */
        public static final String KEYWORD = ".keyword";
    
        /**
         * es的type類型:type字段將在 elasticsearch-version:8 中完全刪除,原本就以爲沒得啥用。
         */
        public static final String DOC_TYPE = "_doc";
    
        /**
         * 學生信息索引類型
         */
        public static final String INDEX_STUDENT = "student_info";
    
    
        /**
         * 自定鏈接符
         */
        public static final String CONNECTOR = " --> ";
    
    }

    Student.java

    package com.butterflytri.entity;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.io.Serializable;
    
    /**
     * @author: WJF
     * @date: 2020/5/16
     * @description: Student
     */
    
    @ToString
    @Getter
    @Setter
    public class Student implements Serializable {
    
        private Long id;
    
        private String studentName;
    
        private String studentNo;
    
        private String sex;
    
        private Integer age;
    
        private String clazz;
    
    }

    StudentMapper.java

    package com.butterflytri.mapper;
    
    import com.butterflytri.entity.Student;
    import org.apache.ibatis.annotations.Mapper;
    
    import java.util.List;
    
    /**
     * @author: WJF
     * @date: 2020/5/16
     * @description: StudentMapper
     */
    @Mapper
    public interface StudentMapper {
    
        /**
         * 查詢全部學生信息
         * @return List<Student>
         */
        List<Student> findAll();
    
        /**
         * 經過id查詢學生信息
         * @param id:學生id
         * @return Student
         */
        Student findOne(Long id);
    
        /**
         * 經過學號查詢學生信息
         * @param studentNo:學生學號
         * @return Student
         */
        Student findByStudentNo(String studentNo);
    
    }

    mybatis的SQL映射文件我就不展現了,也很簡單,你們看接口方法名就應該能夠想象獲得SQL語句是怎樣的。

  • 2.3.3 索引數據到ES服務器

    IndexServiceImpl.java

    package com.butterflytri.service.impl;
    
    import com.butterflytri.constants.Constants;
    import com.butterflytri.entity.Student;
    import com.butterflytri.service.IndexService;
    import com.google.gson.Gson;
    import org.elasticsearch.action.ActionListener;
    import org.elasticsearch.action.index.IndexRequest;
    import org.elasticsearch.action.index.IndexResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.common.xcontent.XContentType;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.io.IOException;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: IndexServiceImpl
     */
    @Service
    public class IndexServiceImpl implements IndexService {
    
        @Resource
        private Gson gson;
    
        @Resource
        private RestHighLevelClient restHighLevelClient;
    
        @Override
        public String index(Student student) {
            StringBuilder builder = new StringBuilder();
            IndexRequest indexRequest = this.initIndexRequest(student);
            try {
                // 同步索引到elasticsearch服務器,獲取索引響應IndexResponse
                IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
                String statusName = indexResponse.status().name();
                int statusCode = indexResponse.status().getStatus();
                builder.append(statusName).append(Constants.CONNECTOR).append(statusCode);
            } catch (IOException e) {
                builder.append("Fail").append(Constants.CONNECTOR).append(e.getMessage());
            }
            return builder.toString();
        }
    
    
        @Override
        public String indexAsync(Student student) {
            StringBuilder builder = new StringBuilder();
            IndexRequest indexRequest = this.initIndexRequest(student);
            // 異步索引到elasticsearch服務器,獲取索引響應IndexResponse
            restHighLevelClient.indexAsync(indexRequest, RequestOptions.DEFAULT,actionListener(builder));
            return builder.toString();
        }
    
    
    
        /**
         * 初始化IndexRequest,並設置數據源。
         * @param student
         * @return IndexRequest
         */
        private IndexRequest initIndexRequest(Student student) {
            // 構建IndexRequest,設置索引名稱,索引類型,索引id
            IndexRequest indexRequest = new IndexRequest(Constants.INDEX_STUDENT);
            // 能夠不設置,默認就是'_doc'
            indexRequest.type(Constants.DOC_TYPE);
            // 設置索引id爲studentId
            indexRequest.id(String.valueOf(student.getId()));
            // 設置數據源
            String studentJson = gson.toJson(student);
            indexRequest.source(studentJson, XContentType.JSON);
            return indexRequest;
        }
    
        /**
         * 異步索引的回調監聽器,根據不一樣的結果作出不一樣的處理
         * @param builder
         * @return ActionListener<IndexResponse>
         */
        private ActionListener<IndexResponse> actionListener(StringBuilder builder) {
            return new ActionListener<IndexResponse>() {
                // 當索引數據到es服務器時,返回不一樣的狀態
                @Override
                public void onResponse(IndexResponse indexResponse) {
                    String statusName = indexResponse.status().name();
                    int statusCode = indexResponse.status().getStatus();
                    builder.append(statusName).append(Constants.CONNECTOR).append(statusCode);
                }
    
                // 當索引數據時出現異常
                @Override
                public void onFailure(Exception e) {
                    builder.append("Fail").append(Constants.CONNECTOR).append(e.getMessage());
                }
            };
        }
    }

    上面的內容很簡單,就是將Student對象格式化爲Json字符串,而後存到es服務器中,你們只要遵照一個規則就好,就是操做es服務器,無論是什麼操做都是用RestHighLevelClient這個類去操做,上面的就是student對象索引的es服務器中,使用restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT),首先就是構建indexRequest對象,這個對象就是索引請求對象,具體幹了什麼看代碼上的註釋。這裏還有個restHighLevelClient.indexAsync()這個方法,這個方法和上面的index方法同樣的效果,只不過是異步調用。

    接下來咱們測試一下這個代碼,請看:

    @Test
        public void indexTest() {
            List<Student> list = studentMapper.findAll();
            for (Student student : list) {
                String message = indexService.index(student);
                System.out.println(message);
            }
        }

    咱們使用ElasticVue插件鏈接es服務器便可看到有一個索引庫:

    當咱們點擊到show按鈕的時候,能夠看到student_info索引庫中有幾條記錄:

    索引數據到數據庫成功了。

  • 2.3.4 獲取Es服務器數據

    獲取數據,是es提供給咱們的API,這個Api只能獲取某個索引的某一條文檔,示例以下:

    GetServiceImpl.java

    @Override
        public Student get(String id) {
            Student student = new Student();
            GetRequest getRequest = new GetRequest(Constants.INDEX_STUDENT, id);
            try {
                GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
                String source = getResponse.getSourceAsString();
                student = gson.fromJson(source, Student.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return student;
        }

    接着咱們在測試類中,調用這個方法而後打印一下結果:

    GetServiceTest.java

    @Test
        public void getTest() {
            Student student = getService.get("1");
            System.out.println(student);
        }

    結果以下:

    更新數據文檔和刪除數據文檔我就不演示了,都是大同小異,你們能夠拉下個人代碼,好好研究一下,都有詳細的註釋,以爲能夠的話,給我點下star也是極好的。下面演示一下searchApi,這個Api是咱們常常須要使用的,特別重要。

  • 2.3.5 搜索Es服務器數據

    ES的搜索API包含不少,好比說組合搜索,區間搜索,高亮顯示,分詞搜索等等。我先給你們演示一下組合搜索,區間搜索其實也是組合搜索的一個子條件,其餘的搜索其實也都是,代碼以下:

    SearchServiceImpl.java

    @Override
        public List<Student> searchRange(Object from, Object to, String field, String index) {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            // 須要搜索的區間字段field
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(field);
            // 左區間
            if (from != null) {
                rangeQueryBuilder.from(from, true);
            }
            // 右區間
            if (to != null) {
                rangeQueryBuilder.to(to, true);
            }
            boolQueryBuilder.must();
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQueryBuilder);
            SearchRequest searchRequest = new SearchRequest(index);
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }

    上面的代碼其實很簡單,就是一個區間查詢構建器,查詢指定字段處於區間的全部數據,rangeQueryBuilder.from(from, true)的第一個參數就是字段的下邊界,第二個參數表明是否包含邊界。SearchResponse就是搜索的響應對象,全部的數據都在SearchHit對象中。

    接下來給你們演示一些組合查詢,這個方法搜索年齡在18到19歲而且班級爲'G0305'的學生。記得ES默認是分頁的,若是想不分頁,必定要記得給搜索字段加上.keyword(字符串加,數字不支持)。

    SearchServiceImpl.java

    @Override
        public List<Student> searchBool() {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            boolQuery.must(QueryBuilders.rangeQuery("age").gte(18).lte(19));
            boolQuery.must(QueryBuilders.termQuery("clazz" + Constants.KEYWORD,"G0305"));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQuery);
            SearchRequest searchRequest = new SearchRequest(Constants.INDEX_STUDENT);
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }

    上面的代碼中的類BoolQueryBuilder就是組合查詢構建器,這個類能夠用來構建組合的條件查詢。boolQuery.must()方法就是用來拼接條件的一種方式,使用這個方法表明必須知足這個條件纔會查詢出來,上面的代碼說明必須知足年齡爲18(包含18)到19(包含19)歲,而且班級爲'G0305'的學生纔會查詢出來。還有其餘的一些常見的組合查詢方法,以下:

    • boolQuery.must():必須知足此條件,至關於=或者&
    • boolQuery.mustNot():必須不知足此條件,至關於!=
    • boolQuery.should():至關於||或者or
    • boolQuery.filter():過濾。

    而後是聚合查詢,很相似於MySQL中的聚合函數,這個示例我就再也不解釋了,代碼註釋很清楚:

    @Override
        public void searchBoolAndAggregation() {
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            boolQuery.must(QueryBuilders.rangeQuery("age").gte(18).lte(19));
            boolQuery.must(QueryBuilders.termQuery("clazz" + Constants.KEYWORD,"G0305"));
            // 聚合分組:按clazz字段分組,並將結果取名爲clazz,es默認是分詞的,爲了精確配置,須要加上‘.keyword’關鍵詞後綴。
            TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("clazz").field("clazz" + Constants.KEYWORD);
            // 聚合求和:求符合查詢條件的學生的年齡的和,並將結果取名爲ageSum,由於不是字符串,因此默認是精確匹配,不支持分詞。
            aggregationBuilder.subAggregation(AggregationBuilders.sum("ageSum").field("age"));
            // 聚合求平均:求符合查詢條件的學生的年齡的平均值,並將結果取名爲ageAvg,由於不是字符串,因此默認是精確匹配,不支持分詞。
            aggregationBuilder.subAggregation(AggregationBuilders.avg("ageAvg").field("age"));
            // 聚合求數量:按學號查詢符合查詢條件的學生個數,並將結果取名爲count,es默認是分詞的,爲了精確配置,須要加上‘.keyword’關鍵詞後綴。
            aggregationBuilder.subAggregation(AggregationBuilders.count("count").field("studentNo" + Constants.KEYWORD));
            SearchSourceBuilder builder = new SearchSourceBuilder();
            builder.query(boolQuery);
            builder.aggregation(aggregationBuilder);
            // 按年齡降序排序。
            builder.sort("age", SortOrder.DESC);
            SearchRequest request = new SearchRequest("student_info");
            request.source(builder);
            try {
                SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    System.out.println(student);
                }
                // 使用Terms對象接收
                Terms clazz = search.getAggregations().get("clazz");
                for (Terms.Bucket bucket : clazz.getBuckets()) {
                    System.out.println(bucket.getDocCount());
    
                    System.out.println("=====================");
                    // 使用ParsedSum對象接收
                    ParsedSum ageCount = bucket.getAggregations().get("ageSum");
                    System.out.println(ageCount.getType());
                    System.out.println(ageCount.getValue());
                    System.out.println(ageCount.getValueAsString());
                    System.out.println(ageCount.getMetaData());
                    System.out.println(ageCount.getName());
    
                    System.out.println("=====================");
                    // 使用ParsedAvg對象接收
                    ParsedAvg ageAvg = bucket.getAggregations().get("ageAvg");
                    System.out.println(ageAvg.getType());
                    System.out.println(ageAvg.getValue());
                    System.out.println(ageAvg.getValueAsString());
                    System.out.println(ageAvg.getMetaData());
                    System.out.println(ageAvg.getName());
    
                    System.out.println("=====================");
                    // 使用ParsedValueCount對象接收
                    ParsedValueCount count = bucket.getAggregations().get("count");
                    System.out.println(count.getType());
                    System.out.println(count.getValue());
                    System.out.println(count.getValueAsString());
                    System.out.println(count.getMetaData());
                    System.out.println(count.getName());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    最後還有分詞查詢,分詞查詢就不加.keyword關鍵字便可。

    @Override
        public List<Student> searchMatch(String matchStudentName) {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            // 分詞查詢時不加'.keyword'關鍵字
            boolQueryBuilder.must(QueryBuilders.matchQuery("studentName",matchStudentName));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQueryBuilder);
            SearchRequest searchRequest = new SearchRequest("student_info");
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits().getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }

    請記住,通常的進行分詞都是字符串才進行分詞搜索,數字等類型只能是精準匹配。

    最後,ES功能很強大,做爲搜索界的扛把子,ES的功能遠遠不止這些,它還能夠高亮搜索,數據分析等等。我在這裏演示的僅僅只是皮毛,甚至都不是皮毛,僅做爲初學者的參考。若有大佬以爲我哪裏寫錯了,或者有不一樣看法,歡迎留言。

3. 項目地址

本項目傳送門:

此教程會一直更新下去,以爲博主寫的能夠的話,關注一下,也能夠更方便下次來學習。

相關文章
相關標籤/搜索