天天看點

Spring Boot 教程 - Elasticsearch

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。

它能很友善的使大量資料具有搜尋、分析和探索的能力。充分利用Elasticsearch的水準伸縮性,能使資料在生産環境變得更有價值。Elasticsearch 的實作原理主要分為以下幾個步驟,首先使用者将資料送出到Elasticsearch 資料庫中,再通過分詞控制器去将對應的語句分詞,将其權重和分詞結果一并存入資料,當使用者搜尋資料時候,再根據權重将結果排名,打分,再将傳回結果呈現給使用者。

Elasticsearch可以用于搜尋各種文檔。它提供可擴充的搜尋,具有接近實時的搜尋,并支援多租戶。”Elasticsearch是分布式的,這意味着索引可以被分成分片,每個分片可以有0個或多個副本。每個節點托管一個或多個分片,并充當協調器将操作委托給正确的分片。再平衡和路由是自動完成的。“相關資料通常存儲在同一個索引中,該索引由一個或多個主分片和零個或多個複制分片組成。一旦建立了索引,就不能更改主分片的數量。

Elasticsearch使用Lucene,并試圖通過JSON和Java API提供其所有特性。它支援facetting和percolating,如果新文檔與注冊查詢比對,這對于通知非常有用。另一個特性稱為“網關”,處理索引的長期持久性;例如,在伺服器崩潰的情況下,可以從網關恢複索引。Elasticsearch支援實時GET請求,适合作為NoSQL資料存儲,但缺少分布式事務。

  1. Elasticsearch深入了解

    2.1 Elasticsearch的底層實作

2.1.1 lucene

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

Lucene是一套用于全文檢索和搜尋的開源程式庫,由Apache軟體基金會支援和提供。Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋。在Java開發環境裡Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是目前以及最近幾年最受歡迎的免費Java資訊檢索程式庫。至于lucene到底是怎麼實作的,牛牛們可能要自己去百度或者谷歌一下啦。

2.1.2 Elasticsearch的基本概念

叢集(Cluster):就是多台ES伺服器在一起構成搜尋伺服器,現在很多應用基本上都有叢集的概念,提高性能,讓應用具有高可用性,一台伺服器挂掉,可以很快有另一台ES伺服器補上。

節點(Node):節點就是叢集中的某一台ES伺服器就稱為一個節點。

索引庫(Index Indices):就是ES伺服器上的某一個索引,相當于Mysql資料庫中的資料庫的概念,一個節點可以有很多個索引庫。

文檔類型(Type):這個概念就相當于Mysql資料庫中表的概念,一個索引庫可以有很多個文檔類型,但是這個概念現在慢慢淡化了,因為在ES中一個索引庫直接存資料文檔就挺好的,這個概念現在來說有點多餘了,是以ES官方也在淡化這個概念,在ES8中,這個概念将會徹底的消失。

文檔(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 |

現在,如果我們想搜尋 quick和brown這兩個關鍵字,我們隻需要查找包含每個詞條的文檔,就相當于我們查詢的時候,是通過這個索引表找到文檔,在通過文檔去找文檔内容中的搜尋關鍵字,與傳統的通過關鍵字去找内容是不同的。

反向索引到底是個怎麼實作的,怎麼個思想,我在這裡就不一一說明了,大家可以看下官方的詳細介紹:反向索引的原理

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

2.2 Elasticsearch的安裝

本示範項目ES版本為7.0.0版本,其他版本的ES的maven依賴與其他的jar包關系請自行查閱官方文檔,保證不沖突。

Windows

Es伺服器的安裝很簡單,Windows版本特别的簡單,直接去官網下載下傳,運作 bin/elasticsearch 或者binelasticsearch.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
  1. 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:

<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, 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。

import com.google.gson.Gson;

import org.springframework.context.annotation.Bean;

  • @description: GsonConfig

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;

  • @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;

  • @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;

  • @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.service.IndexService;

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.common.xcontent.XContentType;

import org.springframework.stereotype.Service;

import javax.annotation.Resource;

import java.io.IOException;

  • @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(字元串加,數字不支援)。

@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中的聚合函數,這個示例我就不再解釋了,代碼注釋很清楚:

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關鍵字即可。

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的功能遠遠不止這些,它還可以高亮搜尋,資料分析等等。我在這裡示範的僅僅隻是皮毛,甚至都不是皮毛,僅作為初學者的參考。如有大佬覺得我哪裡寫錯了,或者有不同見解,歡迎留言。

  1. 項目位址

    本項目傳送門:

GitHub ---> spring-boot-elasticsearch

Gitee ---> spring-boot-elasticsearch

此教程會一直更新下去,覺得部落客寫的可以的話,關注一下,也可以更友善下次來學習。

原文位址

https://www.cnblogs.com/Butterfly-Tri/p/13081498.html