一、Elasticsearch 概述

1.1、Elasticsearch 是什么

image-20210418194013186

​ The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。Elasticsearch,简称为 ES,ES 是一个开源的高扩展的分布式全文搜索引擎,是整个 Elastic Stack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB 级别的数据。

1.2、全文搜索引擎

​ Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。

​ 一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对 SQL 的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。
​ 基于以上原因可以分析得出,在一些生产环境中,使用常规的搜索方式,性能是非常差的:

  • 搜索的数据对象是大量的非结构化的文本数据。
  • 文件记录量达到数十万或数百万个甚至更多。
  • 支持大量基于交互式文本的查询。
  • 需求非常灵活的全文搜索查询。
  • 对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。
  • 对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。为了解决结构化数据搜索和非结构化数据搜索性能问题,我们就需要专业,健壮,强大的全文搜索引擎

​ 这里说到的全文搜索引擎指的是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

1.3、Elasticsearch And Solr

​ Lucene 是 Apache 软件基金会 Jakarta 项目组的一个子项目,提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。但 Lucene 只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来进行应用。

​ 目前市面上流行的搜索引擎软件,主流的就两款:Elasticsearch 和 Solr ,这两款都是基于 Lucene 搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十分类似。

​ 在使用过程中,一般都会将 Elasticsearch 和 Solr 这两个软件对比,然后进行选型。这两个搜索引擎都是流行的,先进的的开源搜索引擎。它们都是围绕核心底层搜索库 - Lucene构建的 - 但它们又是不同的。像所有东西一样,每个都有其优点和缺点:

image-20210418194759873

  • Solr 相比,Elasticsearch 易于安装且非常轻巧。此外,你可以在几分钟内安装并运行 Elasticsearch。但是,如果 Elasticsearch 管理不当,这种易于部署和使用可能会成为一个问题。基于 JSON 的配置很简单,但如果要为文件中的每个配置指定注释,那么它不适合您。总的来说,如果你的应用使用的是 JSON,那么 Elasticsearch 是一个更好的选择。否则,请使用 Solr,因为它的 schema.xml 和 solrconfig.xml 都有很好的文档记录。
  • Solr 拥有更大,更成熟的用户,开发者和贡献者社区。ES 虽拥有的规模较小但活跃的用户社区以及不断增长的贡献者社区。Solr 贡献者和提交者来自许多不同的组织,而 Elasticsearch 提交者来自单个公司
  • Solr 更成熟,但 ES 增长迅速,更稳定。
  • Solr 是一个非常有据可查的产品,具有清晰的示例和 API 用例场景。 Elasticsearch 的文档组织良好,但它缺乏好的示例和清晰的配置说明。

1.4、ES 核心概念介绍

1、集群(Cluster)

一个或者多个安装了 ES 节点的服务器组织在一起,就是集群,这些节点共同持有数据,共同提供搜索服务。

一个集群有一个名字,这个名字是集群的唯一标识,该名字成为 cluster name,默认的集群名称是 elasticsearch,具有相同名称的节点才会组成一个集群。

可以在 config/elasticsearch.yml 文件中配置集群名称:

1
cluster.name: javaboy-es

在集群中,节点的状态有三种:绿色、黄色、红色:

  • 绿色:节点运行状态为健康状态。所有的主分片、副本分片都可以正常工作。
  • 黄色:表示节点的运行状态为警告状态,所有的主分片目前都可以直接运行,但是至少有一个副本分片是不能正常工作的。
  • 红色:表示集群无法正常工作。

2、节点(Node)

集群中的一个服务器就是一个节点,节点中会存储数据,同时参与集群的索引以及搜索功能。一个节点想要加入一个集群,只需要配置一下集群名称即可。默认情况下,如果我们启动了多个节点,多个节点还能够互相发现彼此,那么它们会自动组成一个集群,这是 es 默认提供的,但是这种方式并不可靠,有可能会发生脑裂现象。所以在实际使用中,建议一定手动配置一下集群信息。

3、索引(Index)

索引可以从两方面来理解:

  • 名词

具有相似特征文档的集合。

  • 动词

索引数据以及对数据进行索引操作。

4、类型(Type)

类型是索引上的逻辑分类或者分区。在 es6 之前,一个索引中可以有多个类型,从 es7 开始,一个索引中,只能有一个类型。在 es6.x 中,依然保持了兼容,依然支持单 index 多个 type 结构,但是已经不建议这么使用。

5、文档(Document)

一个可以被索引的数据单元。例如一个用户的文档、一个产品的文档等等。文档都是 JSON 格式的。

6、分片(Shards)

索引都是存储在节点上的,但是受限于节点的空间大小以及数据处理能力,单个节点的处理效果可能不理想,此时我们可以对索引进行分片。当我们创建一个索引的时候,就需要指定分片的数量。每个分片本身也是一个功能完善并且独立的索引。

默认情况下,一个索引会自动创建 1 个分片,并且为每一个分片创建一个副本。

7、副本(Replicas)

副本也就是备份,是对主分片的一个备份。

8、Settings

集群中对索引的定义信息,例如索引的分片数、副本数等等。

9、Mapping

Mapping 保存了定义索引字段的存储类型、分词方式、是否存储等信息。

10、Analyzer 和 Analysis

Analysis 只是一个概念,文本分析是将全文本转换为一系列单词的过程,也叫分词。

Analysis 是通过 analyzer (分词器) 来实现的,可以使用 ES 中自带的分词器,也可以自定义分词器。

除了在数据写入时将词条进行转换,在查询时也可以使用分析器对语句进行分析。

analyzer 由三部分组成,例如有 <p>Hello a world, the world is beautiful</p>;

  1. Character Filter:将文本中的 html 标签剔除掉。
  2. Tokenizer:按照规则进行分词,在英文中按照空格分词。
  3. Token Filter:去掉 stop word(停顿词,a,an,the,is,are等),然后转换为小写。

image-20210419120712729

11、类比

DBMSES
databaseindex
tabletype(7.0后固定为_doc)
RowDocument
ColumnField
Schema(表信息约束)Mapping
SQLDSL

在 ES 7.0前,一个 index 可以创建多个 type,从 7.0 开始,一个索引只能创建一个类型,也就是 _doc

1.5、正排索引和倒排索引

1、正排索引

正排索引就是最普通的索引排序方式。正排索引也是采取key-value pair的方式对数据进行保存,key是doc-id,value则可以存储多种内容,如doc的分词词表、doc所在网页的属性信息等。由此可见,正排索引可以随意添加数据,但如果你要查询某个单词在哪些文档中出现,那么你就不得不将全部文档都遍历一遍,若文档库极大,则时间消耗是不可接受的。

在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合(实际上在搜索引擎索引库中,关键词也已经转换为关键词ID)。例如“文档1”经过分词,提取了20个关键词,每个关键词都会记录它在文档中的出现次数和出现位置。

正排索引一般通过 KEY 去找 VALUE

image-20210418205459644

当用户在主页上搜索关键词“华为手机”时,假设只存在正向索引(forward index),那么就需要扫描索引库中的所有文档,找出所有包含关键词“华为手机”的文档,再根据打分模型进行打分,排出名次后呈现给用户。因为互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。

2、倒排索引

倒排索引是 LuceneElasticSearch 用来做全文检索的标配。倒排索引类似将正排索引反过来,以全部文档中出现的所有words建立一个term dictionary ,然后对于term dictionary 中的每个词,它后面都会跟随一个链表,该链表就是 倒排表倒排表 内存储着如下信息:

  • 该词出现的doc-id
  • 该词在某doc中的出现次数和出现位置

倒排索引以关键词作为主键

image-20210418205610707

1.6、索引基本操作

1、创建索引(PUT 请求)

对比关系型数据库,创建索引就等同于创建数据库。

在 Postman 中,向 ES 服务器发起 PUT 请求,地址为:http://127.0.0.1:9200/shopping

  • 请求结果为:

image-20210418210713192

  • 此时再次发送请求,会提示索引已存在!

image-20210418210916672

2、获取 shopping 索引信息(GET 请求)

发送 GET 请求,请求地址为:http://127.0.0.1:9200/shopping

查看索引向 ES 服务器发送的请求路径和创建索引是一致的。但是HTTP 方法不一致。

3、获取当前所有索引信息(GET 请求)

发送 GET 请求,请求地址为:http://127.0.0.1:9200/_cat/indices?v

这里请求路径中的 _cat 表示查看的意思,indices 表示索引,所以整体含义就是查看当前 ES

服务器中的所有索引,就好像 MySQL 中的 show tables 的感觉,服务器响应结果如下

image-20210418211630448

各参数及含义如下表

表头含义
health当前服务器健康状态:green(集群完整) 、yellow(单点正常、集群不完整) 、red(单点不正常)
status索引打开、关闭状态
index索引名
uuid索引统一编号
pri主分片数量
rep副本数量
docs.count可用文档数量
docs.deleted文档删除状态(逻辑删除)
store.size主分片和副分片整体占空间大小
pri.store.size主分片占空间大小

4、删除单个索引(DELETE 请求)

请求方式为 DELETE,在请求地址栏中输入链接如下:http://localhost:9200/shopping

image-20210418212048172

再次访问 shopping 索引,会提示索引不存在

image-20210418212120103

1.7、文档操作

1、创建文档(POST 请求)

重建 shopping 索引,然后创建文档,添加数据,添加数据的格式为 JSON 格式。

发送 POST 请求,请求地址为:http://localhost:9200/shopping/_doc

其中请求体内容为:

1
2
3
4
5
6
{
"title": "华为手机",
"category": "huawei",
"image": "https://gitee.com/sutianxin/photo/raw/master/20210418212741.png",
"price":3999
}

结果如下

image-20210418212935632

2、在创建文档时自定义文档 id(POST 请求)

请求路径为 POST ,在请求路径中添加自定义的id,请求路径如下

http://localhost:9200/shopping/_doc/1001

image-20210418213516224

3、根据 id 获取文档对象(GET 请求)

发送 GET 请求,请求地址为 http://localhost:9200/shopping/_doc/带查询id,这里我们查询 id 为1001的数据

image-20210418213804078

如果查询一个不存在的数据,那么返回结果的 found 属性会为 false

image-20210418213906787

4、查询索引下的所有文档对象(GET 请求)

发送 GET 请求,请求地址为:http://localhost:9200/索引名称/_search

  • 查询 shopping 索引下的所有文档对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"took": 2383,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "d5Ir5XgBPCVDxZvTxiiG",
"_score": 1.0,
"_source": {
"title": "华为手机",
"category": "huawei",
"image": "https://gitee.com/sutianxin/photo/raw/master/20210418212741.png",
"price": 3999
}
},
{
"_index": "shopping",
"_type": "_doc",
"_id": "1001",
"_score": 1.0,
"_source": {
"title": "华为手机",
"category": "huawei",
"image": "https://gitee.com/sutianxin/photo/raw/master/20210418212741.png",
"price": 3999
}
}
]
}
}

5、文档的全量修改(PUT 请求)

发送 PUT 请求,请求地址为:http://localhost:9200/索引/_doc/要修改的文档id

然后在 Body 中传入新文档的信息,以 JSON 形式传递。

image-20210418214711634

发送请求,查看结果

image-20210418214753942

再次查看该数据

image-20210418214942303

6、文档的局部修改(POST 请求)

发送 POST 请求,请求地址为:http://localhost:9200/索引名/_update/要修改的id,并在 Body 中指定要更新的字段及对应信息.

  • 修改 shopping 索引中 id 为 1001 的字段,将 title 修改为 HUAWEI MATE P40
1
2
3
4
5
{
"doc":{
"title":"HUAWEI MATE P40"
}
}

请求如下

image-20210418215304169

结果如下

image-20210418215318775

7、文档对象的删除(DELETE 请求)

发送 DELETE 请求,请求地址为:http://localhost:9200/索引名/_doc/待删除索引id

  • 删除 id 为 1001 的文档对象

image-20210418220243804

二、ElasticSearch 分词器介绍

2.1、内置分词器

ElasticSearch 核心功能就是数据检索,首先通过索引将文档写入 ES ,查询分析则主要分为两个步骤:

  1. 词条化:分词器将输入文本转换为一个一个的词条流。
  2. 过滤:比如停用词过滤器会从词条中去除不相干的词条;另外还有同义词过滤器,小写过滤器等,ElasticSearch 中内置了多种分词器可供使用。

ES 内置分词器如下:

image-20210420163311390

2.2、中文分词器

Es 中,使用较多的中文分词器是 elasticsearch-analysis-ik,这个是 es 的一个第三方插件,代码托管在 GitHub 上:

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

1、安装方式

  • 首先打开分词器官网:

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

2、测试分词器

  • 创建一个 test 索引
  • 传入文本进行分析
1
2
3
4
5
POST test/_analyze
{
"analyzer": "ik_smart",
"text": "美国留给伊拉克的是一个烂摊子吗"
}
  • 结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
"tokens" : [
{
"token" : "美国",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "留给",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "伊拉克",
"start_offset" : 4,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "的",
"start_offset" : 7,
"end_offset" : 8,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "是",
"start_offset" : 8,
"end_offset" : 9,
"type" : "CN_CHAR",
"position" : 4
},
{
"token" : "一个",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "烂摊子",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "吗",
"start_offset" : 14,
"end_offset" : 15,
"type" : "CN_CHAR",
"position" : 7
}
]
}

3、自定义扩展词库

本地自定义

plugins/ik/config 目录下,新建 ext.dic (文件名任意),在该文件中配置自定义词库

1
芜湖起飞

IKAnalyzer.cfg.xml 中配置自定义词库

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

重启 ES ,测试分词

1
2
3
4
5
POST test/_analyze
{
"analyzer": "ik_smart",
"text": "芜湖起飞肉蛋葱鸡"
}

分词结果

image-20210420182300273

三、字段类型

3.1、核心类型

1、字符串类型

  • string

这是一个已经过期的字符串类型。在 es5 之前,用这个来描述字符串,现在的话,它已经被 text 和 keyword 替代了。

  • text

如果一个字段是要被全文检索的,比如说博客内容、新闻内容、产品描述,那么可以使用 text。

用了 text 之后,字段内容会被分析,在生成倒排索引之前,字符串会被分词器分成一个个词项。

text 类型的字段不用于排序,很少用于聚合。这种字符串也被称为 analyzed 字段。

使用场景:

  1. 存储全文搜索数据, 例如: 邮箱内容、地址、代码块、博客文章内容等。
  2. 默认结合standard analyzer(标准解析器)对文本进行分词、倒排索引。
  3. 默认结合标准分析器进行词命中、词频相关度打分。
  • keyword

这种类型适用于结构化的字段,例如标签、email 地址、手机号码等等,这种类型的字段可以用作过滤、排序、聚合等。

这种字符串也称之为 not-analyzed 字段。

2、数字类型

类型取值范围
long-2^63^ 到 2^63^ - 1
integer-2^31^ 到 2^31^ - 1
short-2^15^ 到 2^15^ - 1
byte-2^7^ 到 2^7^ - 1
double64 位的双精度浮点类型
float32 位的双精度浮点类型
half_float16 位的双精度浮点类型
scaled_float缩放类型的浮点类型

在满足需求的情况下,优先使用范围小的字段。字段长度越短,索引和搜索的效率越高。

浮点数,优先考虑使用 scaled_float。

3、日期类型

由于 JSON 中没有日期类型,所以 es 中的日期类型形式就比较多样:

  • 2020-11-11 或者 2020-11-11 11:11:11
  • 一个从 1970.1.1 零点到现在的一个秒数或者毫秒数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT product/_doc/1
{
"date":"2020-11-11"
}

PUT product/_doc/2
{
"date":"2020-11-11T11:11:11Z"
}


PUT product/_doc/3
{
"date":"1604672099958"
}

以上三个时间都能被解析

4、布尔类型

JSON 中的 “true”、“false”、true、false 都可以。

5、二进制类型

二进制接受的是 base64 编码的字符串,默认不存储,也不可搜索。

6、范围类型

  • integer_range
  • float_range
  • long_range
  • double_range
  • date_range
  • ip_range

定义的时候,指定范围类型即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT product
{
"mappings": {
"properties": {
"date":{
"type": "date"
},
"price":{
"type":"float_range"
}
}
}
}

在插入数据时指定范围,可以使用 gt、gte、lt、lte

1
2
3
4
5
6
7
8
PUT product/_doc/1
{
"date": "2020-11-11",
"price": {
"gt":9,
"lt":11
}
}

3.2、复合类型

1、数组类型

es 中没有专门的数组类型。默认情况下,任何字段都可以有一个或者多个值。

需要注意的是,数组中的元素必须是同一种类型。

添加数组时,数组中的第一个元素决定了整个数组的类型。

2、对象类型

由于 JSON 本身具有层级关系,所以文档包含内部对象。内部对象中,还可以再包含内部对象。

3.3、地理类型

1、使用场景

  • 查找某一个范围内的地理位置
  • 通过地理位置或者相对中心点的距离来聚合文档
  • 把距离整个到文档的评分中
  • 通过距离对文档进行排序

2、geo_point

geo_point 就是一个坐标点,定义方式如下:

1
2
3
4
5
6
7
8
9
10
PUT people
{
"mappings": {
"properties": {
"location":{
"type": "geo_point"
}
}
}
}

创建文档时指定字段类型,存储的时候,有四种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT people/_doc/1
{
"location":{
"lat": 34.27,
"lon": 108.94
}
}

PUT people/_doc/2
{
"location":"34.27,108.94"
}

PUT people/_doc/3
{
// 使用geo_hash
"location":"uzbrgzfxuzup"
}

PUT people/_doc/4
{
"location":[108.94,34.27]
}

注意,使用数组描述,先经度后纬度

  • lat:纬度
  • lon:经度

3.4、特殊类型

1、IP

存储 IP 地址,类型是 ip

1
2
3
4
5
6
7
8
9
10
PUT blog
{
"mappings": {
"properties": {
"remote": {
"type": "ip"
}
}
}
}

添加文档

1
2
3
4
PUT blog/_doc/1
{
"remote":"192.168.91.1"
}

搜索结果

image-20210420190744443

3.5、Mapping 的定义

语法格式如下

1
2
3
4
5
6
PUT 索引名 
{
"mappings" : {
// 定义你的Mapping
}
}

定义mapping的建议方式:写入一个样本文档到临时索引中。此时 ES 会自动生成mapping信息,通过访问 mapping 信息的 API 查询 mapping 定义,修改自动生成的 mapping 成为我们需要的mapping,然后删去原有的索引即可。

建议在 ES 自动生成的基础上修改,ES 的mapping确定后不能修改

  • 创建一个 user 索引,其中 name 为 text 类型,age 为 long 类型,birthday 为 date 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT /user
{
"mappings" : {
"properties" : {
"name": {
"type" : "text"
},
"age": {
"type" : "long"
},
"birthday" : {
"type" : "date"
}
}
}
}

四、搜索

4.1、前期准备

1、创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PUT books
{
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"publish":{
"type": "text",
"analyzer": "ik_max_word"
},
"type":{
"type": "text",
"analyzer": "ik_max_word"
},
"author":{
"type": "keyword"
},
"info":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "double"
}
}
}
}

2、导入数据

执行脚本,导入数据

1
curl -XPOST "http://localhost:9200/books/_bulk?pretty" -H "content-type:application/json" --data-binary @bookdata.json

3、文档保存与搜索

搜索分为两个过程:

  1. 当向索引中保存文档时,默认情况下,es 会保存两份内容,一份是 _source 中的数据,另一份则是通过分词、排序等一系列过程生成的倒排索引文件,倒排索引中保存了词项和文档之间的对应关系。
  2. 搜索时,当 es 接收到用户的搜索请求之后,就会去倒排索引中查询,通过的倒排索引中维护的倒排记录表找到关键词对应的文档集合,然后对文档进行评分、排序、高亮等处理,处理完成后返回文档。

4.2、词项 查询

term 是表达语义的最小单位,在搜索的时候基本都要使用到 term。

term 查询的种类有:Term Query、Range Query 等。

在 ES 中,Term的查询代表完全匹配, Term 查询不会对输入进行分词处理,而是将输入作为一个整体,在创建索引中使用查找准确的词项,我们也可以 Constant Score 将查询转换为一个 filter ,避免算分,利用缓存,提高查询效率。

Term 不会对我们传入的参数做任何处理,而是直接将参数丢进倒排索引库中。

1、term

查询书名中有 无机化学 的书籍, 用于查询的单词不会被进行任何分词处理

1
2
3
4
5
6
7
8
9
GET books/_search
{
"query": {
"term": {
"name": "无机化学"
}
},
"_source":["name"]
}

结果

image-20210420213415175

2、terms

terms 查询和 term 查询机制一样,都不会将指定关键词进行分词,而是直接去分词库中匹配,找到响应文档内容。

terms 是在针对一个字段包含多个值时使用,多个值间以 OR 分隔

  • 查询书籍名称中含有 无机化学 或者 线性代数 的书籍
1
2
3
4
5
6
7
8
9
10
11
12
GET books/_search
{
"query": {
"terms": {
"name": [
"线性代数",
"无机化学"
]
}
},
"_source": ["name"]
}

结果

image-20210420194654481

3、Constant Score

内部包装了过滤查询,故而不会计算相似度分,该查询返回的相似度分与字段上指定boost参数值相同

1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"query": {
"constant_score": {
"filter": {
"term": { "name": "无机化学" }
}
}
},
"_source": ["name"]
}

结果

image-20210420201022240

4、range

range query 中的参数主要有四个:

  1. gt
  2. lt
  3. gte
  4. lte
  • 查询书籍价格在 10 - 20 元之间的书籍
1
2
3
4
5
6
7
8
9
10
11
12
13
GET books/_search
{
"query": {
"range": {
// 进行范围查询的字段,必须为数字类型
"price": {
"gte": 10,
"lte": 20
}
}
},
"_source": ["name","price"]
}

结果(局部)

image-20210420215217404

5、wildcard

wildcard query 即通配符查询。支持单字符和多字符通配符:

  1. ? 表示一个任意字符。
  2. * 表示零个或者多个字符。
  • 查询所有姓张的作者的书
1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"query": {
"wildcard": {
"author": {
"value": "张*"
}
}
},
"_source": ["name","author","price"]
}

结果(局部)

image-20210420220011558

  • 查询作者姓张,且名字只有两个字的书籍
1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"query": {
"wildcard": {
"author": {
"value": "张?"
}
}
},
"_source": ["name","author","price"]
}

结果

image-20210420220422761

6、ids

根据指定的 id 查询

  • 查询id 为 1,2,3的数据
1
2
3
4
5
6
7
8
9
GET books/_search
{
"query": {
"ids": {
"values": [1,2,3]
}
},
"_source": ["name"]
}

结果

image-20210420221807911

4.3、全文查询

全文查询的种类有:Match Query、Match Phrase Query 和 Query String Query 等

索引和搜索时都会进行分词,在查询时,会对输入进行分词,然后每个词项会逐个到底层进行查询,将最终结果进行合并。

1、match_phrase

match_phrase是短语搜索,它会将给定的短语(phrase)当成一个完整的查询条件。

会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。

match_phrase 含有一个 slop 属性

slop 是指关键字之间的最小距离,但是注意不是关键之间间隔的字数。文档中的字段被分词器解析之后,解析出来的词项都包含一个 position 字段表示词项的位置,查询短语分词之后 的 position 之间的间隔要满足 slop 的要求。slop 默认为1

  • 查询名字中带有 十一五化学 的文档
1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"query": {
"match_phrase": {
"name": {
"query": "十一五化学",
"slop": 1
}
}
}
}

此时无法查询到任何结果

image-20210420210141684

  • 我们增大 slop 的值,再次进行查询
1
2
3
4
5
6
7
8
9
10
11
12
GET books/_search
{
"query": {
"match_phrase": {
"name": {
"query": "十一五化学",
"slop": 20
}
}
},
"_source": ["name"]
}

此时查询到 5 条数据

image-20210420210252535

2、multi_match

多条件查询,会对传入的文本进行分词。

  • 查询 name 属性或 info 属性中 匹配 化学无机数学 的文档

进行分词,其实就是查询 name 属性或 info 属性中含有 化学无机数学 的文档

1
2
3
4
5
6
7
8
9
10
GET books/_search
{
"query": {
"multi_match": {
"query": "化学无机数学",
"fields": ["name","info"]
}
},
"_source": ["name","info"]
}

结果

image-20210420202138254

image-20210420202207751

  • 这种查询方式还可以指定字段权重
1
2
3
4
5
6
7
8
9
10
GET books/_search
{
"query": {
"multi_match": {
"query": "化学无机数学",
"fields": ["name","info"]
}
},
"_source": ["name^4","info"]
}

这个表示关键字出现在 name 中的权重是出现在 info 中权重的 4 倍。

3、match

match 会对查询语句进行分词,分词后,如果查询语句中的任何一个词项被匹配,则文档就会被索引到。查询条件相对来说比较宽松。

match 分词后,默认词项之间是 OR 的关系,也就是说,只需要文档中包含一个分词结果,那么就返回文档,可以通过 operator 修改分词后词项之间的关系。

  • 词项间为 OR 的关系
1
2
3
4
5
6
7
8
9
GET books/_search
{
"query": {
"match": {
"name": "计算机化学"
}
},
"_source": ["name"]
}

此时查看结果,发现返回的文档中 name 属性可以只包含 计算机 或只包含 化学

image-20210420204832261

  • 词项间为 AND 的关系
1
2
3
4
5
6
7
8
9
10
11
12
GET books/_search
{
"query": {
"match": {
"name": {
"query": "计算机化学",
"operator": "AND"
}
}
},
"_source": ["name"]
}

此时命中的记录数为0

image-20210420205056950

4.4、simple_query_string 和 query_string

1、query_string

和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。

允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query 一样,支持多字段搜索。

查询 title 中包含 beautiful 和 mind 的所有电影

image-20210419131348485

检索同时包含Token【系统学、es】的文档

1
2
3
4
5
6
7
8
9
GET /tehero_index/_doc/_search
{
"query": {
"query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统学 AND es"
}
}
}

检索包含Token【系统学、es】二者之一的文档

1
2
3
4
5
6
7
8
9
GET /tehero_index/_doc/_search
{
"query": {
"query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统学 OR es"
}
}
}

2、simple_query_string

类似于query_string ,但是会忽略错误的语法,永远不会引发异常,并且会丢弃查询的无效部分。

simple_query_string支持以下特殊字符:

  1. + 表示与运算,相当于query_string 的 AND

  2. | 表示或运算,相当于query_string 的 OR

  3. - 取反单个令牌,相当于query_string 的 NOT

  4. ""表示对检索词进行 match_phrase query

  5. * 字词末尾表示前缀查询

  • + 表示与运算,相当于query_string 的 AND
1
2
3
4
5
6
7
8
9
GET /tehero_index/_doc/_search
{
"query": {
"simple_query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统学 + 间隔"
}
}
}
  • | 表示或运算,相当于query_string 的 OR
1
2
3
4
5
6
7
8
9
GET /tehero_index/_doc/_search
{
"query": {
"simple_query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统学 | 间隔"
}
}
}
  • - 取反单个令牌,相当于query_string 的 NOT
1
2
3
4
5
6
7
8
9
10
GET /tehero_index/_doc/_search
{
"query": {
"simple_query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统学 -间隔",
"default_operator": "and"
}
}
}

注意:参数”default_operator”: “and”。该参数的默认值为or。
上述DSL对应的 sql 语句为:【where Token = 系统学 and Token <> 间隔】

  • ""表示对检索词进行 match_phrase query
1
2
3
4
5
6
7
8
9
GET /tehero_index/_doc/_search
{
"query": {
"simple_query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "\"系统学编程关注\""
}
}
}
  • * 字词末尾表示前缀查询
1
2
3
4
5
6
7
8
9
GET /tehero_index/_doc/_search
{
"query": {
"simple_query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统*"
}
}
}

4.5、fuzzy 模糊查询

在实际搜索中,有时我们可能会打错字,从而导致搜索不到,fuzzy 用于模糊查询,可以自动将拼写错误的搜索文本进行纠正,纠正后去尝试匹配索引中的数据。

fuzzy query 返回与搜索关键字相似的文档。怎么样就算相似?以 LevenShtein 编辑距离为准。编辑距离是指将一个字符变为另一个字符所需要更改字符的次数,更改主要包括四种:

  • 更改字符(javb–〉java)
  • 删除字符(javva–〉java)
  • 插入字符(jaa–〉java)
  • 转置字符(ajva–〉java)
1
2
3
4
5
6
7
8
9
10
11
GET /my_index/my_type/_search
{
"query": {
"fuzzy": {
"属性": {
"value": "要模糊匹配的值",
"fuzziness": 2
}
}
}
}

其中 fuzziness 为调整次数,只能为0、1、2,最多纠正两个错误

  • 传入拼写错误的文本 jaba,查看模糊查询返回的结果
1
2
3
4
5
6
7
8
9
10
11
12
GET books/_search
{
"query": {
"fuzzy": {
"name": {
"value": "jaba",
"fuzziness": 2
}
}
},
"_source": ["name"]
}

返回的结果如下

image-20210420221130345

4.6、特殊查询

1、more_like_this

more_like_this query 可以实现基于内容的推荐,给定一篇文章,可以查询出和该文章相似的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET 要查询的索引/_search
{
"query": {
"more_like_this": {
"fields": [
"FIELD"
],
"like": "text like this one",
"min_term_freq": 1,
"max_query_terms": 12
}
}
}
  1. fields:要匹配的字段,可以有多个
  2. like:要匹配的文本
  3. min_term_freq:词项的最低频率,默认为2,这个指词项在要匹配的文本中的频率,而不是在 ES 文档中的频率
  4. max_query_terms:query 中包含的最大词项数目
  5. min_doc_freq:最小的文档频率,搜索的词,至少在多少个文档中出现,少于指定数目,该词会被忽略
  6. max_doc_freq:最大文档频率
  7. analyzer:分词器,默认使用字段的分词器
  8. stop_words:停用词列表

五、聚合查询

语法格式:

1
2
3
4
5
6
7
8
9
10
GET indexName/_search
{
"aggs": {
"aggs_name" : { # 聚合分析的名字由用户自定义
"aggs_type" : { # 由 es 提供,固定值
// aggregation body
}
}
}
}

ES 中的聚合分析我们主要从两个方面来学习:

  • 指标聚合
  • 桶聚合(类似 group by)

5.1、ES 指标聚合

1、Max Aggregation

统计最大值

  • 查询价格最高的书籍
1
2
3
4
5
6
7
8
9
10
11
12
GET books/_search
{
"size": 0,
"aggs": {
"max_price_book": {
"max": {
"field": "price",
"missing": 1000
}
}
}
}

结果

image-20210421144021453

missing 参数:如果某个文档中缺少 price 字段,则设置该字段的值为 1000。

2、Min Aggregation

统计最小值,用法和 Max Aggregation 基本一致:

1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"aggs": {
"min_price_book": {
"min": {
"field": "price",
"missing": 25
}
}
}
}

结果

image-20210421145212570

3、Avg Aggregation

统计平均值

1
2
3
4
5
6
7
8
9
10
GET books/_search
{
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}

结果

image-20210421145635226

4、Sum Aggregation

求和

1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"size": 0,
"aggs": {
"sum_price": {
"sum": {
"field": "price"
}
}
}
}

结果

image-20210421145711003

5、Cardinality Aggregation

cardinality aggregation 用于基数统计。类似于 SQL 中的 distinct count(0)

  • 查询总共有多少个岗位
1
2
3
4
5
6
7
8
9
10
11
12
# 计算岗位个数
GET employee/_search
{
"size": 0,
"aggs": {
"count_job": {
"cardinality": {
"field": "job"
}
}
}
}

查看结果

image-20210419204556351

6、Stats Aggregation

基本统计,countmaxminavgsum,要求计算属性的类型为数字类型

查询员工工资的信息

1
2
3
4
5
6
7
8
9
10
11
12
# 查询员工工资基本信息
GET employee/_search
{
"size": 0,
"aggs": {
"sal_info": {
"stats": {
"field": "sal"
}
}
}
}

结果

image-20210419213452141

7、Extends Stats Aggregation

高级统计,比 stats 多出来:平方和、方差、标准差、平均值加减两个标准差的区间

1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"size": 0,
"aggs": {
"price_info": {
"extended_stats": {
"field": "price"
}
}
}
}

结果

image-20210421151609265

5.2、桶聚合

1、Terms Aggregation

Terms Aggregation 用于分组聚合,例如,统计各个出版社出版的图书总数量

  • 统计每个职位员工数量
1
2
3
4
5
6
7
8
9
10
11
GET employee/_search
{
"aggs": {
"job_num": {
"terms": {
"field": "job",
"size": 10
}
}
}
}

结果

image-20210421152330207

在 terms 分桶的基础上,还可以对每个桶进行指标聚合

  • 统计每个岗位中员工工资信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET employee/_search
{
"size": 0,
"aggs": {
"job_num": {
"terms": {
"field": "job",
"size": 10
},
"aggs": {
"job_sal_info": {
"stats": {
"field": "sal"
}
}
}
}
}
}

结果

image-20210421153352727

2、Filter Aggregation

过滤器聚合。可以将符合过滤器中条件的文档分到一个桶中,然后可以求其平均值。

  • 例如查询书名中包含 化学 的图书的平均价格:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    GET books/_search
    {
    "size":0,
    "aggs": {
    "java_book_info": {
    "filter": {
    "term": {
    "name": "化学"
    }
    },
    "aggs": {
    "avg_price": {
    "avg": {
    "field": "price"
    }
    }
    }
    }
    }
    }

结果

image-20210421154919595

3、Filters Aggregation

多过滤器聚合。过滤条件可以有多个。

  • 查询书名中含有 java 或者 化学 的书籍,并且平均价格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GET books/_search
{
"size": 0,
"aggs": {
"java_or_huaxue_book": {
"filters": {
"filters": [
{
"term":{
"name":"java"
}
},
{
"term":{
"name":"office"
}
}
]
},
"aggs": {
"avg_book_sal": {
"avg": {
"field": "price"
}
}
}
}
}
}

结果

image-20210421155544449

4、range Aggregation

按照范围聚合,在某一个范围内的文档数统计。

  • 例如统计图书价格在 0-50、50-100、100-150、150以上的图书数量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
GET books/_search
{
"size": 0,
"aggs": {
"book_range_count": {
"range": {
"field": "price",
"ranges": [
{
"key": "0-50的书籍数量",
"to": 51
},
{
"key": "51-100的书籍数量",
"from": 51,
"to": 101
},
{
"key": "100-150的书籍数量",
"from": 101,
"to": 151
},
{
"key": "151后的书籍数量",
"from": 151
}
]
}
}
}
}

结果

image-20210421160255316

5、Range Aggregation

Range Aggregation 也可以用来统计日期,但是也可以使用 Date Range Aggregation,后者的优势在于可以使用日期表达式。

  • 统计两年前到一年后的博客数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET blog/_search
{
"size": 0,
"aggs": {
"near_blog": {
"date_range": {
"field": "date",
"ranges": [
{
"from": "now-24M/M",
"to": "now+1y/y"
}
]
}
}
}
}

结果

image-20210421161616858

  • 12M/M 表示 12 个月。
  • 1y/y 表示 1年。
  • d 表示天

6、Missing Aggregation

空值聚合。

统计所有没有 price 字段的文档:

1
2
3
4
5
6
7
8
9
10
GET books/_search
{
"aggs": {
"NAME": {
"missing": {
"field": "price"
}
}
}
}

结果

image-20210421162631828

7、IP Range Aggregation

IP 地址范围查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET blog/_search
{
"aggs": {
"NAME": {
"ip_range": {
"field": "ip",
"ranges": [
{
"from": "127.0.0.5",
"to": "127.0.0.11"
}
]
}
}
}
}

5.3、聚合过滤问题

1、查询年龄大于30岁的员工的平均工资

先查询出年龄大于30岁的员工,然后进行聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET employee/_search
{
"size": 0,
"query": {
"range": {
"age": {
"gte": 30
}
}
},
"aggs": {
"gt_30_emp_avg_sal": {
"avg": {
"field": "sal"
}
}
}
}

结果

1
2
3
4
5
"aggregations" : {
"gt_30_emp_avg_sal" : {
"value" : 20000.0
}
}

2、查询Java员工的平均工资

先查询出职业为 java 或者 Java 的员工,然后根据 平均工资进行聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET employee/_search
{
"size": 0,
"query": {
"query_string": {
"default_field": "job",
"query": "java OR Java"
}
},
"aggs": {
"java_emp_sal": {
"avg": {
"field": "sal"
}
}
}
}

结果

1
2
3
4
5
"aggregations" : {
"java_emp_sal" : {
"value" : 40488.88888888889
}
}

5.4、高亮显示

日常生活中我们使用搜索工具尝试查询一些信息的时候,常常可以看到返回的结果集中和我们查询条件相符合的字段被特殊的颜色所标记,这就是结果高亮显示。通过高亮显示用户可以明显的发现查询匹配的位置,

ES使用highlight来实现搜索结果中一个或多个字段突出显示。

highlight 的层级与 query 同级,语法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET employee/_search
{
"query": {
"query_string": {
"default_field": "job",
"query": "java OR Java"
}
},
"highlight": {
"fields": {
// 要高亮的字段名,可以有多个
"name": {},
"job": {}
}
}
}

结果

image-20210419234321989

我们可以自定义高亮所用的标签,使用 pre_tags 定义前置标签, post_tags 定义后置标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET employee/_search
{
"query": {
"query_string": {
"default_field": "job",
"query": "java OR Java"
}
},
"highlight": {
"pre_tags": "<span>",
"post_tags": "</span>",
"fields": {
"name": {},
"job": {}
}
}
}

结果

image-20210419234430358

可以单独定义 job 的前置后置标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET employee/_search
{
"query": {
"query_string": {
"default_field": "job",
"query": "java OR Java"
}
},
"highlight": {
"pre_tags": "<span>",
"post_tags": "</span>",
"fields": {
"name": {},
"job": {
"pre_tags": "<em>",
"post_tags": "</em>",
}
}
}
}

六、Spring Data Elasticsearch

6.1、简介

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

Spring Data的官网:http://projects.spring.io/spring-data/

image-20210421190546838

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

image-20210421191146844

特征:

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

6.2、前期准备

1、使用 Spring Initializer 创建一个项目

pom.xml 文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hzx</groupId>
<artifactId>elasticsearch</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elasticsearch</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

2、核心配置文件

1
2
3
4
5
6
7
8
server:
port: 8080
spring:
application:
name: spring-boot-es-demo
elasticsearch:
rest:
uris: http://127.0.0.1:9200

3、创建索引,导入数据

这里以上面的 books 索引及其数据为例。

6.3、实体类及注解

1、实体类

1
2
3
4
5
6
7
8
9
10
@Data
public class Book {
private Long id;
private String author;
private String info;
private String name;
private String type;
private String publish;
private Double price;
}

2、注解

Spring Data 通过注解来声明字段的映射属性,有下面三个注解

  • @Document

作用在类上,标记这个实体类为文档对象,一般有下面四个属性

  1. indexName:对应 ES 中索引库的名称
  2. type:对应在索引库中的类型,在 ES 7后,这个属性已过时,因为 ES 7中只有一个类型,即 doc
  3. shards:分片数量,默认5
  4. replicas:副本数量,默认1
  • @Id

作用在成员变量,标记一个字段作为id主键

  • @Field

作用在成员变量,标记为文档的字段,并指定字段映射属性

  1. type:字段类型,取值是枚举:FieldType
  2. index:是否索引,布尔类型,默认是true
  3. store:是否存储,布尔类型,默认是false
  4. analyzer:分词器名称:ik_max_word,也可以是 ik_smart

3、完整实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@Document(indexName = "book")
public class Book {
@Id
private Long id;
@Field(type = FieldType.Keyword,analyzer = "ik_max_word")
private String author;
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String info;
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String type;
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String publish;
@Field(type = FieldType.Double)
private Double price;
}

6.4、在测试类中使用 ElasticsearchRestTemplate 进行索引操作

1、说明

在新版本的 Spring Data Elasticsearch 中,ElasticsearchTemplate 已被弃用,我们可以使用 ElasticsearchRestTemplate 操作索引。

2、创建索引及映射

ElasticsearchTemplate 中创建索引与映射关系的方法已经过时,查看 ElasticsearchTemplate 源码可以找到替代方法

image-20210421211742915

进入 IndexOperations 接口,可以看到方法如下

image-20210421211843649

创建索引映射,使用 IndexOperations 接口中的 createMapping 方法

  • 通过与该 IndexOperations 接口绑定的实体类创建
1
2
3
4
5
/**
* Creates the index mapping for the entity this IndexOperations is bound to.
* @return mapping object
*/
Document createMapping();
  • 在创建 IndexOperations 对象时进行实体类绑定
1
IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(Book.class);
  • 传入一个实体类的 class 属性进行创建
1
2
3
4
5
6
/**
* Creates the index mapping for the given class
* @param clazz the clazz to create a mapping for
* @return mapping object
*/
Document createMapping(Class<?> clazz);
  • 创建索引的方法,直接使用 IndexOperations 的 create 方法即可。
1
indexOperations.create();

测试

1
2
3
4
5
6
7
@Test
void createIndex() {
IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(Book.class);
// 创建/更新索引
System.out.println(indexOperations.createMapping());
System.out.println(indexOperations.create());
}

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
MapDocument@?#? 
{
"properties": {
"author": {
"type": "keyword",
"analyzer": "ik_max_word"
},
"info": {
"type": "text",
"analyzer": "ik_max_word"
},
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"type": {
"type": "text",
"analyzer": "ik_max_word"
},
"publish": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "double"
}
}
}
true

此时在 kibana 中看到了新创建的索引

image-20210421213319788

3、判断索引是否存在

方法同样存在于 IndexOperations 接口中

1
2
3
4
5
6
/**
* Checks if the index this IndexOperations is bound to exists
*
* @return {@literal true} if the index exists
*/
boolean exists();

测试

1
2
3
4
5
@Test
void createExist() {
IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(Book.class);
System.out.println("book 索引是否存在:" + indexOperations.exists());
}

结果

image-20210421213616776

4、删除索引

方法存在于 IndexOperations 接口中,删除与该 IndexOperations 接口绑定的实体类对应的索引。

1
2
3
4
5
/**
* Deletes the index this {@link IndexOperations} is bound to
* @return {@literal true} if the index was deleted
*/
boolean delete();

测试

1
2
3
4
5
@Test
void createExist() {
IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(Book.class);
System.out.println("book 索引是否存在:" + indexOperations.exists());
}

结果

image-20210421214055808

6.5、创建接口己成 ElasticsearchRepository 实现文档操作

我们需要定义一个 接口,这个接口继承 ElasticsearchRepository,然后就可以实现对文档的基础 CRUD 操作

ElasticsearchRepository 中有两个泛型,第一个为操作的实体类,第二个为实体类主键 ID 类型

image-20210421215119613

1、创建一个接口继承 ElasticsearchRepository

这样我们就获得了对文档进行基础 CRUD 的功能

1
2
public interface BookRepository extends ElasticsearchRepository<Book,Long> {
}

2、测试插入文档数据

使用 ElasticsearchRepository 接口中的 save 方法

image-20210421220556118

测试

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testAddDocument() {
Book book = new Book();
book.setId(1L)
.setAuthor("罗贯中")
.setInfo("《三国演义》是中国文学史上第一部章回小说,是历史演义小说的开山之作,也是第一部文人长篇小说,被列为中国古典四大名著和六大名著之一。明清时期甚至有“第一才子书”之称。")
.setPrice(28.90)
.setType("古典小说")
.setPublish("广东工业大学出版社")
.setName("三国演义");
bookRepository.save(book);
}

结果

image-20210421221429428

3、测试更新文档信息

同样使用 save 方法,当一条数据在索引中已经存在时,再次调用 save 方法即为更新

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testUpdateDocument() {
Book book = new Book();
book.setId(1L)
.setAuthor("罗贯中")
.setInfo("《三国演义》是中国文学史上第一部章回小说,是历史演义小说的开山之作,也是第一部文人长篇小说,被列为中国古典四大名著和六大名著之一。明清时期甚至有“第一才子书”之称。")
.setPrice(128.90)
.setType("古典小说")
.setPublish("广东工业大学出版社")
.setName("《三国演义》");
bookRepository.save(book);
}

查看结果

image-20210421222656611

4、删除文档

有两个方法

  1. 传入待删除的文档对象 id
  2. 传入待删除文档对象
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Deletes the entity with the given id.
* @param id must not be {@literal null}.
* @throws IllegalArgumentException in case the given {@literal id} is {@literal null}
*/
void deleteById(ID id);

/**
* Deletes a given entity.
* @param entity must not be {@literal null}.
* @throws IllegalArgumentException in case the given entity is {@literal null}.
*/
void delete(T entity);

删除 id 为一的文档对象

1
2
3
4
@Test
void testDeleteDocument() {
bookRepository.deleteById(1L);
}

结果,此时索引中不存在任何数据

image-20210421223504056

5、批量插入

使用 ElasticsearchRepository 接口中的 saveAll 方法

1
2
3
4
5
6
7
8
9
10
/**
* Saves all given entities.
*
* @param entities must not be {@literal null} nor must it contain {@literal null}.
* @return the saved entities; will never be {@literal null}. The returned {@literal Iterable} will have the same size
* as the {@literal Iterable} passed as an argument.
* @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is
* {@literal null}.
*/
<S extends T> Iterable<S> saveAll(Iterable<S> entities);

进行批量插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
void testBatchInsertDocument() {
Book book = new Book();
book.setId(1L)
.setAuthor("罗贯中")
.setInfo("《三国演义》是中国文学史上第一部章回小说,是历史演义小说的开山之作,也是第一部文人长篇小说,被列为中国古典四大名著和六大名著之一。明清时期甚至有“第一才子书”之称。")
.setPrice(28.90)
.setType("古典小说")
.setPublish("广东工业大学出版社")
.setName("三国演义");
Book book2 = new Book();
book2.setId(2L)
.setAuthor("罗贯中123")
.setInfo("《三国演义》是中国文学史上第一部章回小说,是历史演义小说的开山之作,也是第一部文人长篇小说,被列为中国古典四大名著和六大名著之一。明清时期甚至有“第一才子书”之称。")
.setPrice(1128.90)
.setType("古典小说")
.setPublish("广东工业大学出版社")
.setName("《三国演义》");
List<Book> list = new ArrayList();
list.add(book);
list.add(book2);
bookRepository.saveAll(list);
}

结果

image-20210421225136184

6.6、数据查询

1、根据 id 查询一个文档对象

使用 ElasticsearchRepositoryfindById 方法,该方法返回一个 Optional 对象,Optional 类中存在一个 value 属性,就是我们要获取的文档对象,可以通过 Optional 对象的 get 方法获取

image-20210421225841544

测试

1
2
3
4
5
@Test
void testFindById() {
Optional<Book> book = bookRepository.findById(1L);
System.out.println("获取到的对象为:" + book.get());
}

查看结果

image-20210421230126598

2、自定义方法

Spring Data 的另一个强大功能,是根据方法名称自动实现功能。

比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。

当然,方法名称要符合一定的约定:

KeywordSampleElasticsearch Query String
AndfindByNameAndPrice{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
OrfindByNameOrPrice{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
IsfindByName{"bool" : {"must" : {"field" : {"name" : "?"}}}}
NotfindByNameNot{"bool" : {"must_not" : {"field" : {"name" : "?"}}}}
BetweenfindByPriceBetween{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
LessThanEqualfindByPriceLessThan{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
GreaterThanEqualfindByPriceGreaterThan{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
BeforefindByPriceBefore{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
AfterfindByPriceAfter{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
LikefindByNameLike{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
StartingWithfindByNameStartingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
EndingWithfindByNameEndingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}}
Contains/ContainingfindByNameContaining{"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}}
InfindByNameIn(Collection<String>names){"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}
NotInfindByNameNotIn(Collection<String>names){"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}
NearfindByStoreNearNot Supported Yet !
TruefindByAvailableTrue{"bool" : {"must" : {"field" : {"available" : true}}}}
FalsefindByAvailableFalse{"bool" : {"must" : {"field" : {"available" : false}}}}
OrderByfindByAvailableTrueOrderByNameDesc{"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

修改实体类中的索引为 books,在我们自定义的接口中编写一个根据价格排序的方法声明

1
2
3
public interface BookRepository extends ElasticsearchRepository<Book,Long> {
List<Book> findByOrderByPriceDesc();
}
  • 编写一个测试方法,获取所有书籍,根据价格排序
1
2
3
4
5
@Test
void testFindOrderByPriceDesc() {
List<Book> books = bookRepository.findByOrderByPriceDesc();
books.forEach(System.out::println);
}

结果(局部)

image-20210421232223882

  • 编写一个方法,返回价格在 100-200 之间的所有书籍

BookRepository

1
2
3
public interface BookRepository extends ElasticsearchRepository<Book,Long> {
List<Book> findByPriceBetween(Double from,Double to);
}

测试方法

1
2
3
4
5
@Test
void testFindPriceBetween() {
List<Book> books = bookRepository.findByPriceBetween(100.0,200.0);
books.stream().map(Book::getPrice).collect(Collectors.toList()).forEach(System.out::println);
}

查看结果,这里只输出价格

image-20210421233102615

6.7、高级查询

虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。

需要 ElasticsearchRestTemplate 对象的 search 方法和 QueryBuilders 对象配合使用

1、QueryBuilders

QueryBuilders 提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等 QueryBuilder 对象。

image-20210421234732846

  1. 构建一个 NativeSearchQueryBuilder 对象
1
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
  1. 调用构造的 NativeSearchQueryBuilder 对象的 withQuery 方法,这个方法接收一个 QueryBuilder 对象,这个QueryBuilder 对象可以通过QueryBuilders 的静态方法获取。

2、使用 match_all 查询全部

1
2
3
4
5
6
7
8
@Test
void testMatchAll() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.matchAllQuery());
SearchHits<Book> hits = elasticsearchRestTemplate.search(queryBuilder.build(), Book.class);
List<Book> books = hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
System.out.println(books.size());
}

结果

image-20210422111218219

3、使用 match 进行查询,查询书名中含有 “无机” 和 “化学” 的书名

1
2
3
4
5
6
7
8
9
@Test
void testMatch() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//queryBuilder.withQuery(QueryBuilders.matchAllQuery());
queryBuilder.withQuery(QueryBuilders.matchQuery("name","无机化学"));
SearchHits<Book> hits = elasticsearchRestTemplate.search(queryBuilder.build(), Book.class);
List<Book> books = hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
System.out.println(books.size());
}

结果

image-20210422111502353

4、分页查询

使用 NativeSearchQueryBuilder 对象的 withPageable 方法,这个方法要传入 Pageable 对象

1
2
3
4
public NativeSearchQueryBuilder withPageable(Pageable pageable) {
this.pageable = pageable;
return this;
}

这个对象可以使用 PageRequest 对象的 of 方法构造,需要传入 page 和 size

1
2
3
public static PageRequest of(int page, int size) {
return of(page, size, Sort.unsorted());
}

查询书名中带有 “无机” “化学” 的前十条数据

1
2
3
4
5
6
7
8
@Test
void testMatchAll() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.matchQuery("name","无机化学"))
.withPageable(PageRequest.of(0,10));
SearchHits<Book> hits = elasticsearchRestTemplate.search(queryBuilder.build(), Book.class);
System.out.println(hits.stream().map(SearchHit::getContent).collect(Collectors.toList()).size());
}

结果

image-20210422113635898

  • 指定要查询的字段和不查询的字段

使用 NativeSearchQueryBuilder 对象的 withSourceFilter 方法,这个方法需要传入一个 FetchSourceFilter 对象,这个对象需要传入两个字符串数组,分别为要返回的属性数组和排除的属性数组

image-20210422114022430

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testSource() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//queryBuilder.withQuery(QueryBuilders.matchAllQuery());
String[] includes = {"name","price","author"};
String[] excludes = {};
queryBuilder.withSourceFilter(new FetchSourceFilter(includes,excludes))
.withQuery(QueryBuilders.matchQuery("name","无机化学"))
.withPageable(PageRequest.of(0,10));
SearchHits<Book> hits = elasticsearchRestTemplate.search(queryBuilder.build(), Book.class);
hits.stream().map(SearchHit::getContent).collect(Collectors.toList()).forEach(System.out::println);
}

结果,可以看到只返回了 includes 数组中的属性

image-20210422114457537

5、范围匹配

使用 NativeSearchQueryBuilder 对象的 withQuery 方法,需要传入一个 RangeQueryBuilder 对象。

  1. 使用 gte/ gt 方法指定下界

  2. 使用 lte / lt方法指定上界

上面两个方法可以配合使用

  1. 使用 from 方法指定下界, to 方法指定上界

上面两个方法只能指定选择一个使用。

  • 查询价格在 100.0 - 200.0 的书籍
1
2
3
4
5
6
7
8
9
@Test
void testRangeQuery() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
String[] includes = {"name","price","author"};
String[] excludes = {};
queryBuilder.withQuery(QueryBuilders.rangeQuery("price").gte(100.0).lte(200.0))
.withSourceFilter(new FetchSourceFilter(includes,excludes));
elasticsearchRestTemplate.search(queryBuilder.build(),Book.class).stream().map(SearchHit::getContent).collect(Collectors.toList()).forEach(System.out::println);
}

结果

image-20210422144141487

6、结果排序

使用 NativeSearchQueryBuilder 对象的 withSort 方法,需要传入一个 SortBuilder 对象。

SortBuilders.fieldSort 表示根据属性排序,这里根据 price 排序,order 方法中传入一个枚举对象,这里 SortOrder.DESC 指降序排序。

1
2
3
4
5
6
7
8
9
10
@Test
void testSortQuery() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
String[] includes = {"name","price","author"};
String[] excludes = {};
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC))
.withPageable(PageRequest.of(0,10))
.withSourceFilter(new FetchSourceFilter(includes,excludes));
elasticsearchRestTemplate.search(queryBuilder.build(),Book.class).stream().map(SearchHit::getContent).collect(Collectors.toList()).forEach(System.out::println);
}

结果

image-20210422145553852

6.8、聚合查询

1、聚合为桶

桶就是分组,比如这里我们按照出版社 price 进行分组:

  1. terms 方法中传入聚合的名称,field 指定要进行桶分组的属性
  2. 使用 withSourceFilter 过滤所有普通结果,当 includes 为空数组时,不返回任何结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
void testAgg() {
String[] includes = {};
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.addAggregation(AggregationBuilders.terms("price_info").field("price"));
queryBuilder.withSourceFilter(new FetchSourceFilter(includes,null));
SearchHits<Book> search = elasticsearchRestTemplate.search(queryBuilder.build(), Book.class);
Terms terms = (Terms) search.getAggregations().asMap().get("price_info");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
buckets.forEach(bucket -> {
System.out.println(bucket.getKeyAsString());
System.out.println(bucket.getDocCount());
});
}

结果

image-20210422152439646

2、嵌套聚合

使用 subAggregation 添加子聚合

然后在遍历中解析子聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testAgg() {
String[] includes = {};
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.addAggregation(AggregationBuilders.terms("price_info").field("price")
.subAggregation(AggregationBuilders.avg("price_avg")).field("price"));
queryBuilder.withSourceFilter(new FetchSourceFilter(includes,null));
SearchHits<Book> search = elasticsearchRestTemplate.search(queryBuilder.build(), Book.class);
Terms terms = (Terms) search.getAggregations().asMap().get("price_info");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
buckets.forEach(bucket -> {
System.out.println(bucket.getKeyAsString());
System.out.println(bucket.getDocCount());
// 解析子聚合
Map<String, Aggregation> aggregationMap = bucket.getAggregations().asMap();
InternalAvg priceAvg = (InternalAvg)aggregationMap.get("price_avg");
System.out.println(priceAvg.getValue());
});
}