Lucene 和 Kibana、ElasticSeach、Spring Data ElasticSearch

什么是全文检索

数据分类

生活中的数据总体分为两种:结构化数据和非结构化数据。

结构化数据 - 行数据,可以用二维表结构来逻辑表达实现的数据;指具有固定格式或有限长度的数据,如数据库,元数据等。

非结构化数据 - 指不定长或无固定格式的数据,如邮件,word 文档等磁盘上的文件。

结构化数据搜索

常见的结构化数据也就是数据库中的数据。

在数据库中搜索很容易实现,通常都是使用 SQL 语句进行查询,而且能很快的得到查询结果。

代码语言:javascript
复制
客户端请求 ----> 连接器

连接器 ----> 缓存区
连接器 ----> 分析器

分析器 ----> 缓存区
分析器 ----> 优化器

优化器 ----> 执行器

Server 层:

  • 连接器
  • 缓存区
  • 分析器
  • 优化器
  • 执行器

Server 层 ----> 存储引擎(引擎,引擎,引擎)

因为数据库中的数据存储是有规律的,有行有列而且数据格式、数据长度都是固定的,所以数据库搜索很容易。

数据库底层文件存储的物理方式:硬盘是块状存储,其基本单位是 1kb 每块;磁头每次读取数据,至少扫描一个块的大小。

假如表里一个元组,数据加起来一共 100 b,则一个块可以放十个元组,通俗的说是 10 条数据,DBMS 在查询时,就把每一块的数据读取出来,判断其中是否有对应的数据。

常见的关系型数据,其最基本常见的存储方式:

  • 堆 - 随着文件的插入,不停地往尾部上堆;它地访问路径就是顺序扫描,扫完了才能查到数据。
  • Hash - 文件的 hash 值就是其存储地址。
  • 索引 + 堆 - 对堆文件的某一列,建立 b+ 树索引。

非结构化数据查询方法

1)顺序扫描法 (Serial Scanning)

用户搜索 --> 文件

所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用 windows 的搜索也可以搜索文件内容,只是相当的慢。

2)全文检索 (Full-text Search)

用户通过查询索引库 --> 生成索引 --> 文档

全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方法。这个过程类似于通过字典的目录查字的过程。

将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。

例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。

这种先建立索引,再对索引进行搜索的过程就叫全文检索 (Full-Text Search)。虽然创建索引的过程也是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的。

建立索引 --> 检索索引

如何实现全文检索

可以使用 Lucene 实现全文检索。Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。

Lucene 适用场景:

  • 在应用中为数据库中的数据提供全文检索实现。
  • 开发独立的搜索引擎服务、系统

Lucene 的特性:

代码语言:javascript
复制
1. 稳定、索引性能高。

  • 每小时能够索引 150 GB 以上的数据
  • 对内存的要求小,只需要 1 MB 的堆内存
  • 增量索引和批量索引一样快
  • 索引的大小约为索引文本大小的 20% ~ 30%
  1. 高效、准确、高性能的搜索算法。
  • 良好的搜索排序
  • 强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等
  • 支持字段搜索(如标题、作者、内容)
  • 可根据任意字段排序
  • 支持多个索引查询结果合并
  • 支持更新操作和查询操作同时进行
  • 支持高亮、join、分组结果功能
  • 速度快
  • 可扩展排序模块,内置包含向量空间模型、BM25 模型可选
  • 可配置存储引擎
  1. 跨平台。
  • 纯 java 编写
  • 作为 Apache 开源许可下的开源项目,你可以在商业或开源项目中使用
  • Lucene 有多种语言实现版(如 C,C++、Python 等),不仅仅是 JAVA

Lucene 架构:

代码语言:javascript
复制
Application:
  • 从文件系统、数据库、Web、手动输入获取数据,然后生成索引。
  • 获取用户的 Query,然后查询索引,返回查询结果给用户。
  • 用户
    --职位搜索--> 应用服务器
    --SQL--> 结构化数据库

    Lucene:

    • 生成索引需要与索引库交互。
    • 查询索引需要与索引库交互。

    用户
    --职位搜索--> 应用服务器
    --特定API--> Lucene索引库

    数据源(网络、数据库、文档)
    ----> Lucene索引库

    全文检索的应用场景

    对于数据量大、数据结构不固定的数据可采用全文检索方式搜索:

    • 单机软件的搜索:word、markdown。
    • 站内搜索:京东、淘宝、拉勾,索引源是数据库。
    • 搜索引擎:百度、Google,索引源是爬虫程序抓取的数据。

    Lucene 实现全文检索的流程说明

    索引和搜索流程图

    代码语言:javascript
    复制
    查询索引:

    1. 用户查询接口
    2. 创建查询
    3. 执行查询
    4. 渲染结果

    用户查询索引
    -----> 索引库

    原始文档
    ----> 创建索引
    --创建索引--> 索引库
    --返回结果--> 用户查询索引

    创建索引:

    1. 获取文档
    2. 构建文档对象
    3. 分析文档(分词)
    4. 创建索引

    对要搜索的原始内容进行索引构建一个索引库,索引过程包括:确定原始内容即要搜索的内容 --> 采集文档 --> 创建文档 --> 分析文档 --> 索引文档。

    从索引库中搜索内容,搜索过程包括:用户通过搜索界面 --> 创建查询 --> 执行搜索从索引库搜索 --> 渲染搜索结果。

    创建索引

    核心概念:

    Document:

    • 用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。
    • 一条记录经过索引之后,就是以一个 Document 的形式存储在索引文件中的。
    • 用户进行搜索,也是以 Document 列表的形式返回。

    Field:

    • 一个 Document 可以包含多个信息域。例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过 Field 在 Document 中存储的。
    • Field 有两个属性可选:存储和索引。通过存储属性可以控制是否对这个 Field 进行存储;通过索引属性可以控制是否对该 Field 进行索引。
    • 如果对标题和正文进行全文搜索,要把索引属性设置为真,同时希望能直接从搜索结果中提取文章标题,把标题域的存储属性设置为真;但是由于正文域太大了,为了缩小索引文件大小,可以将正文域的存储属性设置为假,当需要时再直接读取文件;如果只是希望能从搜索结果中提取最后修改时间,不需要对它进行搜索,可以把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上 Field 不允许那么设置,因为既不存储又不索引的域是没有意义的。

    Term:

    • 是搜索的最小单位,它表示文档的一个词语,Term 由两部分组成:它表示的词语和这个词语所出现的 Field 的名称。

    以招聘网站的搜索为例,在网站上输入关键字搜索显示的内容不是直接从数据库中来的,而是从索引库中获取的,网站的索引数据需要提前创建的。以下是创建的过程:

    • 获得原始文档 - 就是从 MySQL 数据库中通过 SQL 语句查询需要创建索引的数据。
    • 创建文档对象(Document)- 把查询的内容构建成 lucene 能识别的 Document 对象,获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档,文档中包括一个一个的域(Field),这个域对应就是表中的列。
    • 注意 - 每个 Document 可以有多个 Field,不同的 Document 可以有不同的 Field,同一个Document 可以有相同的 Field(域名和域值都相同)。每个文档都有一个唯一的编号,就是文档 id。
    • 分析文档 - 将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词。分好的词会组成索引库中最小的单元:term,一个 term 由域名和词组成。
    • 创建索引 - 对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到 Document(文档)。
    • 注意 - 创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。倒排索引结构是根据内容(词语)找文档。倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。

    倒排索引

    倒排索引 Inverted List 记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的文档及出现的位置。

    文档:索引库中的每一条原始数据,例如一个商品信息、一个职位信息。

    词条:原始数据按照分词算法进行分词,得到的每一个词。

    创建倒排索引,分为以下几步:

    1)创建文档列表 - Lucene 首先对原始文档数据进行编号 DocID,形成列表,就是一个文档列表。

    2)创建倒排索引列表 - 对文档中数据进行分词,得到词条(分词后的一个又一个词)。对词条进行编号,以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)。

    搜索的过程:当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条,然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号。然后根据这些编号去文档列表中找到文档

    查询索引

    查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档

    第一步:创建用户接口 - 用户输入关键字的地方。

    第二步:创建查询 - 指定查询的域名和关键字。

    第三步:执行查询。

    第四步:渲染结果(结果内容显示到页面上 关键字需要高亮)。

    Lucene 实战

    需求说明

    生成职位信息索引库,从索引库检索数据。

    创建数据库 es,将 sql 脚本导入数据库执行。

    代码语言:javascript
    复制
    /*!40101 SET NAMES utf8 */;
    
    

    /!40101 SET SQL_MODE=''/;

    /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 /;
    /
    !40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
    /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
    /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
    CREATE DATABASE /*!32312 IF NOT EXISTS*/es /*!40100 DEFAULT CHARACTER SET latin1 */;

    USE es;

    /*Table structure for table job_info */

    DROP TABLE IF EXISTS job_info;

    CREATE TABLE job_info (
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键 id',
    company_name varchar(100) DEFAULT NULL COMMENT '公司名称',
    company_addr varchar(200) DEFAULT NULL COMMENT '公司联系方式',
    company_info mediumtext COMMENT '公司信息',
    job_name varchar(100) DEFAULT NULL COMMENT '职位名称',
    job_addr varchar(50) DEFAULT NULL COMMENT '工作地点',
    job_info mediumtext COMMENT '职位信息',
    salary_min int(10) DEFAULT NULL COMMENT '薪资范围,最小',
    salary_max int(10) DEFAULT NULL COMMENT '薪资范围,最大',
    url varchar(150) DEFAULT NULL COMMENT '招聘信息详情页',
    time varchar(10) DEFAULT NULL COMMENT '职位最近发布时间',
    PRIMARY KEY (id)
    ) ENGINE=InnoDB AUTO_INCREMENT=7656 DEFAULT CHARSET=utf8 COMMENT='招聘信息';

    中文分词器使用 IK。

    准备开发环境

    第一步:创建一个 maven 工程,创建一个 Spring Boot 项目
    第二步:导入依赖
    代码语言:javascript
    复制
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.renda</groupId>
    <artifactId>lucene-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>lucene-demo</name>
    <description>Demo project for Spring Boot</description>

    &lt;properties&gt;
        &lt;java.version&gt;11&lt;/java.version&gt;
    &lt;/properties&gt;
    
    &lt;dependencies&gt;
        &lt;!-- web 依赖 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;!-- 测试依赖 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;!-- lombok 工具 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
            &lt;version&gt;1.18.4&lt;/version&gt;
            &lt;scope&gt;provided&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;!-- 热部署 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;
            &lt;optional&gt;true&lt;/optional&gt;
        &lt;/dependency&gt;
        &lt;!-- mybatis-plus --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.baomidou&lt;/groupId&gt;
            &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;3.3.2&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- pojo 持久化使用 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;javax.persistence&lt;/groupId&gt;
            &lt;artifactId&gt;javax.persistence-api&lt;/artifactId&gt;
            &lt;version&gt;2.2&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- mysql 驱动 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;!-- 引入 Lucene 核心包及分词器包 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.lucene&lt;/groupId&gt;
            &lt;artifactId&gt;lucene-core&lt;/artifactId&gt;
            &lt;version&gt;4.10.3&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.lucene&lt;/groupId&gt;
            &lt;artifactId&gt;lucene-analyzers-common&lt;/artifactId&gt;
            &lt;version&gt;4.10.3&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.testng&lt;/groupId&gt;
            &lt;artifactId&gt;testng&lt;/artifactId&gt;
            &lt;version&gt;RELEASE&lt;/version&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;!-- IK 中文分词器 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.janeluo&lt;/groupId&gt;
            &lt;artifactId&gt;ikanalyzer&lt;/artifactId&gt;
            &lt;version&gt;2012_u6&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
    
    &lt;build&gt;
        &lt;plugins&gt;
            &lt;!-- 编译插件 --&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
                &lt;configuration&gt;
                    &lt;source&gt;11&lt;/source&gt;
                    &lt;target&gt;11&lt;/target&gt;
                    &lt;encoding&gt;utf-8&lt;/encoding&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
            &lt;!-- 打包插件 --&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
                &lt;executions&gt;
                    &lt;execution&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;repackage&lt;/goal&gt;
                        &lt;/goals&gt;
                    &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
    

    </project>

    第三步:创建引导类 com.renda.LuceneDemoApplication
    代码语言:javascript
    复制
    package com.renda;

    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    @MapperScan("com.renda.mapper")
    public class LuceneDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LuceneDemoApplication.class, args);
    }
    

    }

    第四步:配置 application.yml 文件
    代码语言:javascript
    复制
    server:
    port: 9000
    Spring:
    application:
    name: lagou-lucene
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/es?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: password

    开启驼峰命名匹配映射

    mybatis:
    configuration:
    map-underscore-to-camel-case: true

    第五步:创建实体类、Mapper、Service、Controller 等等

    实体类 com.renda.pojo.JobInfo

    代码语言:javascript
    复制
    package com.renda.pojo;

    import lombok.Data;
    import lombok.ToString;

    import javax.persistence.Id;
    import javax.persistence.Table;

    @Data
    @ToString
    @Table(name = "job_info")
    public class JobInfo {

    @Id
    private long id;
    private String companyName;
    private String companyAddr;
    private String companyInfo;
    private String jobName;
    private String jobAddr;
    private String jobInfo;
    private long salaryMin;
    private long salaryMax;
    private String url;
    private String time;
    

    }

    数据层 com.renda.mapper.JobInfoMapper

    代码语言:javascript
    复制
    public interface JobInfoMapper extends BaseMapper<JobInfo> {
    }

    服务层

    com.renda.service.JobInfoService

    代码语言:javascript
    复制
    public interface JobInfoService {

    /**
     * 通过id查询
     */
    public JobInfo selectById(long id);
    
    /**
     * 查询所有
     */
    public List&lt;JobInfo&gt; selectAll();
    

    }

    com.renda.service.impl.JobInfoServiceImpl

    代码语言:javascript
    复制
    @Service
    public class JobInfoServiceImpl implements JobInfoService {

    @SuppressWarnings(&#34;SpringJavaInjectionPointsAutowiringInspection&#34;)
    @Autowired
    private JobInfoMapper jobInfoMapper;
    
    @Override
    public JobInfo selectById(long id) {
        return jobInfoMapper.selectById(id);
    }
    
    @Override
    public List&lt;JobInfo&gt; selectAll() {
        return jobInfoMapper.selectList(new QueryWrapper&lt;JobInfo&gt;());
    }
    

    }

    控制层 com.renda.controller.JobInfoController

    代码语言:javascript
    复制
    @RestController
    @RequestMapping("/jobInfo")
    public class JobInfoController {

    @Autowired
    private JobInfoService jobInfoService;
    
    @RequestMapping(&#34;/query/{id}&#34;)
    public JobInfo selectById(@PathVariable Long id){
        return jobInfoService.selectById(id);
    }
    
    @RequestMapping(&#34;/query&#34;)
    public List&lt;JobInfo&gt; select(){
        return jobInfoService.selectAll();
    }
    

    }

    测试类 com.renda.LuceneDemoApplicationTests

    代码语言:javascript
    复制
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class LuceneDemoApplicationTests {
    ...
    }

    整体结构:

    代码语言:javascript
    复制
    + src/main/java

    • com.renda.controller
      • JobInfoController
    • com.renda.mapper
      • JobInfoMapper
    • com.renda.pojo
      • JobInfo
    • com.renda.service
      • impl.JobInfoServiceImpl
      • JobInfoService
    • com.renda
      • LuceneDemoApplication
    • src/main/resources
      • static
      • templates
      • application.yml
    • src/test/java
      • com.renda

        • LuceneDemoApplicationTests

    创建索引

    测试类 com.renda.LuceneDemoApplicationTests

    代码语言:javascript
    复制
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class LuceneDemoApplicationTests {

    @Autowired
    private JobInfoService jobInfoService;

    /**

    • 创建索引
      */
      @Test
      public void createIndex() throws Exception {
      // 1.指定索引文件的存储位置,索引具体的表现形式就是一组有规则的文件
      Directory directory = FSDirectory.open(new File("E:/class/index"));
      // 2.配置版本及其分词器
      Analyzer analyzer = new IKAnalyzer();
      IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
      // 3.创建 IndexWriter 对象,作用就是创建索引
      IndexWriter indexWriter = new IndexWriter(directory, config);
      // 先删除已经存在的索引库
      indexWriter.deleteAll();
      // 4.获得索引源 / 原始数据
      List<JobInfo> jobInfoList = jobInfoService.selectAll();
      // 5. 遍历 jobInfoList,每次遍历创建一个 Document 对象
      for (JobInfo jobInfo : jobInfoList) {
      // 创建 Document 对象
      Document document = new Document();
      // 创建 Field 对象,添加到 document 中
      document.add(new LongField("id", jobInfo.getId(), Field.Store.YES));
      // 切分词、索引、存储
      document.add(new TextField("companyName", jobInfo.getCompanyName(), Field.Store.YES));
      document.add(new TextField("companyAddr", jobInfo.getCompanyAddr(), Field.Store.YES));
      document.add(new TextField("companyInfo", jobInfo.getCompanyInfo(), Field.Store.YES));
      document.add(new TextField("jobName", jobInfo.getJobName(), Field.Store.YES));
      document.add(new TextField("jobAddr", jobInfo.getJobAddr(), Field.Store.YES));
      document.add(new TextField("jobInfo", jobInfo.getJobInfo(), Field.Store.YES));
      document.add(new LongField("salaryMin", jobInfo.getSalaryMin(), Field.Store.YES));
      document.add(new LongField("salaryMax", jobInfo.getSalaryMax(), Field.Store.YES));
      document.add(new StringField("url", jobInfo.getUrl(), Field.Store.YES));
      // 将文档追加到索引库中
      indexWriter.addDocument(document);
      }
      // 关闭资源
      indexWriter.close();
      System.out.println("Index was created successfully!");
      }

    }

    生成的索引目录:D:\class\index

    索引 Index:

    • 在 Lucene 中一个索引是放在一个文件夹中的。
    • 同一文件夹中的所有的文件构成一个 Lucene 索引。

    段 Segment:

    • 按层次保存了从索引,一直到词的包含关系:索引 (Index) –-> 段 (segment) –-> 文档 (Document) –-> 域 (Field) –-> 词 (Term)。
    • 也即此索引包含了那些段,每个段包含了那些文档,每个文档包含了哪些域,每个域包含了哪些词。
    • 一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
    • 具有相同前缀文件的属同一个段,如 _0
    • segments.gensegments_1 是段的元数据文件,也即它们保存了段的属性信息。
    代码语言:javascript
    复制
    D:\class\index

    _0.cfe
    _0.cfs
    _0.si
    _1.cfe
    _1.cfs
    _1.si
    segments.gen
    segments_1
    write.lock

    Field 的特性:Document (文档) 是 Field (域) 的承载体,一个 Document 由多个 Field 组成,Field 由名称和值两部分组成,Field 的值是要索引的内容,是要搜索的内容。

    • 是否分词 (tokenized) :
    代码语言:javascript
    复制
    是: 将 Field 的值进行分词处理,分词的目的是为了索引。如: 商品名称、商品描述。这些内容用户会通过输入关键词进行查询,由于内容多样,需要进行分词处理建立索引。

    否: 不做分词处理,如: 订单编号、身份证号, 是一个整体,分词以后就失去了意义,故不需要分词。

    • 是否索引 (indexed)
    代码语言:javascript
    复制
    是: 将 Field 内容进行分词处理后得到的词 (或整体 Field 内容) 建立索引,存储到索引域。索引的目的是为了搜索。如: 商品名称, 商品描述需要分词建立索引。订单编号、身份证号作为整体建立索引。只要可能作为用户查询条件的词, 都需要索引。

    否: 不索引。如: 商品图片路径,不会作为查询条件,不需要建立索引。

    • 是否存储 (stored)
    代码语言:javascript
    复制
    是: 将 Field 值保存到 Document 中。如: 商品名称, 商品价格。凡是将来在搜索结果页面展现给用户的内容,都需要存储。

    否: 不存储。如: 商品描述。内容多格式大,不需要直接在搜索结果页面展现,不做存储。需要的时候可以从关系数据库取。

    常用的 Field 类型:

    • StringField(FieldName, FieldValue, Store.YES) - 字符串 - 不可分词 - 默认索引 - 可以开启存储 - 字符串类型 Field,不可分词,作为一个整体进行索引,如身份证号、订单编号;是否需要存储由 Store.YES 或 Store.No 决定。
    • LongField(FieldName, FieldValue, Store.YES) - 数值型代表 - 默认分词 - 默认索引 - 可以开启存储 - Long 数值类型 Field 代表,默认分词并且索引,如价格;是否需要存储由 Store.YES 或 Store.No 决定。
    • StoredField(FieldName, FieldValue) - 重载方法支持多种类型 - 不可分词 - 不可索引 - 默认开启存储 - 构建不同类型的 Field,不分词,不索引,要存储,如商品图片路径。
    • TextField(FieldName, FieldValue, Store.NO) - 文本类型 - 默认分词 - 默认索引 - 可以开启存储 - 文本类型 Field,默认分词并且索引,是否需要存储由 Store.YES 或 Store.No 决定。

    查询索引

    com.renda.LuceneDemoApplicationTests

    代码语言:javascript
    复制
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class LuceneDemoApplicationTests {

    @Autowired
    private JobInfoService jobInfoService;
    
    ...
    
    @Test
    public void query() throws Exception {
        // 1.指定索引文件的存储位置,索引具体的表现形式就是一组有规则的文件
        Directory directory = FSDirectory.open(new File(&#34;E:/class/index&#34;));
        // 2.IndexReader 对象
        IndexReader indexReader = DirectoryReader.open(directory);
        // 3.创建查询对象,IndexSearcher
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);
        // 使用 term, 查询公司名称中包含&#34;北京&#34;的所有的文档对象
        Query query = new TermQuery(new Term(&#34;companyName&#34;, &#34;北京&#34;));
        TopDocs topDocs = indexSearcher.search(query, );
        // 获得符合查询条件的文档数
        int totalHits = topDocs.totalHits;
        System.out.println(&#34;符合条件的文档数:&#34; + totalHits);
        // 获得命中的文档  ScoreDoc封装了文档id信息
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 文档 id
            int docId = scoreDoc.doc;
            // 通过文档 id 获得文档对象
            Document doc = indexSearcher.doc(docId);
            System.out.println(&#34;id:&#34; + doc.get(&#34;id&#34;));
            System.out.println(&#34;companyName:&#34; + doc.get(&#34;companyName&#34;));
            System.out.println(&#34;companyAddr:&#34; + doc.get(&#34;companyAddr&#34;));
            System.out.println(&#34;companyInfo:&#34; + doc.get(&#34;companyInfo&#34;));
            System.out.println(&#34;jobName:&#34; + doc.get(&#34;jobName&#34;));
            System.out.println(&#34;jobInfo:&#34; + doc.get(&#34;jobInfo&#34;));
            System.out.println(&#34;*******************************************&#34;);
        }
        // 资源释放
        indexReader.close();
    }
    

    }

    这里需要使用可以合理分词的分词器,否则中文会一个字一个字地分词,其中最有名的中文分词器是 IKAnalyzer 分词器。

    中文分词器的使用

    第一步:导入依赖。

    代码语言:javascript
    复制
    <!-- IK 中文分词器 -->
    <dependency>
    <groupId>com.janeluo</groupId>
    <artifactId>ikanalyzer</artifactId>
    <version>2012_u6</version>
    </dependency>

    第二步:根据需求,可以添加配置文件放入到 resources 文件夹中(可以在配置文件额外加扩展词典和停止词典)。

    代码语言:javascript
    复制
    IKAnalyzer.cfg.xml
    stopword.dic

    第三步:创建索引时使用 IKanalyzer。

    代码语言:javascript
    复制
    Analyzer analyzer = new IKAnalyzer();

    考虑一个问题:一个大型网站中的索引数据会很庞大的,所以使用 Lucene 这种原生的写代码的方式就不合适了,所以需要借助一个成熟的项目或软件来实现,目前比较有名是 solr 和 ElasticSearch。

    Elastic Search 介绍和安装

    Elasticsearch 是一个需要安装配置的软件。

    ELK 技术栈说明:

    Elastic 有一条完整的产品线 ELK - Elasticsearch、Logstash、Kibana,前面说的三个就是常说的 ELK 技术栈(开源实时日志分析平台)。

    代码语言:javascript
    复制
    MySQL ---Logstash数据同步---> ElasticSearch索引库

    MySQL ---- MySQL可视化软件

    ElasticSearch索引库 ---- Kibana可视化软件

    Logstash 的作用就是一个数据收集器,将各种格式各种渠道的数据通过它收集解析之后格式化输出到 Elastic Search ,最后再由 Kibana 提供的比较友好的 Web 界面进行汇总、分析、搜索。

    ELK 内部实际就是个管道结构,数据从 Logstash 到 Elastic Search 再到 Kibana 做可视化展示。这三个组件各自也可以单独使用,比如 Logstash 不仅可以将数据输出到 Elastic Search ,也可以到数据库、缓存等。

    简介

    Elastic

    Elastic 官网:https://www.elastic.co/cn/

    Elastic 有一条完整的产品线:Elasticsearch、Logstash、Kibana 等,前面说的三个就是常说的 ELK 技术栈。

    Elasticsearch

    Elasticsearch 官网:https://www.elastic.co/cn/products/elasticsearch

    功能:

    • 分布式的搜索引擎 - 百度、Google、站内搜索。
    • 全文检索 = 提供模糊搜索等自动度很高的查询方式,并进行相关性排名,高亮等功能。
    • 数据分析引擎(分组聚合)- 电商网站一周内手机销量 Top 10。
    • 对海量数据进行近乎实时处理 - 水平扩展,每秒钟可处理海量事件,同时能够自动管理索引和查询在集群中的分布方式,以实现极其流畅的操作。

    Elastic Search 具备以下特点:

    • 高速、扩展性、最相关的搜索结果。
    • 分布式 - 节点对外表现对等,每个节点都可以作为入门,加入节点自动负载均衡。
    • JSON - 输入输出格式是 JSON。
    • Restful 风格,一切 API 都遵循 Rest 原则,容易上手。
    • 近实时搜索,数据更新在 Elasticsearch 中几乎是完全同步的,数据检索近乎实时。
    • 安装方便 - 没有其它依赖,下载后安装很方便,简单修改几个参数就可以搭建集群。
    • 支持超大数据:可以扩展到 PB 级别的结构化和非结构化数据。
    版本

    目前 Elasticsearch 最新的版本是 7.x,企业内目前用的比较多是 6.x,以 6.2.4 为例子,需要 JDK 1.8 及以上。

    安装和配置

    为了快速看到效果可以直接在本地 window 下安装 Elasticsearch,实际开发是在 Linux 中使用,但使用方式是一样的;环境要求:JDK 8 及以上版本。

    第一步:解压安装包

    把压缩包 elasticsearch-6.2.4.zip 放到一个没有中文没有空格的位置,解压即可。

    代码语言:javascript
    复制
    \bin            命令,启动,关闭
    \config 配置文件
    \lib ES 的 jar 包依赖
    \logs 日志
    \modules ES 工作所依赖的一些组件,启动时加载
    \plugins 放置一些第三方插件,如 IK 分词器
    LICENSE.txt
    NOTICE.txt
    README.textile
    第二步:修改配置文件

    1、修改索引数据和日志数据存储的路径 \config\elasticsearch.yml

    第 33 行和 37 行,修改完记得把注释打开:

    代码语言:javascript
    复制
    # ----------------------------------- Paths ------------------------------------

    Path to directory where to store the data (separate multiple locations by comma):

    path.data: e:\class\es\data

    Path to log files:

    path.logs: e:\class\es\logs

    第三步:启动

    进入 bin 目录中直接双击 elasticsearch.bat

    如果启动失败,需要修改虚拟机内存的大小,默认为 1 G,可以调小。

    在 config 目录下找到 jvm.options 文件 ,修改后如下:

    代码语言:javascript
    复制
    # Xms represents the initial size of total heap space

    Xmx represents the maximum size of total heap space

    -Xms256m
    -Xmx256m

    • Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。
    • Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出 OutOfMemory 异常。

    访问

    启动后台部分输出如下:

    代码语言:javascript
    复制
    [2020-11-08T21:52:33,482][INFO ][o.e.n.Node               ] [G9aQRl_] starting ...
    [2020-11-08T21:52:34,307][INFO ][o.e.t.TransportService ] [G9aQRl_] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300}, {[::1]:9300}
    [2020-11-08T21:52:37,363][INFO ][o.e.c.s.MasterService ] [G9aQRl_] zen-disco-elected-as-master ([0] nodes joined), reason: new_master {G9aQRl_}{G9aQRl_qR2KeysskhZY0xQ}{87fblLs1Rci_HVjZVzVlOQ}{127.0.0.1}{127.0.0.1:9300}
    [2020-11-08T21:52:37,363][INFO ][o.e.c.s.ClusterApplierService] [G9aQRl_] new_master {G9aQRl_}{G9aQRl_qR2KeysskhZY0xQ}{87fblLs1Rci_HVjZVzVlOQ}{127.0.0.1}{127.0.0.1:9300}, reason: apply cluster state (from master [master {G9aQRl_}{G9aQRl_qR2KeysskhZY0xQ}{87fblLs1Rci_HVjZVzVlOQ}{127.0.0.1}{127.0.0.1:9300} committed version [1] source [zen-disco-elected-as-master ([0] nodes joined)]])
    [2020-11-08T21:52:37,426][INFO ][o.e.g.GatewayService ] [G9aQRl_] recovered [0] indices into cluster_state
    [2020-11-08T21:52:37,762][INFO ][o.e.h.n.Netty4HttpServerTransport] [G9aQRl_] publish_address {127.0.0.1:9200}, bound_addresses {127.0.0.1:9200}, {[::1]:9200}
    [2020-11-08T21:52:37,762][INFO ][o.e.n.Node ] [G9aQRl_] started

    可以看到绑定了两个端口:

    9300 - 集群节点间通讯接口,接收 tcp 协议。

    9200 - 客户端访问接口,接收 Http 协议。

    在浏览器中访问:http://127.0.0.1

    代码语言:javascript
    复制
    {
    "name" : "G9aQRl_",
    "cluster_name" : "elasticsearch",
    "cluster_uuid" : "ezpbwfUtTqubgqUGPvJWAg",
    "version" : {
    "number" : "6.2.4",
    "build_hash" : "ccec39f",
    "build_date" : "2018-04-12T20:37:28.497551Z",
    "build_snapshot" : false,
    "lucene_version" : "7.2.1",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
    },
    "tagline" : "You Know, for Search"
    }

    安装 kibana

    什么是 Kibana

    Kibana 是一个基于 Node.js 的 Elasticsearch 索引库数据统计工具,可以利用 Elasticsearch 的聚合功能,生成各种图表,如柱形图,线状图,饼;而且还提供了操作 Elasticsearch 索引数据的控制台,并且提供了一定的 API 提示。

    安装

    因为 Kibana 依赖于 node,需要在 windows 下先安装 Node.js,直接双击运行 node.js 的安装包:node-v10.15.0-x64.msi

    安装成功后在任意 DOS 窗口输入:node -v,即可查看到 node 版本。

    然后安装 kibana,版本与 Elasticsearch 保持一致,也是 6.2.4。

    直接解压安装包即可:kibana-6.2.4-windows-x86_64.zip

    配置运行
    配置

    进入安装目录下的 config 目录,修改 kibana.yml 文件的第 21 行(注释放开)。

    确保 elasticsearch 服务器的地址如下:

    代码语言:javascript
    复制
    elasticsearch.url: "http://localhost:9200"
    运行

    进入安装目录下的 bin 目录,双击 kibana.bat 启动。

    代码语言:javascript
    复制
      log   [14:05:55.974] [info][listening] Server running at http://localhost:5601
    log [14:05:56.031] [info][status][plugin:elasticsearch@6.2.4] Status changed from yellow to green - Ready

    可以看到 Kibana 的监听端口是 5601,于是直接访问:http://127.0.0.1

    控制台

    成功访问 Kibana 后,选择左侧的 DevTools 菜单,即可进入控制台页面。

    在页面右侧,就可以输入请求,访问 Elasticsearch 了。

    编写 Restful 请求;这里类似于 POST 或者浏览器,可以向 ES 发送请求,但是不用写 ES 的地址,因为在 config/kibana.yml 文件中已经定义了 ES 的地址,剩下的只需要填写对应的 uri 和参数即可:

    代码语言:javascript
    复制
    GET _search
    {
    "query": {
    "match_all": {}
    }
    }

    点击按钮执行请求,返回执行结果,显示的格式是 JSON,请求格式和响应格式一样都是 JSON 格式:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": []
    }
    }

    安装 ik 分词器

    Lucene 的 IK 分词器早在 2012 年已经没有维护了,现在要使用的是在其基础上维护升级的版本,并且开发为 Elasticsearch 的集成插件了,与 Elasticsearch 一起维护升级,版本也保持一致。

    https://github.com/medcl/elasticsearch-analysis-ik

    安装

    1、 解压 elasticsearch-analysis-ik-6.2.4.zip 后,将解压后的文件夹拷贝到 elasticsearch-6.2.4\plugins 下,并重命名文件夹为 ik

    2、重新启动 ElasticSearch,即可加载 IK 分词器。

    测试

    在 kibana 控制台输入下面的请求:

    代码语言:javascript
    复制
    GET /_analyze
    {
    "analyzer": "ik_max_word",
    "text": "我是中国人"
    }

    运行得到结果:

    代码语言:javascript
    复制
    {
    "tokens": [
    {
    "token": "我",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_CHAR",
    "position":
    },
    {
    "token": "是",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_CHAR",
    "position":
    },
    {
    "token": "中国人",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_WORD",
    "position":
    },
    {
    "token": "中国",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_WORD",
    "position":
    },
    {
    "token": "国人",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_WORD",
    "position":
    }
    ]
    }

    使用 smart 分词:

    代码语言:javascript
    复制
    GET /_analyze
    {
    "analyzer": "ik_smart",
    "text": "我是中国人"
    }

    结果:

    代码语言:javascript
    复制
    {
    "tokens": [
    {
    "token": "我",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_CHAR",
    "position":
    },
    {
    "token": "是",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_CHAR",
    "position":
    },
    {
    "token": "中国人",
    "start_offset": ,
    "end_offset": ,
    "type": "CN_WORD",
    "position":
    }
    ]
    }

    安装 Head 插件

    简介

    elasticsearch-head 是一个界面化的集群操作和管理工具,可以对集群进行傻瓜式操作。可以通过插件把它集成到 es(首选方式),也可以安装成一个独立 webapp。

    es-head 主要有三个方面的操作:

    1. 显示集群的拓扑,并且能够执行索引和节点级别操作。
    2. 搜索接口能够查询集群中原始 JSON 或表格格式的检索数据。
    3. 能够快速访问并显示集群的状态。

    官方的文档:https://github.com/mobz/elasticsearch-head

    安装

    基于谷歌浏览器:

    1)直接下载压缩包 elasticsearch-head.7z

    2)解压。

    3)在谷歌浏览器中点击“加载已解压的压缩程序”,找到 elasticsearch-head 文件夹,点击打开即可进行安装。

    使用 kibana 对索引库操作

    基本概念

    节点、集群、分片及副本

    1、节点 (node)

    一个节点是一个 Elasticsearch 的实例。

    在服务器上启动 Elasticsearch 之后,就拥有了一个节点。如果在另一台服务器上启动 Elasticsearch,这就是另一个节点。甚至可以通过启动多个 Elasticsearch 进程,在同一台服务器上拥有多个节点。

    2、集群(cluster)

    多个协同工作的 Elasticsearch 节点的集合被称为集群。

    在多节点的集群上,同样的数据可以在多台服务器上传播。这有助于性能。这同样有助于稳定性,如果每个分片至少有一个副本分片,那么任何一个节点宕机后,Elasticsearch 依然可以进行服务,返回所有数据。

    但是它也有缺点:必须确定节点之间能够足够快速地通信,并且不会产生脑裂效应(集群的 2 个部分不能彼此交流,都认为对方宕机了)。

    3、分片 (shard)

    索引可能会存储大量数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文档的单个索引占用了 1 TB 的磁盘空间,可能不适合单个节点的磁盘,或者可能太慢而无法单独满足来自单个节点的搜索请求。

    为了解决此问题,Elasticsearch 提供了将索引细分为多个碎片的功能。创建索引时,只需定义所需的分片数量即可。每个分片本身就是一个功能齐全且独立的“索引”,可以托管在群集中的任何节点上。

    分片很重要,主要有两个原因:

    • 它允许水平分割 / 缩放内容量。
    • 它允许跨碎片(可能在多个节点上)分布和并行化操作,从而提高性能 / 吞吐量。

    分片如何分布以及其文档如何聚合回到搜索请求中的机制完全由 Elasticsearch 管理,并且对用户是透明的。

    在随时可能发生故障的网络 / 云环境中,非常有用,强烈建议使用故障转移机制,以防碎片 / 节点因某种原因脱机或消失。为此,Elasticsearch 允许将索引分片的一个或多个副本制作为所谓的副本分片(简称副本)。

    4、副本(replica)

    分片处理允许用户推送超过单机容量的数据至 Elasticsearch 集群。副本则解决了访问压力过大时单机无法处理所有请求的问题。

    分片可以是主分片,也可以是副本分片,其中副本分片是主分片的完整副本。副本分片用于搜索,或者是在原有的主分片丢失后成为新的主分片。

    注意:可以在任何时候改变每个分片的副本分片的数量,因为副本分片总是可以被创建和移除的。这并不适用于索引划分为主分片的数量,在创建索引之前,必须决定主分片的数量。过少的分片将限制可扩展性,但是过多的分片会影响性能。默认设置的5份是一个不错的开始。

    文档、类型、索引及映射

    1、文档 (document)

    Elasticsearch 是面向文档的,这意味着索引和搜索数据的最小单位是文档。

    在 Elasticsearch 中文档有几个重要的属性:

    • 它是自我包含的,一篇文档同时包含字段和它们的取值。
    • 它可以是层次的。文档中还包含新的文档,字段还可以包含其他字段和取值。例如 “location” 字段可以同时包含 “city” 和 “street“ 两个字段。
    • 它拥有灵活的结构。文档不依赖于预先定义的模式。并非所有的文档都需要拥有相同的字段,它们不受限于同一个模式。

    2、类型 (type)

    类型是文档的逻辑容器,类似于表格是行的容器。在不同的类型中,最好放入不同结构的文档。例如,可以用一个类型定义聚会时的分组,而另一个类型定义人们参加的活动。

    3、索引 (index)

    索引是映射类型的容器。一个 Elasticsearch 索引是独立的大量的文档集合。每个索引存储在磁盘上的同组文件中,索引存储了所有映射类型的字段,还有一些设置。

    4、映射(mapping)

    所有文档在写入索引前都将被分析,用户可以设置一些参数,决定如何将输入文本分割为词条,哪些词条应该被过滤掉,或哪些附加处理有必要被调用(比如移除 HTML 标签)。这就是映射扮演的角色:存储分析链所需的所有信息。

    Elasticsearch 也是基于 Lucene 的全文检索库,本质也是存储数据,很多概念与 MySQL 类似的。

    对比关系:

    代码语言:javascript
    复制
    索引库 indices ----- Database 数据库
    类型 type ---- Table 数据表
    文档 Document ----- Row 行
    域字段 Field ----- Columns 列

    映射配置 mappings ----- 每个列的约束(类型、长度)

    详细说明:

    • 类型(type)是模拟 mysql 中的 table 概念,一个索引库下可以有不同类型的索引(目前 6.X 以后的版本只能有一个类型),类似数据库中的表概念。数据库表中有表结构,也就是表中每个字段的约束信息;索引库的类型中对应表结构的叫做映射 (mapping) ,用来定义每个字段的约束。
    • 文档(document)存入索引库原始的数据;比如每一条商品信息,就是一个文档。
    • 字段(field)文档中的属性。
    • 映射配置(mappings)字段的数据类型、属性、是否索引、是否存储等特性。

    创建索引库

    语法

    Elasticsearch 采用 Rest 风格 API,因此其 API 就是一次 http 请求,可以用任何工具发起 http 请求。

    创建索引的请求格式:

    • 请求方式:PUT
    • 请求路径:/索引库名
    • 请求参数:json 格式
    代码语言:javascript
    复制
    {
    "settings": {
    "属性名": "属性值"
    }
    }

    settings:就是索引库设置,其中可以定义索引库的各种属性,目前可以不设置,都走默认。

    使用 kibana 创建

    Kibana 的控制台,可以对 http 请求进行简化,示例:

    代码语言:javascript
    复制
    PUT /renda

    响应数据:

    代码语言:javascript
    复制
    {
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "renda"
    }

    相当于是省去了 elasticsearch 的服务器地址,而且还有语法提示。

    查看索引库

    Get 请求可以查看索引信息:

    代码语言:javascript
    复制
    GET /renda

    响应数据:

    代码语言:javascript
    复制
    {
    "renda": {
    "aliases": {},
    "mappings": {},
    "settings": {
    "index": {
    "creation_date": "1604847321532",
    "number_of_shards": "5",
    "number_of_replicas": "1",
    "uuid": "ZlK7XTn6S8OIOAdhPCvnxA",
    "version": {
    "created": "6020499"
    },
    "provided_name": "renda"
    }
    }
    }
    }

    删除索引库

    删除索引使用 DELETE请 求:

    代码语言:javascript
    复制
    DELETE /renda

    响应信息:

    代码语言:javascript
    复制
    {
    "acknowledged": true
    }

    再次查看 renda:

    代码语言:javascript
    复制
    GET /renda

    响应信息:

    代码语言:javascript
    复制
    {
    "error": {
    "root_cause": [
    {
    "type": "index_not_found_exception",
    "reason": "no such index",
    "resource.type": "index_or_alias",
    "resource.id": "renda",
    "index_uuid": "na",
    "index": "renda"
    }
    ],
    "type": "index_not_found_exception",
    "reason": "no such index",
    "resource.type": "index_or_alias",
    "resource.id": "renda",
    "index_uuid": "na",
    "index": "renda"
    },
    "status":
    }

    使用 kibana 对类型及映射操作

    有了索引库 ,等于有了数据库中的 database。接下来就需要索引库中的类型了,也就是数据库中的表。创建数据库表需要设置字段约束,索引库也一样,在创建索引库的类型时,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做字段映射 (mapping)。

    注意:Elasticsearch7.x 取消了索引 type 类型的设置,不允许指定类型,默认为 _doc;但字段仍然是有类型的,还需要设置字段的约束信息,叫做字段映射(mapping)。

    字段的约束在 Lucene 中见到过,包括到不限于:

    • 字段的数据类型
    • 是否要存储
    • 是否要索引
    • 是否分词
    • 分词器是什么

    创建字段映射

    请求方式依然是 PUT:

    代码语言:javascript
    复制
    PUT /索引库名/_mapping/typeName
    {
    "properties": {
    "字段名": {
    "type": "类型",
    "index": true,
    "store": true,
    "analyzer": "分词器"
    }
    }
    }

    typeName 类型名称:就是前面的 type 的概念,类似于数据库中的表。

    字段名:任意填写,下面指定许多属性,例如:

    • type - 类型,可以是 text、keyword、long、short、date、integer、object 等。
    • index - 是否索引,默认为 true。
    • store - 是否存储,默认为 false。
    • analyzer - 分词器,这里的 ik_max_word 即使用 ik 分词器。

    发起请求:

    代码语言:javascript
    复制
    PUT renda/_mapping/goods
    {
    "properties": {
    "title": {
    "type": "text",
    "store": true,
    "analyzer": "ik_max_word"
    },
    "images": {
    "type": "keyword",
    "store": true,
    "index": false
    },
    "price": {
    "type": "float"
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "acknowledged": true
    }

    上述案例中,就给 renda 这个索引库添加了一个名为 goods 的类型,并且在类型中设置了 3 个字段:

    • title - 商品标题
    • images - 商品图片
    • price - 商品价格

    并且给这些字段设置了一些属性,至于这些属性对应的含义,后面会详细介绍。

    查看映射关系

    查看某个索引库中的所有类型的映射语法:

    代码语言:javascript
    复制
    GET /索引库名/_mapping

    如果要查看某个类型映射,可以再路径后面跟上类型名称:

    代码语言:javascript
    复制
    GET /索引库名/_mapping/类型名

    示例请求:

    代码语言:javascript
    复制
    GET /renda/_mapping/goods

    响应:

    代码语言:javascript
    复制
    {
    "renda": {
    "mappings": {
    "goods": {
    "properties": {
    "images": {
    "type": "keyword",
    "index": false,
    "store": true
    },
    "price": {
    "type": "float"
    },
    "title": {
    "type": "text",
    "store": true,
    "analyzer": "ik_max_word"
    }
    }
    }
    }
    }
    }

    映射属性详解

    1)type

    Elasticsearch 中支持的数据类型非常丰富:

    代码语言:javascript
    复制
    核心类型:
    字符串类型 - text,keyword - 结构化搜索,全文文本搜索、聚合、排序等
    整数类型 - integer,long,short,byte - 字段的长度越短,索引和搜索的效率越高
    浮点类型 - double,float,half_float,scaled_float
    逻辑类型 - boolean
    日期类型 - date
    范围类型 - range
    二进制类型 - binary
    该 binary 类型接受二进制值作为 Base64 编码的字符串。
    该字段默认情况下不存储(store),并且不可搜索。

    复合类型:
    数组类型 - array
    对象类型 - object - 用于单个 JSON 对象
    嵌套类型 - nested - 用于 JSON 对象数组

    地理类型:
    地理坐标类型 - geo_point - 纬度/经度积分
    地理地图 - geo_shape - 用于多边形等复杂形状

    特殊类型:
    IP 类型 - ip - 用于 IPv4 和 IPv6 地址
    范围类型 - completion - 提供自动完成建议
    令牌计数类型 - token_count - 计算字符串中令牌的数量

    String 类型分两种:

    • text:使用文本数据类型的字段,它们会被分词,文本字段不用于排序,很少用于聚合,如文章标题、正文。
    • keyword:关键字数据类型,用于索引结构化内容的字段,不会被分词,必须完整匹配的内容,如邮箱,身份证号;支持聚合。

    这两种类型都是比较常用的,但有的时候,对于一个字符串字段,可能希望两种都支持,此时,可以利用其多字段特性。

    代码语言:javascript
    复制
    "properties": {
    "title":{
    "type": "text",
    "analyzer": "ik_max_word",
    "fields": {
    "sort": {
    "type": "keyword"
    }
    },
    "index": true
    }
    }

    Numerical:数值类型,分两类

    • 基本数据类型:longintergershortbytedoublefloathalf_float
    • double 双精度 64 位
    • float 单精度 32 位
    • half_float 半精度 16 位
    • 浮点数的高精度类型:scaled_float。带有缩放因子的缩放类型浮点数,依靠一个 long 数字类型通过一个固定的 (double 类型) 缩放因数进行缩放;需要指定一个精度因子,比如 10 或 100。Elasticsearch 会把真实值乘以这个因子后存储,取出时再还原。

    Date:日期类型,Elasticsearch 可以对日期格式化为字符串存储,但是建议存储为毫秒值,存储为 long,节省空间。

    Array:数组类型

    • 进行匹配时,任意一个元素满足,都认为满足。
    • 排序时,如果升序则用数组中的最小值来排序,如果降序则用数组中的最大值来排序。
    代码语言:javascript
    复制
    字符串数组:["one", "two"]
    整数数组:[1, 2]
    数组的数组:[1, [2, 3]],等价于 [1,2,3]
    对象数组:[{"name": "Mary", "age": 12}, {"name": "John", "age": 10}]

    Object:对象,JSON 文档本质上是分层的 - 文档包含内部对象,内部对象本身还包含内部对象。

    代码语言:javascript
    复制
    {
    "region": "US",
    "manager.age": ,
    "manager.name ": "John Smith"
    }

    索引方法如下:

    {
    "mappings": {
    "properties": {
    "region": { "type": "keyword" },
    "manager": {
    "properties": {
    "age": { "type": "integer" },
    "name": { "type": "text" }
    }
    }
    }
    }
    }

    如果存储到索引库的是对象类型,例如上面的 manager,会把 manager 编成两个字段:manager.namemanager.age

    IP 地址:

    代码语言:javascript
    复制
    PUT my_index
    {
    "mappings": {
    "_doc": {
    "properties": {
    "ip_addr": {
    "type": "ip"
    }
    }
    }
    }
    }

    PUT my_index/_doc/
    {
    "ip_addr": "192.168.1.1"
    }

    GET my_index/_search
    {
    "query": {
    "term": {
    "ip_addr": "192.168.0.0/16"
    }
    }
    }

    2)index

    index 影响字段的索引情况。

    • true:字段会被索引,则可以用来进行搜索过滤。默认值就是 true,只有当某一个字段的 index 值设置为 true 时,检索 ES 才可以作为条件去检索。
    • false:字段不会被索引,不能用来搜索。

    index 的默认值就是 true,也就是说不进行任何配置,所有字段都会被索引。

    但是有些字段是不希望被索引的,比如商品的图片信息 URL,就需要手动设置 index 为 false。

    3)store

    是否将数据进行额外存储。

    在 lucene 中,如果一个字段的 store 设置为 false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。

    但是在 Elasticsearch 中,即便 store 设置为 false,也可以搜索到结果。

    原因是 Elasticsearch 在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做 _source 的属性中。而且我们可以通过过滤 _source 来选择哪些要显示,哪些不显示。

    如果设置 store 为 true,就会在 _source 以外额外存储一份数据,多余,因此一般都会将 store 设置为 false,事实上,store 的默认值就是 false。

    在某些情况下,这对 store 某个领域可能是有意义的。例如,如果文档包含一个 title ,一个 date 和一个非常大的 content 字段,则可能只想检索 the title 和 the date 而不必从一个大 _source 字段中提取这些字段:

    代码语言:javascript
    复制
    PUT my_index
    {
    "mappings": {
    "_doc": {
    "properties": {
    "title": {
    "type": "text",
    "store": true
    },
    "date": {
    "type": "date",
    "store": true
    },
    "content": {
    "type": "text"
    }
    }
    }
    }
    }
    4)boost

    网站权重:是指搜索引擎给网站(包括网页)赋予一定的权威值,对网站(含网页)权威的评估评价。一个网站权重越高,在搜索引擎所占的份量越大,在搜索引擎排名就越好。提高网站权重,不但利于网站(包括网页)在搜索引擎的排名更靠前,还能提高整站的流量,提高网站信任度。所以提高网站的权重具有相当重要的意义。权重即网站在 SEO 中的重要性,权威性。Page Strength:1、权重不等于排名;2、权重对排名有着非常大的影响;3、整站权重的提高有利于内页的排名。

    权重,新增数据时,可以指定该数据的权重,权重越高,得分越高,排名越靠前。

    代码语言:javascript
    复制
    PUT my_index
    {
    "mappings": {
    "_doc": {
    "properties": {
    "title": {
    "type": "text",
    "boost":
    },
    "content": {
    "type": "text"
    }
    }
    }
    }
    }

    title 字段上的匹配项的权重是字段上的匹配项的权重的两倍 content,默认 boost 值为 1.0

    提升仅适用于 Term 查询;不提升 prefix,range 和模糊查询。

    一次创建索引库和类型

    代码语言:javascript
    复制
    第一步:
    PUT /renda

    第二步:
    PUT renda/_mapping/goods
    {
    "properties": {
    "title": {
    "type": "text",
    "analyzer": "ik_max_word"
    },
    "images": {
    "type": "keyword",
    "index": "false"
    },
    "price": {
    "type": "float"
    }
    }
    }

    刚才的案例中是把创建索引库和类型分开来做,其实也可以在创建索引库的同时,直接制定索引库中的类型,基本语法:

    代码语言:javascript
    复制
    put /索引库名
    {
    "settings":{
    "索引库属性名":"索引库属性值"
    },
    "mappings":{
    "类型名":{
    "properties":{
    "字段名":{
    "映射属性名":"映射属性值"
    }
    }
    }
    }
    }

    示例:

    代码语言:javascript
    复制
    PUT /renda
    {
    "settings": {},
    "mappings": {
    "goods": {
    "properties": {
    "title": {
    "type": "text",
    "analyzer": "ik_max_word"
    }
    }
    }
    }
    }

    结果:

    代码语言:javascript
    复制
    {
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "renda2"
    }

    使用 Kibana 对文档操作

    文档,即索引库中某个类型下的数据,会根据规则创建索引,将来用来搜索。可以类比做数据库中的每一行数据。

    新增文档

    新增并随机生成 id。

    通过 POST 请求,可以向一个已经存在的索引库中添加文档数据。

    语法:

    代码语言:javascript
    复制
    POST /索引库名/类型名
    {
    "key":"value"
    }

    示例:

    代码语言:javascript
    复制
    POST /renda/goods/
    {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price": 2699.00
    }

    响应:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_version": ,
    "result": "created",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    可以看到结果显示为:created,是创建成功了。

    另外,需要注意的是,在响应结果中有个 _id 字段,这个就是这条文档数据的唯一标示 ,以后的增删改查都依赖这个 id 作为唯一标示。

    可以看到 id 的值为:gPeQqHUB-UTJAEEuqOm9,这里新增时没有指定 id,所以是 ES 随机生成的 id

    查看文档

    根据 Rest 风格,新增是 post,查询应该是 get,不过查询一般都需要条件,这里把刚刚生成数据的 id 带上。

    通过 kibana 查看数据:

    代码语言:javascript
    复制
    GET /renda/goods/gPeQqHUB-UTJAEEuqOm

    查看结果:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_version": ,
    "found": true,
    "_source": {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    • _source:源文档信息,所有的数据都在里面。
    • _id:这条文档的唯一标示。
    • 自动生成的 id,长度为 20 个字符,URL 安全,base64 编码,GUID 全局唯一标识符,分布式系统并行生成时不可能会发生冲突。
    • 在实际开发中不建议使用 ES 生成的 ID,太长且为字符串类型,检索时效率低。建议:将数据表中唯一的 ID,作为 ES 的文档 ID。

    新增文档并自定义 id

    如果想要自己新增的时候指定 id,可以这么做:

    代码语言:javascript
    复制
    POST /索引库名/类型/id值
    {
    ...
    }

    示例:

    代码语言:javascript
    复制
    POST /renda/goods/
    {
    "title": "白米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price": 2699.00
    }

    得到的数据:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "2",
    "_version": ,
    "result": "created",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    修改数据

    PUT - 修改文档。

    POST - 新增文档。

    把刚才新增的请求方式改为 PUT,就是修改了,不过修改必须指定id。

    • id 对应文档存在,则修改。
    • id 对应文档不存在,则新增。

    比如,使用 id 为 3,不存在,则应该是新增:

    代码语言:javascript
    复制
    PUT /renda/goods/
    {
    "title": "黑米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price": 3999.00,
    "stock": ,
    "saleable": true
    }

    结果:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_version": ,
    "result": "created",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    可以看到是 created,是新增。

    再次执行刚才的请求,不过把数据改一下:

    代码语言:javascript
    复制
    PUT /renda/goods/
    {
    "title": "X米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price": 4999.00,
    "stock": ,
    "saleable": true
    }

    结果:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_version": ,
    "result": "updated",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    可以看到结果是:updated,显然是更新数据。

    删除数据

    删除使用 DELETE 请求,同样,需要根据 id 进行删除。

    语法:

    代码语言:javascript
    复制
    DELETE /索引库名/类型名/id值

    示例:

    代码语言:javascript
    复制
    DELETE /renda/goods/

    结果:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_version": ,
    "result": "deleted",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    智能判断

    Elasticsearch 非常智能,不需要给索引库设置任何 mapping 映射,它也可以根据输入的数据来判断类型,动态添加数据映射。

    测试一下:

    代码语言:javascript
    复制
    POST /renda/goods/
    {
    "title":"超大米手机",
    "images":"http://image.renda.com/12479122.jpg",
    "price":3299.00,
    "stock": ,
    "saleable":true,
    "subTitle":"大米"
    }

    额外添加了 stock 库存,saleable 是否上架,subtitle 副标题,3 个字段。

    响应结果:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_version": ,
    "result": "created",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    执行 GET renda 查看索引库的映射关系:

    代码语言:javascript
    复制
    {
    "renda": {
    "aliases": {},
    "mappings": {
    "goods": {
    "properties": {
    "images": {
    "type": "keyword",
    "index": false,
    "store": true
    },
    "price": {
    "type": "float"
    },
    "saleable": {
    "type": "boolean"
    },
    "stock": {
    "type": "long"
    },
    "subTitle": {
    "type": "text",
    "fields": {
    "keyword": {
    "type": "keyword",
    "ignore_above":
    }
    }
    },
    "title": {
    "type": "text",
    "store": true,
    "analyzer": "ik_max_word"
    }
    }
    }
    },
    "settings": {
    "index": {
    "creation_date": "1604847840423",
    "number_of_shards": "5",
    "number_of_replicas": "1",
    "uuid": "w-Q9ingUTjm2MsfaQ0730g",
    "version": {
    "created": "6020499"
    },
    "provided_name": "renda"
    }
    }
    }
    }

    stocksaleablesubtitle 都被成功映射了。

    如果没有事先定义对应的 Mapping,那么就会根据文档中的字段数据推断类型并创建;而且 ES 会在匹配类型中选择范围最大的作为新建的 Mapping 字段的类型。

    subtitle 是 String 类型数据,ES 无法智能判断是否应该分词,它就会存入两个字段,从而满足分词和不分词这两种情况。例如:

    • subtitle - text 类型
    • subtitle.keyword - keyword 类型
    • keyword 的 ignore_above 为 256,就是说如果此字段值超过了 256 字符,此时将不被索引

    这种智能映射,底层原理是动态模板映射,如果想修改这种智能映射的规则,其实只要修改动态模板即可。

    动态映射模板

    动态模板的语法:

    1)模板名称,随便起。

    2)匹配条件,凡是符合条件的未定义字段,都会按照这个规则来映射。

    3)映射规则,匹配成功后的映射规则。

    可以把所有未映射的 String 类型数据自动映射为 keyword 类型:

    代码语言:javascript
    复制
    PUT renda
    {
    "mappings": {
    "goods": {
    "properties": {
    "title": {
    "type": "text",
    "analyzer": "ik_max_word",
    "index": true,
    "store": true
    }
    },
    "dynamic_templates": [
    {
    "strings": {
    "match_mapping_type": "string",
    "mapping": {
    "type": "keyword",
    "index": false,
    "store": true
    }
    }
    }
    ]
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "renda3"
    }

    在这个案例中,做了两个映射配置:

    • title 字段 - 统一映射为 text 类型,并制定分词器。
    • 其它字段 - 只要是 string 类型,统一都处理为 keyword 类型。

    这样,未知的 string 类型数据就不会被映射为 text 和 keyword 并存,而是统一以 keyword 来处理。

    新增一个数据:

    代码语言:javascript
    复制
    POST /renda/goods/
    {
    "title":"X米手机",
    "images":"http://image.renda.com/12479122.jpg",
    "price":3299.00
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "_index": "renda3",
    "_type": "goods",
    "_id": "1",
    "_version": ,
    "result": "created",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    查看 imagesprice 会被映射为什么类型:

    代码语言:javascript
    复制
    GET /renda3/_mapping

    结果:

    代码语言:javascript
    复制
    {
    "renda3": {
    "mappings": {
    "goods": {
    "dynamic_templates": [
    {
    "strings": {
    "match_mapping_type": "string",
    "mapping": {
    "index": false,
    "store": true,
    "type": "keyword"
    }
    }
    }
    ],
    "properties": {
    "images": {
    "type": "keyword",
    "index": false,
    "store": true
    },
    "price": {
    "type": "float"
    },
    "title": {
    "type": "text",
    "store": true,
    "analyzer": "ik_max_word"
    }
    }
    }
    }
    }
    }

    可以看到 images 被映射成了 keyword,而非之前的 text 和 keyword 并存,说明动态模板生效了。

    ElasticSearch 查询

    基本查询

    基本语法:

    代码语言:javascript
    复制
    GET /索引库名/_search
    {
    "query":{
    "查询类型":{
    "查询条件":"查询条件值"
    }
    }
    }

    这里的 query 代表一个查询对象,里面可以有不同的查询属性。

    查询类型:match_all、match、term、range 等等。

    查询条件:查询条件会根据类型的不同,写法也有差异。

    查询所有(match_all)

    示例:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query":{
    "match_all": {}
    }
    }

    query:代表查询对象。

    match_all:代表查询所有。

    结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "2",
    "_score": ,
    "_source": {
    "title": "白米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": ,
    "_source": {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_score": ,
    "_source": {
    "title": "超大米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price": ,
    "stock": ,
    "saleable": true,
    "subTitle": "大米"
    }
    }
    ]
    }
    }

    结果解析:

    代码语言:javascript
    复制
    took - 检索所耗费的时间,单位是毫秒。

    timed_out - 是否超时。

    _shards - 分片信息。

    hits - 命中结果,检索结果信息。
    total - 搜索到的总条数。
    max_score - 所有结果中文档得分的最高分。
    hits - 搜索结果的文档对象数组,每个元素是一条搜索到的文档信息。
    _index - 索引库。
    _type - 文档类型。
    _id - 文档 id。
    _score - 评分;索引库的一个概念;关联度。
    _source - 原始数据。

    文档得分:使用 ES 时,对于查询出的文档无疑会有文档相似度之别;而理想的排序是和查询条件相关性越高排序越靠前,而这个排序的依据就是 _score

    匹配查询(match)

    加入一条数据用于测试:

    代码语言:javascript
    复制
    PUT /renda/goods/
    {
    "title": "小米电视4A",
    "images": "http://image.renda.com/12479122.jpg",
    "price": 3899.00
    }

    索引库中有 3 部手机,1 台电视。

    match 类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "match": {
    "title": "小米电视"
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": 0.5753642,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_score": 0.5753642,
    "_source": {
    "title": "小米电视4A",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": 0.2876821,
    "_source": {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    ]
    }
    }

    在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是 or 的关系。

    某些情况下,需要更精确查找,即 and 关系。比如在电商平台精确搜索商品时,希望这个关系(查询条件切分词之后的关系)变成 and,可以这样做:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "match": {
    "title": {
    "query": "小米电视",
    "operator": "and"
    }
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": 0.5753642,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_score": 0.5753642,
    "_source": {
    "title": "小米电视4A",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    ]
    }
    }

    此时,只有同时包含小米和电视的词条才会被搜索到。

    词条匹配(term)

    term 查询被用于精确值匹配,这些精确值可能是数字、时间、布尔,或者那些未分词的字符串、keyword 类型的字符串。

    效果类似于:select * from tableName where colName='value';

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query":{
    "term":{
    "price": 2699.00
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "2",
    "_score": ,
    "_source": {
    "title": "白米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": ,
    "_source": {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    ]
    }
    }
    布尔组合(bool)

    bool 把各种其它查询通过 must - 与must_not - 非should - 或 的方式进行组合。

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query":{
    "bool":{
    "must": {
    "match": {
    "title": "小米"
    }
    },
    "must_not": {
    "match": {
    "title": "电视"
    }
    },
    "should": {
    "match": {
    "title": "手机"
    }
    }
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": 0.5753642,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": 0.5753642,
    "_source": {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    ]
    }
    }
    范围查询(range)

    range 查询找出那些落在指定区间内的数字或者时间。

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "range": {
    "price": {
    "gte": 3000.0,
    "lt": 4000.00
    }
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "3",
    "_score": ,
    "_source": {
    "title": "小米电视4A",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    ]
    }
    }

    range 查询允许以下字符:

    • gt - 大于
    • gte - 大于等于
    • lt - 小于
    • lte - 小于等于
    模糊查询(fuzzy)

    fuzzy 查询是 term 查询的模糊等价,很少直接使用它。

    新增一个商品:

    代码语言:javascript
    复制
    POST /renda/goods/
    {
    "title": "Apple手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price": 6899.00
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "5",
    "_version": ,
    "result": "created",
    "_shards": {
    "total": ,
    "successful": ,
    "failed":
    },
    "_seq_no": ,
    "_primary_term":
    }

    fuzzy 查询是 term 查询的模糊等价,它允许用户搜索词条与实际词条的拼写出现偏差,但是偏差的编辑距离不得超过 2

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "fuzzy": {
    "title": "applas"
    }
    }
    }

    上面的查询,也能查询到 apple 手机:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": 0.17260925,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "5",
    "_score": 0.17260925,
    "_source": {
    "title": "Apple手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    }
    }
    ]
    }
    }

    结果过滤

    默认情况下,Elasticsearch 在搜索的结果中,会把文档中保存在 _source 的所有字段都返回。

    如果只想获取其中的部分字段,可以添加 _source 的过滤。

    直接指定字段

    示例:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "_source": ["title","price"],
    "query": {
    "term": {
    "price":
    }
    }
    }

    返回的结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "2",
    "_score": ,
    "_source": {
    "price": ,
    "title": "白米手机"
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": ,
    "_source": {
    "price": ,
    "title": "小米手机"
    }
    }
    ]
    }
    }
    指定 includes 和 excludes

    includes:来指定想要显示的字段。

    excludes:来指定不想要显示的字段。

    二者都是可选的。

    示例:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "_source": {
    "includes":["title", "price"]
    },
    "query": {
    "term": {
    "price":
    }
    }
    }

    与下面的结果将是一样的:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "_source": {
    "excludes": ["images"]
    },
    "query": {
    "term": {
    "price":
    }
    }
    }

    响应结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "2",
    "_score": ,
    "_source": {
    "price": ,
    "title": "白米手机"
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": ,
    "_source": {
    "price": ,
    "title": "小米手机"
    }
    }
    ]
    }
    }

    过滤(filter)

    Elasticsearch 使用的查询语言(DSL)拥有一套查询组件,这些组件可以以无限组合的方式进行搭配。

    这套组件可以在以下两种情况下使用:过滤情况 - filtering context 和查询情况 - query context。

    如何选择查询与过滤:

    通常的规则是,使用查询(query)语句来进行全文搜索或者其它任何需要影响相关性得分的搜索;除此以外的情况都使用过滤(filters)。

    条件查询中进行过滤:

    所有的查询都会影响到文档的评分及排名。如果需要在查询结果中进行过滤,并且不希望过滤条件影响评分,那么就不要把过滤条件作为查询条件来用,而是使用 filter 方式:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query":{
    "bool":{
    "must":{ "match": { "title": "小米手机" }},
    "filter":{
    "range":{"price":{"gt":2000.00,"lt":3800.00}}
    }
    }
    }
    }

    无查询条件,直接过滤:

    如果一次查询只有过滤,没有查询条件,不希望进行评分,可以使用 constant_score 取代只有 filter 语句的 bool 查询。在性能上是完全相同的,但对于提高查询简洁性和清晰度有很大帮助。

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query":{
    "constant_score": {
    "filter": {
    "range": {"price":{"gt":2000.00, "lt":3000.00}}
    }
    }
    }
    }

    排序

    单字段排序

    sort 可以按照不同的字段进行排序,并且通过 order 指定排序的方式。

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "match": {
    "title": "小米手机"
    }
    },
    "sort": [
    {
    "price": {
    "order": "desc"
    }
    }
    ]
    }
    多字段排序

    假定想要结合使用 price_score 进行查询,并且匹配的结果首先按照价格排序,然后按照相关性得分排序:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query":
    {
    "bool":
    {
    "must":
    {
    "match":
    {
    "title": "小米手机"
    }
    },
    "filter":{
    "range":
    {
    "price":
    {
    "gt":,
    "lt":
    }
    }
    }
    }
    },
    "sort": [
    {
    "price":
    {
    "order": "desc"
    }
    },
    {
    "_score":
    {
    "order": "desc"
    }

        }
    ]
    

    }

    分页

    Elasticsearch 中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回。那么,如何实现分页查询呢?

    Elasticsearch 的分页与 MySQL 数据库非常相似,都是指定两个值:

    • from - 目标数据的偏移值(开始位置),默认 from 为 0。
    • size - 每页大小。
    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "match_all": {}
    },
    "sort": [
    {
    "price": {
    "order": "asc"
    }
    }
    ],
    "from": ,
    "size":
    }

    结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": null,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "5",
    "_score": null,
    "_source": {
    "title": "Apple手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    },
    "sort": [

        ]
      }
    ]
    

    }
    }

    高亮

    高亮原理:

    • 服务端搜索数据,得到搜索结果。
    • 把搜索结果中,搜索关键字都加上约定好的标签。
    • 前端页面提前写好标签的 CSS 样式,即可高亮。

    Elasticsearch 中实现高亮的语法比较简单:

    代码语言:javascript
    复制
    GET /renda/_search
    {
    "query": {
    "match": {
    "title": "手机"
    }
    },
    "highlight": {
    "pre_tags": "<em>",
    "post_tags": "</em>",
    "fields": {
    "title": {}
    }
    }
    }

    在使用 match 查询的同时,加上一个 highlight 属性:

    代码语言:javascript
    复制
    pre_tags:前置标签
    post_tags:后置标签
    fields:需要高亮的字段
    title:这里声明 title 字段需要高亮

    结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": 0.2876821,
    "hits": [
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "5",
    "_score": 0.2876821,
    "_source": {
    "title": "Apple手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    },
    "highlight": {
    "title": [
    "Apple<em>手机</em>"
    ]
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "2",
    "_score": 0.2876821,
    "_source": {
    "title": "白米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    },
    "highlight": {
    "title": [
    "白米<em>手机</em>"
    ]
    }
    },
    {
    "_index": "renda",
    "_type": "goods",
    "_id": "gPeQqHUB-UTJAEEuqOm9",
    "_score": 0.2876821,
    "_source": {
    "title": "小米手机",
    "images": "http://image.renda.com/12479122.jpg",
    "price":
    },
    "highlight": {
    "title": [
    "小米<em>手机</em>"
    ]
    }
    }
    ]
    }
    }

    聚合 Aggregations

    聚合可以极其方便的实现对数据的统计、分析,例如:

    • 什么品牌的手机最受欢迎?
    • 这些手机的平均价格、最高价格、最低价格?
    • 这些手机每月的销售情况如何?

    实现这些统计功能的比结构化数据库的 SQL 要方便的多,而且查询速度非常快,可以实现近实时搜索效果。

    基本概念

    Elasticsearch 中的聚合,包含多种类型,最常用的两种,一个叫”桶“ ,一个叫”度量“。

    桶(bucket)类似于 Group By。

    桶的作用,是按照某种方式对数据进行分组,每一组数据在 ES 中称为一个 桶 ,例如根据国籍对人划分,可以得到中国桶 、英国桶、日本桶等等,或者按照年龄段对人进行划分:010, 1020, 2030, 3040 等。

    Elasticsearch 中提供的划分桶的方式有很多:

    • Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组。
    • Histogram Aggregation:根据数值阶梯分组,与日期类似,需要知道分组的间隔(interval)。
    • Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组。
    • Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组。

    综上所述,bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往 bucket 中往往会嵌套另一种聚合:度量 - metrics aggregations。

    度量(metrics)相当于聚合的结果。

    分组完成以后,一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在 ES 中称为度量。

    比较常用的一些度量聚合方式:

    • Avg Aggregation - 求平均值。
    • Max Aggregation - 求最大值。
    • Min Aggregation - 求最小值。
    • Percentiles Aggregation - 求百分比。
    • Stats Aggregation - 同时返回 avg、max、min、sum、count 等。
    • Sum Aggregation - 求和。
    • Top hits Aggregation - 求前几。
    • Value Count Aggregation - 求总数。

    为了测试聚合,先批量导入一些数据。

    创建索引:

    代码语言:javascript
    复制
    PUT /car
    {
    "mappings": {
    "orders": {
    "properties": {
    "color": {
    "type": "keyword"
    },
    "make": {
    "type": "keyword"
    }
    }
    }
    }
    }

    注意:在 ES 中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词,必须使用 keyword 或数值类型 。这里将 color 和 make 这两个文字类型的字段设置为 keyword 类型,这个类型不会被分词,将来就可以参与聚合。

    导入数据,这里是采用批处理的 API,可以直接复制到 Kibana 运行即可:

    代码语言:javascript
    复制
    POST /car/orders/_bulk
    { "index": {}}
    { "price" : , "color" : "红", "make" : "本田", "sold" : "2020-10-28" }
    { "index": {}}
    { "price" : , "color" : "红", "make" : "本田", "sold" : "2020-11-05" }
    { "index": {}}
    { "price" : , "color" : "绿", "make" : "福特", "sold" : "2020-05-18" }
    { "index": {}}
    { "price" : , "color" : "蓝", "make" : "丰田", "sold" : "2020-07-02" }
    { "index": {}}
    { "price" : , "color" : "绿", "make" : "丰田", "sold" : "2020-08-19" }
    { "index": {}}
    { "price" : , "color" : "红", "make" : "本田", "sold" : "2020-11-05" }
    { "index": {}}
    { "price" : , "color" : "红", "make" : "宝马", "sold" : "2020-01-01" }
    { "index": {}}
    { "price" : , "color" : "蓝", "make" : "福特", "sold" : "2020-02-12" }

    聚合为桶

    首先,按照汽车的颜色 color 来划分桶,按照颜色分桶,最好是使用 TermAggregation 类型,按照颜色的名称来分桶。

    代码语言:javascript
    复制
    GET /car/_search
    {
    "size" : ,
    "aggs" : {
    "popular_colors" : {
    "terms" : {
    "field" : "color"
    }
    }
    }
    }

    分析:

    代码语言:javascript
    复制
    size:查询条数,这里设置为 0,因为不关心搜索到的数据,只关心聚合结果,提高效率
    aggs:声明这是一个聚合查询,是 aggregations 的缩写
    popular_colors:给这次聚合起一个名字,可任意指定
    terms:聚合的类型,这里选择 terms,是根据词条内容(这里是颜色)划分
    field:划分桶时依赖的字段

    结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": []
    },
    "aggregations": {
    "popular_colors": {
    "doc_count_error_upper_bound": ,
    "sum_other_doc_count": ,
    "buckets": [
    {
    "key": "红",
    "doc_count":
    },
    {
    "key": "绿",
    "doc_count":
    },
    {
    "key": "蓝",
    "doc_count":
    }
    ]
    }
    }
    }

    结果分析:

    代码语言:javascript
    复制
    hits:查询结果为空,因为设置了 size 为
    aggregations:聚合的结果
    popular_colors:定义的聚合名称
    buckets:查找到的桶,每个不同的 color 字段值都会形成一个桶
    key:这个桶对应的 color 字段的值
    doc_count:这个桶中的文档数量

    通过聚合的结果发现,目前红色的小车比较畅销。

    桶内度量

    前面的例子展示每个桶里面的文档数量,这很有用。但通常,应用需要提供更复杂的文档度量。例如,每种颜色汽车的平均价格是多少?

    因此,需要告诉 Elasticsearch 使用哪个字段,使用何种度量方式进行运算,这些信息要嵌套在 桶内,度量的运算会基于桶内的文档进行。

    现在,为刚刚的聚合结果添加求价格平均值的度量:

    代码语言:javascript
    复制
    GET /car/_search
    {
    "size" : ,
    "aggs" : {
    "popular_colors" : {
    "terms" : {
    "field" : "color"
    },
    "aggs":{
    "avg_price": {
    "avg": {
    "field": "price"
    }
    }
    }
    }
    }
    }
    • aggs:在上一个 aggs(popular_colors) 中添加新的 aggs,可见度量也是一个聚合
    • avg_price:聚合的名称
    • avg:度量的类型,这里是求平均值
    • field:度量运算的字段

    结果:

    代码语言:javascript
    复制
    {
    "took": ,
    "timed_out": false,
    "_shards": {
    "total": ,
    "successful": ,
    "skipped": ,
    "failed":
    },
    "hits": {
    "total": ,
    "max_score": ,
    "hits": []
    },
    "aggregations": {
    "popular_colors": {
    "doc_count_error_upper_bound": ,
    "sum_other_doc_count": ,
    "buckets": [
    {
    "key": "红",
    "doc_count": ,
    "avg_price": {
    "value":
    }
    },
    {
    "key": "绿",
    "doc_count": ,
    "avg_price": {
    "value":
    }
    },
    {
    "key": "蓝",
    "doc_count": ,
    "avg_price": {
    "value":
    }
    }
    ]
    }
    }
    }

    可以看到每个桶中都有自己的 avg_price 字段,这是度量聚合的结果。

    Elasticsearch 集群

    单点的问题

    单点的 Elasticsearch 存在的问题:

    • 单台机器存储容量有限,无法实现高存储。
    • 单服务器容易出现单点故障,无法实现高可用。
    • 单服务的并发处理能力有限,无法实现高并发。

    所以,为了应对这些问题,需要对 Elasticsearch 搭建集群。

    集群的结构

    数据分片

    首先,面临的第一个问题就是数据量太大,单点存储量有限的问题。

    可以把数据拆分成多份,每一份存储到不同机器节点(node),从而实现减少每个节点数据量的目的。这就是数据的分布式存储,也叫做:数据分片(Shard)。

    代码语言:javascript
    复制
    完整索引库indices ----> [分片shard1, 分片shard2, 分片shard3]

    创建索引,分为三个分片,
    将每个分片放在不同的集群节点中,以此实现高存储。

    数据备份

    数据分片解决了海量数据存储的问题,但是如果出现单点故障,那么分片数据就不再完整,这又该如何解决呢?

    可以给每个分片数据进行备份,存储到其它节点,防止数据丢失,这就是数据备份,也叫数据副本(replica) 。

    数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本过高。

    为了在高可用和成本间寻求平衡:

    • 首先对数据分片,存储到不同节点。
    • 然后对每个分片进行备份,放到对方节点,完成互相备份。

    这样可以大大减少所需要的服务节点数量。

    以 3 分片,每个分片备份一份为例:

    代码语言:javascript
    复制
    node-01 : 0, 2
    node-02 : 0, 1
    node-03 : 1, 2

    集群有三个节点,分别是 node-01、node-02、node-03;
    新建索引 renda,指定分片为 3,副本为 1,三个主数据,三个副本;
    三个分片为 0,1,2。

    0 对应 node-01,
    1 对应 node-02,
    2 对应 node-03,

    在这个集群中,如果出现单节点故障,并不会导致数据缺失,所以保证了集群的高可用,同时也减少了节点中数据存储量。并且因为是多个节点存储数据,因此用户请求也会分发到不同服务器,并发能力也得到了一定的提升。

    搭建集群

    集群需要多台机器,这里用一台机器来模拟,因此需要在一台虚拟机中部署多个 Elasticsearch 节点,每个 Elasticsearch 的端口都必须不一样。

    一台机器进行模拟:将 ES 的安装包复制三份,修改端口号,data 和 log 存放位置的不同。

    实际开发中:将每个 ES 节点放在不同的服务器上。

    集群名称为:renda-elastic,部署 3 个 elasticsearch 节点,分别是:

    • node-01:http 端口 9201,TCP 端口 9301
    • node-02:http 端口 9202,TCP 端口 9302
    • node-03:http 端口 9203,TCP 端口 9303

    http:表示使用 http 协议进行访问时使用端口,elasticsearch-head、kibana、postman,默认端口号是 9200。

    tcp:集群间的各个节点进行通讯的端口,默认 9300。

    第一步:复制 es 软件粘贴 3 次,分别改名。

    第二步:修改每一个节点的配置文件 config 下的 elasticsearch.yml,下面以第一份配置文件为例。

    三个节点的配置文件几乎一致,除了:node.name、path.data、path.logs、http.port、transport.tcp.port。

    node-01:

    代码语言:javascript
    复制
    # 允许跨域名访问
    http.cors.enabled: true

    当设置允许跨域,默认为*,表示支持所有域名

    http.cors.allow-origin: "*"

    允许所有节点访问

    network.host: 0.0.0.0

    集群的名称,同一个集群下所有节点的集群名称应该一致

    cluster.name: renda-elastic

    当前节点名称 每个节点不一样

    node.name: node-01

    数据的存放路径 每个节点不一样,不同 es 服务器对应的 data 和 log 存储的路径不能一样

    path.data: e:\class\es-9201\data

    日志的存放路径 每个节点不一样

    path.logs: e:\class\es-9201\logs

    http协议的对外端口 每个节点不一样,默认:9200

    http.port: 9201

    TCP协议对外端口 每个节点不一样,默认:9300

    transport.tcp.port: 9301

    三个节点相互发现,包含自己,使用 tcp 协议的端口号

    discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]

    声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1

    discovery.zen.minimum_master_nodes: 2

    是否为主节点

    node.master: true

    node-02:

    代码语言:javascript
    复制
    # 允许跨域名访问
    http.cors.enabled: true
    http.cors.allow-origin: "*"
    network.host: 0.0.0.0

    集群的名称

    cluster.name: renda-elastic

    当前节点名称 每个节点不一样

    node.name: node-02

    数据的存放路径 每个节点不一样

    path.data: e:\class\es-9202\data

    日志的存放路径 每个节点不一样

    path.logs: e:\class\es-9202\logs

    http 协议的对外端口 每个节点不一样

    http.port: 9202

    TCP 协议对外端口 每个节点不一样

    transport.tcp.port: 9302

    三个节点相互发现

    discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]

    声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1

    discovery.zen.minimum_master_nodes: 2

    是否为主节点

    node.master: true

    node-03:

    代码语言:javascript
    复制
    # 允许跨域名访问
    http.cors.enabled: true
    http.cors.allow-origin: "*"
    network.host: 0.0.0.0

    集群的名称

    cluster.name: renda-elastic

    当前节点名称 每个节点不一样

    node.name: node-03

    数据的存放路径 每个节点不一样

    path.data: e:\class\es-9203\data

    日志的存放路径 每个节点不一样

    path.logs: e:\class\es-9203\logs

    http协议的对外端口 每个节点不一样

    http.port: 9203

    TCP协议对外端口 每个节点不一样

    transport.tcp.port: 9303

    三个节点相互发现

    discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]

    声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1

    discovery.zen.minimum_master_nodes: 2

    是否为主节点

    node.master: true

    第三步:启动集群

    把三个节点分别启动,要确保一个一个地启动。

    Chrome 浏览器使用 Head 插件查看节点启动状态,connect http://localhost:9201/

    测试集群中创建索引库

    配置 kibana.yml:

    代码语言:javascript
    复制
    # 端口号改为 9201 或 9202 或 9203 都可以
    elasticsearch.url: "http://localhost:9201"

    再重启 Kibana。

    搭建集群以后就要创建索引库了,那么问题来了,当创建一个索引库后,数据会保存到哪个服务节点上呢?如果对索引库分片,那么每个片会在哪个节点呢?

    使用 ElasticSearch-Head 创建新的 Index:名称为 renda,分片数为 3,副本为 1。

    对比创建索引库的 API 示例:

    代码语言:javascript
    复制
    PUT /renda
    {
    "settings": {
    "number_of_shards": ,
    "number_of_replicas":
    }
    }

    这里有两个配置:

    • number_of_shards:分片数量,这里设置为 3
    • number_of_replicas:副本数量,这里设置为 1,每个分片一个备份,一个原始数据,共 2 份。

    通过 chrome 浏览器的 head 插件查看,可以查看到分片的存储结构。

    可以看到,renda 这个索引库,有三个分片,分别是 0、1、2,每个分片有 1 个副本,共 6 份。

    • node-01 上保存了 1 号分片和 2 号分片的副本
    • node-02 上保存了 0 号分片和 2 号分片的副本
    • node-03 上保存了 0 号分片和 1 号分片的副本

    集群工作原理

    Shard 与 Replica 机制

    1)一个 index 包含多个 shard,也就是一个 index 存在多个服务器上。

    2)每个 shard 都是一个最小工作单元,承载部分数据,比如有三台服务器,现在有三条数据,这三条数据在三台服务器上各方一条。

    3)增减节点时,shard 会自动在 nodes 中负载均衡。

    4)primary shard(主分片)和 replica shard(副本分片),每个 document 肯定只存在于某一个 primary shard 以及其对应的 replica shard 中,不可能存在于多个 primary shard。

    5)replica shard 是 primary shard 的副本,负责容错,以及承担读请求负载。

    6)primary shard 的数量在创建索引的时候就固定了,replica shard 的数量可以随时修改。

    7)primary shard 的默认数量是 5,replica 默认是 1(每个主分片一个副本分片),默认有 10 个 shard,5 个 primary shard,5 个 replica shard。

    8)primary shard 不能和自己的 replica shard 放在同一个节点上(否则节点宕机,primary shard 和副本都丢失,起不到容错的作用),但是可以和其他 primary shard 的 replica shard 放在同一个节点上。

    集群写入数据
    1. 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node (协调节点)。
    2. Coordinating node,对document进行路由,将请求转发给对应的node(根据一定的算法选择对应的节点进行存储)。
    3. 实际上的 node 上的 primary shard 处理请求,将数据保存在本地,然后将数据同步到 replica node。
    4. Coordinating node,如果发现 primary node 和所有的 replica node 都搞定之后,就会返回请求到客户端。

    这个路由简单的说就是取模算法,比如说现在有 3 台服务器,这个时候传过来的 id 是 5,那么 5 % 3 = 2,就放在第 2 台服务器。

    ES 查询数据
    倒排序算法

    倒排序算法:通过分词把词语出现的 id 进行记录下来,再查询的时候先去查到哪些 id 包含这个数据,然后再根据 id 把数据查出来。

    查询过程
    1. 客户端发送一个请求给 coordinate node 协调节点。
    2. 协调节点将搜索的请求转发给所有的 shard 对应的 primary shard 或 replica shard。
    3. Query phase(查询阶段),每一个 shard 将自己搜索的结果(其实也就是一些唯一标识),返回给协调节点,由协调节点进行数据的合并,排序,分页等操作,产出最后的结果。
    4. Fetch phase(获取阶段),接着由协调节点,根据唯一标识去各个节点进行拉取数据,最终返回给客户端。

    Elasticsearch 客户端

    客户端介绍

    在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html

    注意选择版本为 6.2.4 ,与之前的版本保持一致。

    创建 Demo 工程

    初始化项目

    使用 Spring Initializr 初始化项目 elasticsearch-demo --> 选择 Developer Tools 的 Spring Boot DevTools、Lombok,Web 的 Spring Web。

    POM 文件

    注意,这里直接导入了 SpringBoot 的启动器,方便后续整合 Spring Data Elasticsearch,不过还需要手动引入 Elasticsearch 的 High-level-Rest-Client 的依赖。

    另外还要注意确保 spring boot 版本号与 es client 相对应,否则运行时会报创建 elasticsearchRestHighLevelClient 的错误;如果出现了这种错误,就需要 Maven clean 一下项目,然后确保版本号正确后再重新运行。

    代码语言:javascript
    复制
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.renda</groupId>
    <artifactId>elasticsearch-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elasticsearch-demo</name>
    <description>Demo project for Spring Boot</description>

    &lt;properties&gt;
        &lt;java.version&gt;11&lt;/java.version&gt;
    &lt;/properties&gt;
    
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
            &lt;optional&gt;true&lt;/optional&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
            &lt;optional&gt;true&lt;/optional&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
            &lt;exclusions&gt;
                &lt;exclusion&gt;
                    &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;
                    &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;
                &lt;/exclusion&gt;
            &lt;/exclusions&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;junit&lt;/groupId&gt;
            &lt;artifactId&gt;junit&lt;/artifactId&gt;
            &lt;version&gt;4.12&lt;/version&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-logging&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.google.code.gson&lt;/groupId&gt;
            &lt;artifactId&gt;gson&lt;/artifactId&gt;
            &lt;version&gt;2.8.5&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;
            &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;
            &lt;version&gt;3.8.1&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- Apache 开源组织提供的用于操作 JAVA BEAN 的工具包 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;commons-beanutils&lt;/groupId&gt;
            &lt;artifactId&gt;commons-beanutils&lt;/artifactId&gt;
            &lt;version&gt;1.9.1&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- ES 高级 Rest Client --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.elasticsearch.client&lt;/groupId&gt;
            &lt;artifactId&gt;elasticsearch-rest-high-level-client&lt;/artifactId&gt;
            &lt;version&gt;6.4.3&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
    
    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
    

    </project>

    配置文件

    在 resource 下创建 application.yml

    索引库及映射

    创建索引库的同时,也会创建 type 及其映射关系,但是这些操作不建议使用 java 客户端完成,原因如下:

    • 索引库和映射往往是初始化时完成,不需要频繁操作,不如提前配置好。
    • 官方提供的创建索引库及映射 API 非常繁琐,需要通过字符串拼接 json 结构。

    因此,这些操作建议还是使用 Rest 风格 API 去实现。

    以一个商品数据为例来创建索引库:

    com.renda.pojo.Product

    代码语言:javascript
    复制
    @Data
    public class Product {

    private Long id;
    
    private String title; // 标题
    
    private String category; // 分类
    
    private String brand; // 品牌
    
    private Double price; // 价格
    
    private String images; // 图片地址
    

    }

    分析一下数据结构:

    • id:可以认为是主键,将来判断数据是否重复的标示,不分词,可以使用 keyword 类型。
    • title:搜索字段,需要分词,可以用 text 类型。
    • category:商品分类,这个是整体,不分词,可以使用 keyword 类型。
    • brand:品牌,与分类类似,不分词,可以使用 keyword 类型。
    • price:价格,这个是 double 类型。
    • images:图片,用来展示的字段,不搜索,index 为 false,不分词,可以使用 keyword 类型。

    可以编写这样的映射配置:

    代码语言:javascript
    复制
    PUT /renda
    {
    "settings": {
    "number_of_shards": ,
    "number_of_replicas":
    },
    "mappings": {
    "item": {
    "properties": {
    "id": {
    "type": "keyword"
    },
    "title": {
    "type": "text",
    "analyzer": "ik_max_word"
    },
    "category": {
    "type": "keyword"
    },
    "brand": {
    "type": "keyword"
    },
    "images": {
    "type": "keyword",
    "index": false
    },
    "price": {
    "type": "double"
    }
    }
    }
    }
    }

    索引数据操作

    有了索引库,接下来看看如何新增索引数据。

    操作 MySQL 数据库:

    • 获取数据库连接
    • 完成数据的增删改查
    • 释放资源
    初始化客户端

    完成任何操作都需要通过 HighLevelRestClient 客户端。

    编写一个测试类:

    com.renda.ElasticsearchDemoApplicationTests

    代码语言:javascript
    复制
    @SpringBootTest
    @RunWith(SpringRunner.class)
    class ElasticsearchDemoApplicationTests {

    private RestHighLevelClient restHighLevelClient;
    
    /**
     * 初始化客户端
     */
    @Before
    public void init() {
        RestClientBuilder restClientBuilder = RestClient.builder(
                new HttpHost(&#34;127.0.0.1&#34;, , &#34;http&#34;),
                new HttpHost(&#34;127.0.0.1&#34;, , &#34;http&#34;),
                new HttpHost(&#34;127.0.0.1&#34;, , &#34;http&#34;)
        );
        restHighLevelClient = new RestHighLevelClient(restClientBuilder);
    }
    
    /**
     * 关闭客户端
     */
    @After
    public void close() throws IOException {
        restHighLevelClient.close();
    }
    

    }

    新增文档

    示例:

    com.renda.ElasticsearchDemoApplicationTests

    代码语言:javascript
    复制
    package com.renda;

    import com.google.gson.Gson;
    import com.renda.pojo.Product;
    import org.apache.http.HttpHost;
    import org.elasticsearch.action.index.IndexRequest;
    import org.elasticsearch.action.index.IndexResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestClientBuilder;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.common.xcontent.XContentType;
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;

    import java.io.IOException;

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ElasticsearchDemoApplicationTests {

    private RestHighLevelClient restHighLevelClient;
    private Gson gson = new Gson();
    
    ...
    
    /**
     * 插入文档
     */
    @Test
    public void testInsert() throws IOException {
        // 1.文档数据
        Product product = new Product();
        product.setBrand(&#34;华为&#34;);
        product.setCategory(&#34;手机&#34;);
        product.setId(1L);
        product.setImages(&#34;http://image.huawei.com/1.jpg&#34;);
        product.setPrice(5999.99);
        product.setTitle(&#34;华为P30&#34;);
        // 2.将文档数据转换为 json 格式
        String source = gson.toJson(product);
        // 3.创建索引请求对象 访问哪个索引库、哪个 type、指定文档 ID
        // public IndexRequest(String index, String type, String id)
        IndexRequest request = new IndexRequest(&#34;renda&#34;, &#34;item&#34;, product.getId().toString());
        request.source(source, XContentType.JSON);
        // 4.发出请求
        IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
        System.out.println(response);
    }
    

    }

    看下响应:

    代码语言:javascript
    复制
    IndexResponse[
    index=renda,
    type=item,
    id=,
    version=,
    result=updated,
    seqNo=,
    primaryTerm=,
    shards={
    "total":,
    "successful":,
    "failed":
    }
    ]
    查看文档

    根据 Rest 风格,查看应该是根据 id 进行 get 查询,难点是对结果的解析:

    代码语言:javascript
    复制
    ...

    /**

    • 查看文档
      */
      @Test
      public void testView() throws IOException {
      // 初始化 GetRequest 对象
      GetRequest getRequest = new GetRequest("renda", "item", "1");
      // 执行查询
      GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
      // 取出数据
      String source = getResponse.getSourceAsString();
      Product product = gson.fromJson(source, Product.class);
      System.out.println(product);
      }

    ...

    结果:

    代码语言:javascript
    复制
    Product(
    id=,
    title=华为P,
    category=手机,
    brand=华为,
    price=5999.99,
    images=http://image.huawei.com/1.jpg
    )
    修改文档

    新增时,如果传递的 id 是已经存在的,则会完成修改操作,如果不存在,则是新增。

    删除文档

    根据 id 删除:

    代码语言:javascript
    复制
    /**

    • 删除文档
      */
      @Test
      public void testDelete() throws IOException {
      // 初始化 DeleteRequest 对象
      DeleteRequest request = new DeleteRequest("renda", "item", "1");
      // 执行删除
      DeleteResponse response = restHighLevelClient.delete(request, RequestOptions.DEFAULT);
      System.out.println(response);
      }

    结果:

    代码语言:javascript
    复制
    DeleteResponse[
    index=renda,
    type=item,
    id=,
    version=,
    result=deleted,
    shards=ShardInfo
    {
    total=2,
    successful=2,
    failures=[]
    }
    ]

    搜索数据

    查询所有 match_all
    代码语言:javascript
    复制
    /**
    
  • 可重用代码
    */
    public void baseQuery(SearchSourceBuilder sourceBuilder) throws IOException {
    // 创建搜索请求对象
    SearchRequest request = new SearchRequest();
    // 查询构建工具
    request.source(sourceBuilder);
    // 执行查询
    SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
    // 获得查询结果
    SearchHits hits = response.getHits();
    // 获得文件数组
    SearchHit[] hitsHits = hits.getHits();
    for(SearchHit searchHit: hitsHits){
    String json = searchHit.getSourceAsString();
    // 将 json 反序列化为 Product 格式
    Product product = gson.fromJson(json, Product.class);
    System.out.println(product);
    }
    }
  • /**

    • 查看所有文档
      */
      @Test
      public void matchAll() throws IOException {
      // 查询构建工具
      SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
      // 添加查询条件,执行查询类型
      sourceBuilder.query(QueryBuilders.matchAllQuery());
      // 调用基础查询方法
      baseQuery(sourceBuilder);
      }

    结果示例:

    代码语言:javascript
    复制
    item = Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=3, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.renda.com/13123.jpg'}

    注意,上面的代码中,搜索条件是通过 sourceBuilder.query(QueryBuilders.matchAllQuery()) 来添加的。这个 query() 方法接受的参数是:QueryBuilder 接口类型。

    这个接口提供了很多实现类,分别对应不同类型的查询,例如:term 查询、match 查询、range 查询、boolean 查询等。

    因此,如果要使用各种不同查询,其实仅仅是传递给 sourceBuilder.query() 方法的参数不同而已。而这些实现类不需要去 new ,官方提供了 QueryBuilders 工厂帮构建各种实现类。

    关键字搜索 match

    搜索类型的变化,仅仅是利用 QueryBuilders 构建的查询对象不同而已,其他代码基本一致:

    代码语言:javascript
    复制
    @Test
    public void matchQuery() throws IOException {
    SearchSourceBuilder builder = new SearchSourceBuilder();
    // 设置查询类型和查询条件
    builder.query(QueryBuilders.matchQuery("title", "手机"));
    // 调用基础查询方法
    baseQuery(builder);
    }

    结果示例:

    代码语言:javascript
    复制
    item = Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.renda.com/13123.jpg'}
    范围查询 range
    代码语言:javascript
    复制
    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price");

    支持下面的范围关键字:

    • gt(Object from) 大于
    • gte(Object from) 大于等于
    • lt(Object from) 小于
    • lte(Object from) 小于等于

    示例:

    代码语言:javascript
    复制
    @Test
    public void rangeQuery() throws IOException {
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 执行查询条件和查询类型
    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price");
    rangeQueryBuilder.gte();
    rangeQueryBuilder.lte();
    sourceBuilder.query(rangeQueryBuilder);
    baseQuery(sourceBuilder);
    }

    结果:

    代码语言:javascript
    复制
    item = Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=2, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=1, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.renda.com/13123.jpg'}
    source 过滤

    _source:存储原始文档。

    默认情况下,索引库中所有数据都会返回,如果想只返回部分字段,可以通过 source filter 来控制。

    代码语言:javascript
    复制
    @Test
    public void sourceFilter() throws IOException {
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 执行查询条件和查询类型
    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price");
    rangeQueryBuilder.gte();
    rangeQueryBuilder.lte();
    sourceBuilder.query(rangeQueryBuilder);
    // source 过滤,只保留 id、title、price
    sourceBuilder.fetchSource(new String[]{"id", "title", "price"}, null);
    baseQuery(sourceBuilder);
    }

    结果:

    代码语言:javascript
    复制
    item = Item{id=5, title='荣耀V10', category='null', brand='null', price=2799.0, images='null'}
    item = Item{id=2, title='坚果手机R1', category='null', brand='null', price=3699.0, images='null'}
    item = Item{id=4, title='小米Mix2S', category='null', brand='null', price=4299.0, images='null'}
    item = Item{id=1, title='小米手机7', category='null', brand='null', price=3299.0, images='null'}
    item = Item{id=3, title='华为META10', category='null', brand='null', price=4499.0, images='null'}

    排序

    依然是通过 sourceBuilder 来配置:

    代码语言:javascript
    复制
    @Test
    public void sortAndPage() throws IOException {
    // 创建搜索请求对象
    SearchRequest request = new SearchRequest();
    // 查询构建工具
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 添加查询条件,执行查询类型
    sourceBuilder.query(QueryBuilders.matchAllQuery());
    // 执行排序 价格降序排序
    sourceBuilder.sort("price", SortOrder.DESC);

    baseQuery(sourceBuilder);
    }

    结果:

    代码语言:javascript
    复制
    item = Item{id=, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.renda.com/13123.jpg'}

    分页

    分页需要视图层传递两个参数:

    • 当前页:currentPage
    • 每页大小:pageSize

    而 elasticsearch 中需要的不是当前页,而是起始位置,有公式可以计算出:

    • 起始位置:startPos = (currentPage - 1) * pageSize
    • 第一页:(1 - 1) * 5 = 0
    • 第二页:(2 - 1) * 5 = 5

    代码:

    代码语言:javascript
    复制
    @Test
    public void sortAndPage() throws IOException {
    // 创建搜索请求对象
    SearchRequest request = new SearchRequest();
    // 查询构建工具
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 添加查询条件,执行查询类型
    sourceBuilder.query(QueryBuilders.matchAllQuery());
    // 执行排序 价格降序排序
    sourceBuilder.sort("price", SortOrder.DESC);

    // 分页信息
    int currentPage = ;
    int pageSize = ;
    int startPos = (currentPage - ) * pageSize;
    //设置分页
    sourceBuilder.from(startPos);
    sourceBuilder.size();

    baseQuery(sourceBuilder);
    }

    结果:

    代码语言:javascript
    复制
    item = Item{id=, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='小米手机7', category='手机', brand='小米', price=3299.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='坚果手机R1', category='手机', brand='锤子', price=3699.0, images='http://image.renda.com/13123.jpg'}

    当 currentPage 为 2 的时候,结果是:

    代码语言:javascript
    复制
    item = Item{id=, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.renda.com/13123.jpg'}
    item = Item{id=, title='华为META10', category='手机', brand='华为', price=4499.0, images='http://image.renda.com/13123.jpg'}

    Spring Data Elasticsearch

    什么是 Spring Data Elasticsearch

    Spring Data Elasticsearch - SDE 是 Spring Data 项目下的一个子模块。

    Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如 MySQL),还是非关系数据库(如 Redis),或者类似 Elasticsearch 这样的索引数据库;从而简化开发人员的代码,提高开发效率。

    Spring Data Elasticsearch 的页面:https://projects.spring.io/spring-data-elasticsearch/

    特征:

    • 支持 Spring 的基于 @Configuration 的 java 配置方式,或者 XML 配置方式。
    • 提供了用于操作 ES 的便捷工具类 ElasticsearchTemplate,包括实现文档到 POJO 之间的自动智能映射。
    • 利用 Spring 的数据转换服务实现的功能丰富的对象映射。
    • 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式,可以定义 JavaBean:类名、属性。
    • 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似 MyBatis,根据接口自动得到实现);当然,也支持人工定制查询。

    配置 Spring Data Elasticsearch

    在 pom 文件中,引入 Spring Data Elasticsearch 的启动器:

    代码语言:javascript
    复制
    <!-- Spring data elasticsearch -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>

    然后,只需要在 resources 下的 application.yml 文件,引入 Elasticsearch 的 host 和 port 即可:

    代码语言:javascript
    复制
    spring:
    data:
    elasticsearch:
    cluster-name: renda-elastic
    cluster-nodes: 127.0.0.1:9301,127.0.0.1:9302,127.0.0.1:9303

    需要注意的是,Spring Data Elasticsearch 底层使用的不是 Elasticsearch 提供的 RestHighLevelClient,而是 TransportClient,并不采用 Http 协议通信,而是访问 Elasticsearch 对外开放的 tcp 端口,在之前集群配置中,设置的分别是:9301,9302,9303

    确保引导类如下:

    代码语言:javascript
    复制
    @SpringBootApplication
    public class ElasticsearchDemoApplication {

    public static void main(String[] args) {
    SpringApplication.run(ElasticsearchDemoApplication.class, args);
    }

    }

    另外,SpringBoot 已经配置好了各种 SDE 配置,并且注册了一个 ElasticsearchTemplate 供使用。

    索引库操作

    创建索引库

    Pojo 对象:

    代码语言:javascript
    复制
    @Data
    public class Product {

    private Long id;
    
    private String title; // 标题
    
    private String category; // 分类
    
    private String brand; // 品牌
    
    private Double price; // 价格
    
    private String images; // 图片地址
    

    }

    创建一个测试类,然后注入 ElasticsearchTemplate:

    代码语言:javascript
    复制
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ElasticsearchSpringDataTests {

    @Autowired
    private ElasticsearchTemplate template;
    

    }

    创建索引库的 API 示例:

    代码语言:javascript
    复制
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ElasticsearchSpringDataTests {

    @Autowired
    private ElasticsearchTemplate template;
    
    @Test
    public void createIndex() {
        template.createIndex(Product.class);
    }
    

    }

    运行测试方法,发现报错:Product is not a Document;因为创建索引库需要指定的信息,比如:索引库名、类型名、分片、副本数量、映射信息都没有填写。

    自定义工具类类似,SDE 也是通过实体类上的注解来配置索引库信息的,需要在 Product 上添加下面的一些注解:

    代码语言:javascript
    复制
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Document(indexName = "renda", type = "product", shards = , replicas = )
    public class Product {

    @Id
    private Long id;
    
    @Field(type = FieldType.Text, analyzer = &#34;ik_max_word&#34;)
    private String title; // 标题
    
    @Field(type = FieldType.Keyword)
    private String category; // 分类
    
    @Field(type = FieldType.Keyword)
    private String brand; // 品牌
    
    @Field(type = FieldType.Double)
    private Double price; // 价格
    
    @Field(type = FieldType.Keyword, index = false)
    private String images; // 图片地址
    

    }

    @Document:声明索引库配置

    • indexName:索引库名称
    • type:类型名称,默认是 “docs”
    • shards:分片数量,默认 5
    • replicas:副本数量,默认 1

    @Id:声明实体类的 id
    @Field:声明字段属性

    • type:字段的数据类型
    • analyzer:指定分词器类型
    • index:是否创建索引
    创建映射

    刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样:

    代码语言:javascript
    复制
    @Test
    public void createType() {
    template.putMapping(Product.class);
    }

    索引数据 CRUD

    SDE 的索引数据 CRUD 并没有封装在 ElasticsearchTemplate 中,而是有一个叫做 ElasticsearchRepository 的接口。

    需要自定义接口,继承 ElasticsearchRespository:

    com.renda.repository.ProductRepository

    代码语言:javascript
    复制
    package com.renda.repository;

    import com.renda.pojo.Product;
    import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

    /**

    • 当 SDE 访问索引库时,

    • 需要定义一个持久层的接口去继承 ElasticsearchRepository 接口即可,

    • 无需实现

    • @author Renda Zhang

    • @since 2020-11-10 23:00
      */
      public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
      }

    创建索引数据

    创建索引有单个创建和批量创建之分。

    单个创建:

    代码语言:javascript
    复制
    @Test
    public void insertDocument() {
    Product product = new Product(6L, "小米手机", "手机", "锤子", 3299.99, "http://image.renda.com/1.jpg");
    productRepository.save(product);
    System.out.println("Successfully Saved");
    }

    批量创建:

    代码语言:javascript
    复制
    @Test
    public void insertDocuments() {
    Product product1 = new Product(2L, "坚果手机", "手机", "phone", 3299.99, "http://image.renda.com/1.jpg");
    Product product2 = new Product(3L, "华为手机", "手机", "phone", 3299.99, "http://image.renda.com/1.jpg");
    Product product3 = new Product(4L, "苹果手机", "手机", "phone", 3299.99, "http://image.renda.com/1.jpg");
    Product product4 = new Product(5L, "索尼手机", "手机", "phone", 3299.99, "http://image.renda.com/1.jpg");
    List<Product> list = new ArrayList<>();
    list.add(product1);
    list.add(product2);
    list.add(product3);
    list.add(product4);
    productRepository.saveAll(list);
    System.out.println("Successfully Saved All");
    }
    查询索引数据

    默认提供了根据 id 查询,查询所有两个功能。

    根据 id 查询:

    代码语言:javascript
    复制
    @Test
    public void findById() {
    Optional<Product> optional = productRepository.findById(3L);
    // orElse 方法的作用:如果 optional 中封装的实体对象为空也就是没有从索引库中查询出匹配的文档,返回 orElse 方法的参数
    Product product = optional.orElse(null);
    System.out.println(product);
    }

    结果:

    代码语言:javascript
    复制
    Product(id=, title=华为手机, category=手机, brand=phone, price=3299.99, images=http://image.renda.com/1.jpg)

    查询所有:

    代码语言:javascript
    复制
    @Test
    public void findAll() {
    productRepository.findAll().forEach(System.out::println);
    }

    结果:

    代码语言:javascript
    复制
    Product(id=, title=小米手机, category=手机, brand=锤子, price=3299.99, images=http://image.renda.com/1.jpg)
    Product(id=, title=坚果手机, category=手机, brand=phone, price=3299.99, images=http://image.renda.com/1.jpg)
    Product(id=, title=苹果手机, category=手机, brand=phone, price=3299.99, images=http://image.renda.com/1.jpg)
    Product(id=, title=索尼手机, category=手机, brand=phone, price=3299.99, images=http://image.renda.com/1.jpg)
    Product(id=, title=华为手机, category=手机, brand=phone, price=3299.99, images=http://image.renda.com/1.jpg)
    自定义方法查询

    ProductRepository 提供的查询方法有限,但是它却提供了非常强大的自定义查询功能。

    只要遵循 Spring Data 提供的语法,可以任意定义方法声明:

    com.renda.repository.ProductRepository

    代码语言:javascript
    复制
    public interface ProductRepository extends ElasticsearchRepository<Product, Long> {

    /**

    • 查询价格范围
      */
      List<Product> findByPriceBetween(Double from, Double to);

    }

    无需写实现,SDE 会自动实现该方法,直接用即可:

    代码语言:javascript
    复制
    @Test
    public void findByPrice() {
    List<Product> list = productRepository.findByPriceBetween(2000.00, 4000.00);
    System.out.println(list.size());
    }

    支持的一些语法示例:

    代码语言:javascript
    复制
    And
    findByNameAndPrice
    {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}

    Or
    findByNameOrPrice
    {"bool" : {"should" : [{"field" : {"name" : "?"}}, {"field" : {"price" : "?"}}]}}

    Is
    findByName
    {"bool" : {"must" : {"field" :{"name" : "?"}}}}

    Not
    findByNameNot
    {"bool" : {"must_not" :{"field" : {"name" : "?"}}}}

    Between
    findByPriceBetween
    {"bool" : {"must" : {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true}}}}}

    LessThanEqual
    findByPriceLessThan
    {"bool" : {"must" : {"range" :{"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true}}}}}

    GreaterThanEqual
    findByPriceGreaterThan
    {"bool" : {"must" : {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true}}}}}

    Before
    findByPriceBefore
    {"bool" : {"must" : {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true}}}}}

    After
    findByPriceAfter
    {"bool" : {"must" : {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true}}}}}

    Like
    findByNameLike
    {"bool" : {"must" : {"field" : {"name" : {"query" : "?*", "analyze_wildcard" : true}}}}}

    StartingWith
    findByNameStartingWith
    {"bool" : {"must" : {"field" : {"name" : {"query" : "?*", "analyze_wildcard" : true}}}}}

    EndingWith
    findByNameEndingWith
    {"bool" : {"must" : {"field" : {"name" : {"query" : "*?", "analyze_wildcard" : true}}}}}

    Contains/Containing
    findByNameContaining
    {"bool" : {"must" : {"field" : {"name" : {"query" : "?", "analyze_wildcard" : true}}}}}

    In
    findByNameIn(Collection<String>names)
    {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}

    NotIn
    findByNameNotIn(Collection<String>names)
    {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}

    Near
    findByStoreNear
    Not Supported Yet !

    True
    findByAvailableTrue
    {"bool" : {"must" : {"field" : {"available" : true}}}}

    False
    findByAvailableFalse
    {"bool" : {"must" : {"field" : {"available" : false}}}}

    OrderBy
    findByAvailableTrueOrderByNameDesc
    {"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

    原生查询

    如果上述接口依然不符合需求,SDE 也支持原生查询,这个时候还是使用 ElasticsearchTemplate。

    而查询条件的构建是通过一个名为 NativeSearchQueryBuilder 的类来完成的,不过这个类的底层还是使用 ES 的原生 API 中的 QueryBuilders 、 AggregationBuilders 、 HighlightBuilders 等工具。

    需求:查询 title 中包含小米手机的商品,以价格升序排序,分页查询:每页展示 2 条,查询第 1 页;对查询结果进行聚合分析:获取品牌及个数。

    示例:

    代码语言:javascript
    复制
    @Test
    public void nativeQuery() {
    // 1.构架一个原生查询器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 2.source 过滤
    // 2.1 参数:final String[] includes, final String[] excludes
    // 如果不想执行 source 过滤可以将该行注释
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[], new String[]));
    // 3.查询条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
    // 4.设置分页和排序规则
    queryBuilder.withPageable(PageRequest.of(, , Sort.by(Sort.Direction.ASC, "price")));
    // 5.高亮
    // ...
    // 6.聚合
    queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
    // 7.查询
    AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);

    // 8. 解析结果
    // 获取分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List&lt;Product&gt; content = result.getContent();
    System.out.println(total + &#34;  &#34; + totalPages);
    content.forEach(System.out::println);
    // 获取聚合结果
    Aggregations resultAggregations = result.getAggregations();
    Terms terms = resultAggregations.get(&#34;brandAgg&#34;);
    terms.getBuckets().forEach(bucket -&gt; {
        System.out.println(&#34;品牌:&#34; + bucket.getKeyAsString());
        System.out.println(&#34;数量:&#34; + bucket.getDocCount());
    });
    

    }

    上述查询没有实现高亮结果,以下实现高亮展示。

    1、首先,自定义搜索结果映射:

    com.renda.resultMapper.ESSearchResultMapper

    代码语言:javascript
    复制
    package com.renda.resultMapper;

    import com.google.gson.Gson;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.search.SearchHit;
    import org.elasticsearch.search.SearchHits;
    import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.elasticsearch.core.SearchResultMapper;
    import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
    import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;

    import java.util.ArrayList;
    import java.util.Map;

    /**

    • 自定义结果映射,处理高亮

    • @author Renda Zhang

    • @since 2020-11-10 23:50
      */
      public class ESSearchResultMapper implements SearchResultMapper {

      /**

      • 完成结果映射

      • 操作的重点应该是将原有的结果:_source 取出来,放入高亮的数据

      • @return AggregatedPage 需要三个参数进行构建:pageable, List<Product>, 总记录数
        */
        @Override
        public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
        // 获得总记录数
        SearchHits searchHits = searchResponse.getHits();
        if (searchHits.getHits().length <= ) {
        return null;
        }

        // 记录列表
        ArrayList<T> list = new ArrayList<>();
        // 获取原始的搜索结果
        for (SearchHit hit : searchHits) {
        // 获取 _source 属性中的所有数据
        Map<String, Object> map = hit.getSourceAsMap();
        // 获得高亮的字段
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        // 每个高亮字段都需要进行设置
        for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
        // 获得高亮的 key:高亮字段
        String key = highlightField.getKey();
        // 获得 value:高亮之后的效果
        HighlightField value = highlightField.getValue();
        // 将高亮字段和文本效果放入到 map 中,覆盖对应数据
        map.put(key, value.getFragments()[].toString());
        }
        // 将 map 转换为对象
        // map --> jsonString --> 对象
        Gson gson = new Gson();
        T t = gson.fromJson(gson.toJson(map), aClass);
        list.add(t);
        }

        // 返回
        return new AggregatedPageImpl<>(list, pageable, searchHits.getTotalHits());
        }
        }

    2、高亮实现:

    代码语言:javascript
    复制
    @Test
    public void nativeQuery() {
    // 1.构架一个原生查询器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 2.source 过滤
    // 2.1 参数:final String[] includes, final String[] excludes
    // 如果不想执行 source 过滤可以将该行注释
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[], new String[]));
    // 3.查询条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
    // 4.设置分页和排序规则
    queryBuilder.withPageable(PageRequest.of(, , Sort.by(Sort.Direction.DESC, "price")));
    // 5.高亮
    HighlightBuilder.Field field = new HighlightBuilder.Field("title");
    field.preTags("<font style='color:red'>");
    field.postTags("</font>");
    queryBuilder.withHighlightFields(field);
    // 6.聚合
    queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
    // 7.查询
    AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class, new ESSearchResultMapper());

    // 8. 解析结果
    // 获取分页结果
    long total = result.getTotalElements();
    int totalPages = result.getTotalPages();
    List<Product> content = result.getContent();
    System.out.println(total + " " + totalPages);
    content.forEach(System.out::println);
    }