
00 ElasticSearch 笔记
ElasticSearch
ElasticSearch 的概述
ElasticSearch 是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容,是一种 分布式搜索引擎
ElasticSearch 结合kibana、Logstash、Beats,也就是 Elastic Stack(ELK)。被广泛应用在 日志数据分析、实时监控 等领域

- ElasticSearch 是 Elastic Stack 的核心,负责存储、搜索、分析数据
ElasticSearch 底层是 基于 lucene 来实现的,官网地址: https://www.elastic.co/cn/
- Lucene 是一个Java语言的搜索引擎类库,是 Apache 公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/
- Lucene 的优势:
- 易扩展
- 高性能(基于倒排索引)
- Lucene 的缺点:
- 只限于Java语言开发
- 学习曲线陡峭
- 不支持水平扩展
相比于 lucene,elasticsearch优势有:
- 支持分布式,可水平扩展
- 提供 Restful 接口,可被任何语言调用
搜索引擎技术排名:
- Elasticsearch:开源的 分布式 搜索引擎
- Splunk:商业项目
- Solr:Apache的开源搜索引擎
倒排索引
倒排索引的概念 是 基于MySQL这样的正向索引 而言的
正向索引:

-
如果是根据 id 查询,那么直接走索引,查询速度非常快
-
如果是基于 title 做模糊查询,只能是 逐行扫描 数据,流程如下:
- 用户搜索数据,条件是 title 符合
"%手机%" - 逐行获取数据,比如id为1的数据
- 判断数据中的 title 是否符合用户搜索条件
- 如果符合则放入结果集,不符合则丢弃。回到步骤1
- 用户搜索数据,条件是 title 符合
倒排索引:
倒排索引中有两个非常重要的概念:
- 文档(
Document):用来搜索的数据,其中的 每一条数据就是一个文档。例如一个网页、一个商品信息 - 词条(
Term):文档按照语义分为词语。对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引 是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档 id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如 hash 表结构索引

eg:
倒排索引的 搜索流程 如下(以搜索"华为手机"为例):
1)用户输入条件"华为手机"进行搜索。
2)对用户输入内容分词,得到词条:华为、手机。
3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。

总结:
-
正向索引 是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是 根据文档找词条的过程。
-
而 倒排索引 则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的 id,然后根据 id 获取文档。是 根据词条找文档的过程。
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据 索引字段 搜索、排序速度非常快
- 缺点:
- 根据 非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
ElasticSearch 的概念
文档:
ElasticSearch是面向 文档(Document) 存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为 json 格式后存储在 ElasticSearch 中,一条数据就是一个文档,ES 中是 Json 格式

字段:Json文档中往往包含很多的 字段(Field),类似于数据库中的列,就是 Json 文档中的字段
索引(Index):就是 相同类型 的文档的集合
映射(mapping):是 索引中文档的字段约束信息,类似 表的结构约束,比如字段名称、类型等

MySQL vs ES:
| MySQL | Elasticsearch | 说明 |
|---|---|---|
| Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
| Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
| SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
-
MySQL:擅长 事务类型 操作,可以确保数据的安全和一致性
-
Elasticsearch:擅长 海量数据 的搜索、分析、计算
在企业中,往往是两者结合使用:
- 对 安全性要求较高 的 写 操作,使用 mysql 实现
- 对 查询性能要求较高 的 搜索 需求,使用 elasticsearch 实现
- 两者再基于某种方式,实现数据的同步,保证一致性

安装ES、Kibana
ES 安装流程:
-
因为要部署 kibana 容器,因此需要让 es 和 kibana 容器互联。所以这里先创建一个网络
1
docker network create es-net
-
运行 docker 命令,部署单点 es
1
2
3
4
5
6
7
8
9
10
11
12
13
14拉取资源
docker pull elasticsearch:7.12.1
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1-e "cluster.name=es-docker-cluster":设置集群名称-e "http.host=0.0.0.0":监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小-e "discovery.type=single-node":非集群模式-v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录--privileged:授予逻辑卷访问权--network es-net:加入一个名为es-net的网络中-p 9200:9200:端口映射配置
-
在浏览器中输入:http://192.168.88.129:9200 即可看到elasticsearch的响应结果
安装 kibana 流程:
-
拉取资源
1
docker pull kibana:7.12.1
-
运行 docker 命令,部署 kibana
1
2
3
4
5
6docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1--network es-net:加入一个名为 es-net 的网络中,与 elasticsearch 在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200":设置 elasticsearch 的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch-p 5601:5601:端口映射配置
-
kibana 启动一般比较慢,需要多等待一会,可以通过命令,并查看运行日志
1
docker logs -f kibana
-
在浏览器输入地址访问:http://192.168.88.129:5601,即可看到结果

分词器
分词器的作用:
- 创建 倒排索引 时对 文档分词
- 用户搜索时,对输入的内容分词
es 在创建 倒排索引 时需要对文档分词,在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好
eg:
1 | POST /_analyze |
- 语法说明:
POST:请求方式/_analyze:请求路径,这里省略了 http://192.168.88.129:9200,有 kibana 来补充请求参数- json 风格:
analyzer:分词器类型,这里是默认的 standard 分词器text:文本,就是要分词的内容
- json 风格:
分词结果:
1 | { |
因为只能对中文逐字分词,不是很友好,所以不能使用默认的中文分词器
所以,处理中文分词,一般会使用 IK分词器,官方地址:https://github.com/medcl/elasticsearch-analysis-ik,按照官方地址安装即可
- ik分词器 包含两种 模式:
- ik_smart:智能切分,粗粒度,占用内存较少
- ik_max_word:最细切分(分词更详细),细粒度,占用内存更多
eg:
1 | POST /_analyze |
分词结果:
1 | { |
ik分词器 - 拓展词库
要拓展 ik 分词器的词库,只需要修改一个 ik 分词器目录中的 config 目录中的 IkAnalyzer.cfg.xml 文件:
1 |
|
然后在名为 ext.dic 的文件中,添加想要拓展的词语即可
ik分词器 - 停用词库
要禁用某些敏感词条,只需要修改一个 ik 分词器目录中的 config 目录中的 IkAnalyzer.cfg.xml 文件:
1 |
|
然后在名为 stopword.dic 的文件中,添加想要拓展的词语即可
索引库操作
索引库就类似数据库表,mapping 映射就类似 表的结构。要向 es 中存储数据,必须先创建 “库” 和 “表”
mapping 属性
mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
- type:字段数据类型
- 常见的简单类型有:
- 字符串:
text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址) - 数值:
long、integer、short、byte、double、float - 布尔:
boolean - 日期:
date - 对象:
object
- 字符串:
- 常见的简单类型有:
- index:是否 创建索引,**默认 **为
true - analyzer:使用 哪种分词器,值为分词器的名称,eg:ik_smart、standard
- properties:该字段的 子字段
索引库的 CRUD 操作
ES 中通过 Restful 请求操作索引库、文档。请求内容用 DSL 语句来表示。
创建索引库
创建 索引库 和 mapping 的 DSL 语法 如下:
1 | PUT /索引库名称 |
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
eg:
需要分词的json文档:
1 | { |
创建索引库:
1 | PUT /heisenberg |
查看索引库
1 | GET /索引库名称 |
eg:
1 | GET /heisenberg |
删除索引库
1 | DELETE /索引库名称 |
eg:
1 | DELETE /heisenberg |
修改索引库
倒排索引 结构虽然 不复杂,但是一旦数据结构改变(比如改变了分词器),就需要 重新创建 倒排索引,这简直是灾难。因此索引库 一旦创建,无法修改mapping
虽然无法修改 mapping 中已有的字段,但是却 允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响
1 | PUT /索引库名/_mapping |
总结:
- 创建 索引库:
PUT /索引库名 - 查询 索引库:
GET /索引库名 - 删除 索引库:
DELETE /索引库名 - 添加字段:
PUT /索引库名/_mapping
文档操作
索引库 相当于 数据库的表,文档 相当于 一条数据
新增文档
语法:
1 | POST /索引库名/_doc/文档id |
eg:
1 | # 创建文档 |
查询文档
根据 Rest 风格,新增是 post,查询应该是 get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
1 | GET /{索引库名称}/_doc/{id} |
eg:
通过kibana查看数据:
1 | GET /heisenberg/_doc/1 |
删除文档
删除使用 DELETE 请求,同样,需要根据 id 进行删除:
语法:
1 | DELETE /{索引库名}/_doc/id值 |
eg:
1 | # 根据id删除数据 |
修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
全量修改
全量修改是 覆盖原来的文档,其本质是:
- 根据指定的 id 删除文档
- 新增一个相同 id 的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
这种方式既能 修改 又能 新增
语法:
1 | PUT /{索引库名}/_doc/文档id |
eg:
1 | PUT /heisenberg/_doc/1 |
增量修改
增量修改是 只修改指定 id 匹配的文档中的 部分字段。
语法:
1 | POST /{索引库名}/_update/文档id |
示例:
1 | POST /heisenberg/_update/1 |
总结:
文档操作有:
- 创建文档:
POST /{索引库名}/_doc/文档id { json文档 } - 查询文档:
GET /{索引库名}/_doc/文档id - 删除文档:
DELETE /{索引库名}/_doc/文档id - 修改文档:
- 全量修改:
PUT /{索引库名}/_doc/文档id { json文档 } - 增量修改:
POST /{索引库名}/_update/文档id { "doc": {字段}}
- 全量修改:
RestClient(索引库、文档)
RestClient 概述
ES 官方提供了各种不同语言的客户端,用来操作 ES。这些客户端的本质就是 组装 DSL 语句,通过 http 请求发送给 ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的 Java Rest Client 又包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client
RestClient 操作索引库
利用 JavaRestClient 实现 创建、删除 索引库,判断 索引库 是否存在
根据 酒店数据 创建 索引库,索引库名为 hotel,mapping 属性根据 数据库结构 定义
基本步骤:
- 创建分析数据结构
- 定义 mapping 属性
- 初始化 JavaRestClient
- 利用 JavaRestClient 创建索引库
- 利用 JavaRestClient 删除索引库
- 利用 JavaRestClient 判断索引库是否存在
mapping 映射分析
创建索引库,最关键的是 mapping 映射,而 mapping 映射要考虑的信息包括:
- 字段名(可以根据数据库表定义)
- 字段数据类型(可以根据数据库表定义)
- 是否参与搜索
- 是否需要分词
其中:
- 字段名、字段数据类型,可以参考数据表结构的名称和类型
- 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
- 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
- 分词器,我们可以统一使用 ik_max_word
eg:
表的结构:

对应的 mapping 映射为:
1 | PUT /hotel |
ES 中支持 两种地理坐标数据类型:
geo_point:由纬度(latitude)和经度(longitude)确定的 一个点。例如:“32.8752345, 120.2981576”geo_shape:有多个 geo_point 组成的复杂几何图形。例如 一条直线,“LINESTRING (-77.03653 38.897676, -77.009051 38.889939)”
字段拷贝可以使用 copy_to 属性将 当前字段 拷贝到 指定字段
初始化 JavaRestClient
流程:
-
引入 es 的 RestHighLevelClient 依赖
1
2
3
4<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency> -
因为 SpringBoot 默认的 ES 版本是 7.6.2,所以需要覆盖默认的 ES 版本
1
2
3
4<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties> -
初始化 RestHighLevelClient,创建一个测试类验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class HotelIndexTest {
private RestHighLevelClient restHighLevelClient;
// 客户端初始化
void setUp() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.88.129:9200")
));
}
// 关闭客户端
void tearDown () throws IOException {
this.restHighLevelClient.close();
}
}
创建 索引库

1 | public class HotelIndexTest { |
- 创建 Request 对象。因为是创建索引库的操作,因此 Request 是 CreateIndexRequest
- 添加请求参数,其实就是 DSL 的 JSON 参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅
- 发送请求,client.indices() 方法的返回值是 IndicesClient 类型,封装了所有与索引库操作有关的方法
删除 索引库
1 |
|
判断 索引库是否存在
1 |
|
总结:
JavaRestClient 操作 elasticsearch 的流程基本类似。核心是 client.indices() 方法来获取索引库的操作对象。
索引库操作的基本步骤:
- 初始化 RestHighLevelClient
- 创建 XxxIndexRequest。XXX 是 Create、Get 、Delete
- 准备 DSL( Create 时需要,其它是无参)
- 发送请求。调用 RestHighLevelClient#indices().xxx() 方法,xxx 是 create、exists 、delete
RestClient 操作文档
去数据库查询酒店数据,导入到 hotel 索引库,实现酒店数据的CRUD。
基本步骤如下:
- 初始化 JavaRestClient(这步同上)
- 利用 JavaRestClient 新增酒店数据
- 利用 JavaRestClient 根据id查询酒店数据
- 利用 JavaRestClient 删除酒店数据
- 利用 JavaRestClient 修改酒店数据
新增 文档

1 |
|
- 创建 Request 对象
- 准备请求参数,也就是DSL中的JSON文档
- 发送请求
变化的地方在于,这里直接使用 client.xxx() 的API,不再需要 client.indices() 了
查询 文档

可以看到,结果是一个 JSON,其中文档放在一个 _source 属性中,因此解析就是拿到 _source,反序列化为 Java对象 即可。
与之前类似,也是三步走:
- 准备 Request 对象。这次是查询,所以是 GetRequest
- 发送请求,得到结果。因为是查询,这里调用 client.get() 方法
- 解析结果,就是对 JSON 做反序列化
1 |
|
修改 文档
这里做 局部更新 的方法

与之前类似,也是三步走:
- 准备 Request 对象。这次是修改,所以是 UpdateRequest
- 准备参数。也就是 JSON 文档,里面包含要修改的字段
- 更新文档。这里调用 client.update() 方法
1 |
|
删除 文档
1 |
|
- 准备 Request 对象,因为是删除,这次是 DeleteRequest 对象。要指定索引库名和 id
- 准备参数,无参
- 发送请求。因为是删除,所以是 client.delete() 方法
批量导入文档
案例需求:利用 BulkRequest 批量将数据库数据导入到索引库中。
步骤如下:
-
利用 mybatis-plus 查询酒店数据
-
将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
-
利用 JavaRestClient 中的 BulkRequest 批处理,实现批量新增文档
批量处理 BulkRequest,其本质就是将多个普通的 CRUD 请求组合在一起发送,能添加的请求包括:
- IndexRequest:就是 新增
- UpdateRequest:就是 修改
- DeleteRequest:就是 删除
1 |
|
其实还是三步走:
- 创建 Request 对象。这里是 BulkRequest
- 准备参数。批处理的参数,就是其它 Request 对象,这里就是多个 IndexRequest
- 发起请求。这里是批处理,调用的方法为 client.bulk() 方法
执行批量查询的指令:
1 | GET /hotel/_search |
总结:
文档操作的基本步骤:
- 初始化 RestHighLevelClient
- 创建 XxxRequest。XXX是 Index、Get 、Update 、Delete 、Bulk
- 准备参数(Index、Update、Bulk 时需要)
- 发送请求。调用 RestHighLevelClient#.xxx() 方法,xxx是 index、get 、update 、delete 、bulk
- 解析结果(Get 时需要)
DSL 查询文档
DSL 查询分类
Elasticsearch 提供了基于 JSON 的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
-
查询所有:查询出所有数据,一般测试用。例如:
match_all -
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去 倒排索引库 中匹配。例如:
match_querymulti_match_query
-
精确查询:根据精确词条值查找数据,一般是查找 keyword、数值 、日期 、boolean 等类型字段,这些词条不需要分词,直接去 倒排索引库 中匹配。例如:
idsrangeterm
-
地理(geo)查询:根据 经纬度 查询。例如:
geo_distancegeo_bounding_box
-
复合(compound)查询:复合查询可以 将上述各种查询条件组合 起来,合并查询条件。例如:
boolfunction_score
DSL 查询语法
基本查询语法
查询的语法基本一致:
1 | GET /indexName/_search |
eg:
以查询所有为例,其中:
- 查询类型为 match_all
- 没有查询条件
1 | // 查询所有 |
全文检索查询
全文检索查询的基本流程如下:
- 对用户搜索的内容做 分词,得到词条
- 根据词条去 倒排索引库 中匹配,得到文档 id
- 根据文档 id 找到文档,返回给用户
比较常用的场景包括:
- 商城的输入框搜索
- 百度输入框搜索
基本语法:
常见的全文检索查询包括:
match查询:单字段查询multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
match 查询语法 如下:
1 | GET /indexName/_search |
mulit_match 语法 如下:
1 | GET /indexName/_search |
eg:
1 | # match 查询 |

1 | #muti_match 查询 |

- 将brand、name、business值都利用copy_to复制到了all 字段中。因此根据三个字段搜索,和根据all字段搜索效果当然一样了。
- 但是,搜索字段越多,对查询性能影响越大,因此 建议采用copy_to,然后单字段查询的方式
总结:
match 和 multi_match 的区别是什么:
- match:根据一个字段查询
- multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
精确查询
精确查询一般是查找 keyword、数值、日期、boolean 等类型字段。所以 不会 对搜索条件分词。常见的有:
- term:根据 词条精确值 查询
- range:根据 值的范围 查询
term 查询:
精确查询 的字段搜是 不分词 的字段,因此查询的条件也必须是 不分词 的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据
基本语法:
1 | // term查询 |
eg:
1 | # term 查询 |

range查询:
范围查询,一般应用在 对数值类型做范围过滤 的时候。比如做 价格范围过滤。
基本语法:
1 | // range查询 |
eg:
1 | # range 查询 |

精确查询常见的有:
- term 查询:根据词条精确匹配,一般搜索 keyword类型、数值类型、布尔类型、日期类型 字段
- range 查询:根据数值范围查询,可以是数值、日期的范围
地理查询
地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
常见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
- 微信:搜索我附近的人
矩形范围 查询
矩形范围查询,也就是 geo_bounding_box 查询,查询坐标落在某个矩形范围的所有文档
查询时,需要指定矩形的 左上、**右下 **两个点的坐标,然后 画出一个矩形,落在该矩形内的都是符合条件的点
基本语法:
1 | // geo_bounding_box查询 |
附近查询
附近查询,也叫做距离查询 geo_distance :查询到指定中心点小于某个距离值的所有文档。
也就是说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件
基本语法:
1 | // geo_distance 查询 |
eg:
1 | # distance 查询 |

复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
相关性算分:
当利用 match 查询时,文档结果 会根据与搜索词条的关联度打分(_score),返回结果时 按照分值降序排列
在 elasticsearch 中,早期使用的打分算法是 TF-IDF 算法,公式如下:

在后来的5.1版本升级中,elasticsearch 将算法改进为 BM25 算法,公式如下:

eg:
搜索 “虹桥如家”,打分情况:
1 | [ |
总结:
elasticsearch 会根据 词条 和 文档的相关度 做打分,算法由两种:
- TF-IDF :在 elasticsearch5.0 之前,会随着 词频增加 而 越来越大
- BM25 :在 elasticsearch5.0 之后,会随着 词频增加 而 增大,但增长曲线会趋于水平
Function Score Query:
使用 function score query,可以 修改文档的相关性算分(query score),根据新得到的算分排序

function score 查询中包含四部分内容:
- **原始查询 **条件:query 部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- 过滤条件:filter 部分,符合该条件的文档才会重新算分
- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的 函数算分(function score),有四种函数:
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法
- 加权模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用function score替换query score
- 其它,例如:sum、avg、max、min
function score 的运行流程如下:
- 根据 原始条件 查询搜索文档,并且计算相关性算分,称为 原始算分(query score)
- 根据 过滤条件,过滤文档
- 符合 过滤条件 的文档,基于 算分函数 运算,得到 函数算分(function score)
- 将 原始算分(query score)和 函数算分(function score)基于 运算模式 做运算,得到最终结果,作为相关性算分。
其中的关键点是:
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 加权模式:决定最终算分结果
基本语法:
1 | GET /hotel/_search |
- 原始条件:不确定,可以任意变化
- 过滤条件:brand = “如家”
- 算分函数:可以简单粗暴,直接给固定的算分结果,weight
- 运算模式:比如求和
eg:
1 | # function score 查询 |

总结:
function score query定义的三要素是:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算 function score
- 加权方式:function score 与 query score 如何运算
Boolean Query:
布尔查询 是 一个或多个查询子句的组合,每一个子句就是一个 子查询。子查询的组合方式有:
must:必须匹配每个子查询,类似 “与”should:选择性匹配子查询,类似 “或”must_not:必须不匹配,不参与算分,类似“非”filter:必须匹配,不参与算分
语法举例:
1 | GET /hotel/_search |
搜索时,参与 打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:
- 搜索框的关键字搜索,是全文检索查询,使用 must 查询,参与算分
- 其它过滤条件,采用filter查询。不参与算分
eg:
搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
分析:
- 名称搜索,属于全文检索查询,应该参与算分。放到must中
- 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
- 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中
1 | # bool查询 |

搜索结果处理
排序
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索 结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等
普通字段排序
keyword、数值、日期类型 排序的语法基本一致
基本语法:
1 | GET /indexName/_search |
- 排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
eg:
需求描述:酒店数据按照用户评价(score) 降序排序,评价相同的按照价格 (price) 升序排序
1 | GET /hotel/_search |
地理坐标排序
地理坐标排序略有不同
基本语法:
1 | GET /indexName/_search |
这个查询的含义是:
- 指定一个坐标,作为目标点
- 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
- 根据距离排序
eg:
需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序
提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
1 | GET /hotel/_search |
分页
elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改 from、size 参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
类似于mysql中的 limit ?, ?
基本分页
分页的基本语法:
1 | GET /hotel/_search |
深度分页
如果要查询 990~1000 的数据,在DSL语句中,from设为990,size设为10,即查询990开始的数据,也就是 第990~第1000条数据。但是在 elasticsearch 内部分页时,必须先查询 0~1000 条,然后截取其中的 990 ~ 1000 的这 10 条。如果 es 是单点模式,这并无太大影响,但是 elasticsearch 将来一定是集群,例如我集群有5个节点,要查询TOP1000的数据,并不是每个节点查询200条就可以了。因此要想获取整个集群TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此 elasticsearch 会禁止 from + size 超过 10000 的请求
针对深度分页,ES提供了两种解决方案,官方文档:
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式scroll:原理将排序后的文档 id 形成快照,保存在内存。官方已经不推荐使用
总结:
分页查询的常见实现方案以及优缺点:
-
from + size:- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
-
search after:- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
-
scroll:- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 search after 方案
高亮
高亮:就是在搜索结果中把搜索关键字突出显示
高亮显示的实现分为两步:
- 给文档中的所有关键字都添加一个标签,例如:
<em>标签 - 页面给
<em>标签编写 CSS 样式
基本语法:
1 | GET /hotel/_search |
eg:
1 | # 高亮查询,默认情况下,ES的搜索字段必须和高亮字段一致 |
注意:
- 高亮是对关键字高亮,因此 搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:
required_field_match=false

总结:
查询的DSL是一个大的 JSON对 象,包含下列属性:
query:查询条件from和size:分页条件sort:排序条件highlight:高亮条件

RestClient(查询文档)
入门案例
发起查询请求
以 match_all 查询为例:
1 |
|
-
第一步,创建
SearchRequest对象,指定索引库名 -
第二步,利用
request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
-
第三步,利用client.search()发送请求,得到响应

注意:两个关键的 API
一个是request.source(),其中包含了查询、排序、分页、高亮等所有功能

另一个是QueryBuilders,其中包含 match、term、function_score、bool 等各种查询

解析响应结果
1 |
|

elasticsearch返回的结果是一个JSON字符串,结构包含:
hits:命中的结果total:总条数,其中的value是具体的总条数值max_score:所有结果中得分最高的文档的相关性算分hits:搜索结果的文档数组,其中的每个文档都是一个json对象_source:文档中的原始数据,也是json对象
因此,解析响应结果,就是逐层解析JSON字符串,流程如下:
SearchHits:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果SearchHits#getTotalHits().value:获取总条数信息SearchHits#getHits():获取SearchHit数组,也就是文档数组SearchHit#getSourceAsString():获取文档结果中的_source,也就是原始的json文档数据
总结:
查询的基本步骤是:
- 创建 SearchRequest 对象
- 准备 Request.source(),也就是DSL
- QueryBuilders 来构建查询条件
- 传入 Request.source() 的 query() 方法
- 发送请求,得到结果
- 解析结果(参考JSON结果,从外到内,逐层解析)
match 查询
全文检索的 match 和 multi_match 查询与 match_all 的API基本一致。差别是查询条件,也就是 query 的部分。
Java 代码上的差异主要是 request.source().query() 中的参数了。同样是利用 QueryBuilders 提供的方法:


eg:
1 |
|
精确查询
精确查询主要是两者:
term:词条精确匹配range:范围查询
与之前的查询相比,差异同样在查询条件,其它都一样

布尔查询
布尔查询是用 must、must_not 、filte 等方式组合其它查询

eg:
1 |
|
排序和分页
搜索结果的排序和分页是与 query 同级的参数,因此同样是使用 request.source() 来设置

eg:
1 |
|
高亮
高亮的代码与之前代码差异较大,有两点:
- 请求构建(查询的 DSL):其中除了查询条件,还需要添加高亮条件,同样是与
query同级。 - 结果解析:结果除了要解析
_source文档数据,还要解析高亮结果
高亮请求构建

eg:
1 |
|
- 高亮查询必须使用 全文检索 查询,并且要有搜索关键字,将来才可以对关键字高亮
高亮结果解析
高亮的结果与查询的文档结果默认是分离的,并不在一起

- 第一步:从结果中获取
source。hit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为HotelDoc对象 - 第二步:获取高亮结果。
hit.getHighlightFields(),返回值是一个 Map,key 是高亮字段名称,值是 HighlightField 对象,代表高亮值 - 第三步:从 map 中根据高亮字段名称,获取高亮字段值对象 HighlightField
- 第四步:从 HighlightField 中获取 Fragments,并且转为字符串。这部分就是真正的高亮字符串了
- 第五步:用高亮的结果替换 HotelDoc 中的非高亮结果
eg:
1 |
|
总结:
- 所有搜索 DSL 的构建,只需要记住一个 API:SearchRequest 的
source()方法 - 高亮结果解析是参考 JSON 结果,逐层解析
数据聚合
**聚合(aggregations)**可以极其方便的实现对数据的 统计、分析 、运算
例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
聚合的分类
聚合常见的有三类:
-
**桶(Bucket)**聚合:用来 对文档做分组,类似于 MySQL 的 group by
TermAggregation:按照 文档字段值 分组- 例如:按照品牌值分组、按照国家分组
Date Histogram:按照 日期阶梯 分组- 例如:一周为一组,或者一月为一组
-
**度量(Metric)**聚合:用以 计算一些值,比如:最大值、最小值、平均值等
Avg:求平均值Max:求最大值Min:求最小值Stats:同时求 max、min、avg、sum 等
-
**管道(pipeline)**聚合:其它聚合的结果为基础做聚合
**注意:**参加聚合的字段必须是 keyword、日期、数值、布尔类型,一定不能是 text 类型
DSL 实现聚合
Bucket 聚合
例如:现在统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据 酒店品牌的名称 做聚合,也就是 Bucket聚合
基本语法:
1 | GET /hotel/_search |
eg:
1 | // 聚合功能 |

聚合结果排序
默认情况下,Bucket 聚合会统计 Bucket 内的文档数量,记为_count,并且按照 _count 降序排序。所以可以指定 order 属性,自定义聚合的排序方式
基本语法:
1 | GET /hotel/_search |
eg:
1 | GET /hotel/_search |
限定聚合范围
默认情况下,Bucket 聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。可以限定要聚合的文档范围,只要添加 query 条件即可
基本语法:
1 | GET /hotel/_search |

总结:
- aggs 代表聚合,与 query 同级,此时 query 的作用是:限定聚合的的文档范围
- 聚合的三要素:聚合名称、聚合类型、聚合字段
- 聚合可配置属性有:
- size:指定聚合结果数量
- order:指定聚合结果排序方式
- field:指定聚合字段
Metrics 聚合
例如,在上面已经对酒店按照品牌分组,形成了一个个桶。现在需要对桶内的酒店做运算,获取每个品牌的用户评分的 min、max、avg 等值。这就要用到 Metric 聚合了,例如 stat 聚合:就可以获取 min、max、avg 等结果
基本语法:
1 | GET /hotel/_search |
score_stats 聚合是在 brandAgg 的聚合内部嵌套的子聚合。因为需要在 每个桶 分别计算

RestAPI 实现聚合
聚合条件与 query 条件同级别,因此需要使用 request.source() 来指定聚合条件
聚合条件的语法:

聚合条件结果解析的语法:

eg:
1 |
|
自动补全
自动补全:根据用户输入的字母,提示完整词条的功能。因为需要根据拼音字母来推断,因此要用到拼音分词功能
拼音分词器
要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有 elasticsearch 的拼音分词插件。地址:https://github.com/medcl/elasticsearch-analysis-pinyin
官网下载方式就是进入 ElasticSearch 的 docker 容器内执行下面的指令
1 | bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-pinyin/7.12.1 |
测试拼音分词器是否可用的方法:
1 | POST /_analyze |
输出结果:

自定义分词器
elasticsearch中分词器(analyzer)的组成包含三部分:
character filters:在 tokenizer 之前对文本进行处理- 例如:删除字符、替换字符
tokenizer:将文本按照一定的规则切割成词条(term)- 例如:keyword,就是不分词;还有 ik_smart
tokenizer filter:将 tokenizer 输出的词条做进一步处理- 例如:大小写转换、同义词处理、拼音处理等

eg:
1 | PUT /test |
输出结果:

注意:为了避免搜索到同音字,搜索时不要使用拼音分词器
自动补全查询
elasticsearch 提供了 Completion Suggester 查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
-
参与补全查询的字段必须是
completion类型1
2
3
4
5
6
7
8
9
10
11// 创建索引库
PUT test
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
} -
字段的内容一般是 用来补全的多个词条 形成的数组
1
2
3
4
5
6
7
8
9
10
11
12
13// 示例数据
POST test/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test/_doc
{
"title": ["Nintendo", "switch"]
}
completion suggestion 查询的DSL语句
基本语法:
1 | // 自动补全查询 |
总结:
自动补全 对 字段 的要求:
- 类型是
completion类型 - 字段值是 多词条 的数组
RestAPI 实现自动补全
举例:实现 hotel 搜索框的自动补全功能
流程:
-
修改 hotel 索引库结构,设置自定义拼音分词器
-
修改索引库的 name、all 字段,使用 自定义分词器
-
索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义的分词器
-
给 HotelDoc 类添加 suggestion 字段,内容包含 brand、business
-
重新导入数据到 hotel 库
修改酒店映射结构
1 | // 酒店数据索引库 |
修改 HotelDoc 类
1 |
|
自动补全查询的 JavaAPI


eg:
1 |
|
数据同步
elasticsearch 中的酒店数据来自于 mysql 数据库,因此 mysql 数据发生改变时,elasticsearch 也必须跟着改变,这个就是 elasticsearch 与 mysql 之间的 数据同步
数据同步问题分析
方案一:同步调用

基本步骤如下:
- hotel-demo 对外提供接口,用来修改 elasticsearch 中的数据
- 酒店管理服务在完成数据库操作后,直接调用 hotel-demo 提供的接口
方案二:异步通知

流程如下:
- hotel-admin 对 mysql 数据库数据完成增、删、改后,发送MQ消息
- hotel-demo 监听 MQ,接收到消息后完成 elasticsearch 数据修改
方案三:监听binlog

流程如下:
- 给 mysql 开启 binlog 功能
- mysql 完成增、删、改操作都会记录在 binlog 中
- hotel-demo 基于 canal 监听 binlog 变化,实时更新 elasticsearch 中的内容
三个方案优缺点
方式一:同步调用
- 优点:实现简单,粗暴
- 缺点:业务耦合度高
方式二:异步通知
- 优点:低耦合,实现难度一般
- 缺点:依赖 mq 的可靠性
方式三:监听binlog
- 优点:完全解除服务间耦合
- 缺点:开启 binlog 增加数据库负担、实现复杂度高
MQ实现mysql与ES数据同步

流程:
-
导入依赖
1
2
3
4
5<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency> -
声明队列交换机名称
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
26public class MqConstants {
/**
* 交换机
*/
public final static String HOTEL_EXCHANGE = "hotel.topic";
/**
* 监听新增和修改的队列
*/
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
/**
* 监听删除的队列
*/
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
/**
* 新增或修改的RoutingKey
*/
public final static String HOTEL_INSERT_KEY = "hotel.insert";
/**
* 删除的RoutingKey
*/
public final static String HOTEL_DELETE_KEY = "hotel.delete";
} -
声明队列交换机
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
public class MqConfig {
public TopicExchange topicExchange(){
return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
}
public Queue insertQueue(){
return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
}
public Queue deleteQueue(){
return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
}
public Binding insertQueueBinding(){
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
}
public Binding deleteQueueBinding(){
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
}
} -
…(没学MQ,听不懂,先省略…)
ES集群
ES集群结构
单机的 elasticsearch 做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题
- 海量数据存储问题:将索引库从逻辑上拆分为 N 个分片(shard),存储到多个节点
- 单点故障问题:将分片数据在不同节点备份(replica )
ES集群相关概念:
-
集群(cluster):一组拥有共同的 cluster name 的 节点。
-
节点(node) :集群中的一个 Elasticearch 实例
-
分片(shard):索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中
- 解决问题:数据量太大,单点存储量有限的问题。

此处,把数据分成3片:shard0、shard1、shard2
-
主分片(Primary shard):相对于副本分片的定义。
-
副本分片(Replica shard)每个主分片可以有一个或者多个副本,数据和主分片一样。
数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高了
为了在高可用和成本间寻求平衡,可以这样做:
- 首先对数据分片,存储到不同节点
- 然后对每个分片进行备份,放到对方节点,完成互相备份
这样可以大大减少所需要的服务节点数量,如图,以3分片,每个分片备份一份为例:

现在,每个分片都有1个备份,存储在3个节点:
- node0:保存了分片 0 和 1
- node1:保存了分片 0 和 2
- node2:保存了分片 1 和 2
集群脑裂问题
ElasticSearch 中集群节点有不同的职责划分:

- 默认情况下,集群中的任何一个节点都同时具备上述四种角色
但是真实的集群一定要将集群职责分离:
master节点:对 CPU 要求高,但是 内存 要求低data节点:对 CPU 和 内存 要求都高coordinating节点:协调节点。对 网络带宽、CPU 要求高
职责分离可以让根据不同节点的需求分配不同的硬件去部署。而且避免业务之间的互相干扰

集群脑裂:是因为集群中的节点失联导致的
- 默认情况下,每个节点都是 master eligible 节点,因此一旦 master 节点宕机,其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题

解决脑裂的方案:要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此 eligible 节点数量最好是奇数。对应配置项是 discovery.zen.minimum_master_nodes,在 es7.0 以后,已经成为默认配置,因此一般不会发生脑裂问题
- 例如:3个节点形成的集群,选票必须超过 (3 + 1) / 2 ,也就是2票。node3 得到 node2 和 node3 的选票,当选为主。node1 只有自己1票,没有当选。集群中依然只有1个主节点,没有出现脑裂
总结:
master eligible 节点的作用是:
- 参与集群选主
- 主节点可以管理集群状态、管理分片信息、处理创建和删除索引库的请求
data 节点的作用是:
- 数据的CRUD
coordinator 节点的作用是:
-
路由请求到其它节点
-
合并查询到的结果,返回给用户
集群分布式存储
当新增文档时,应该保存到不同分片,保证数据均衡,那么 coordinating node 确定数据该存储到哪个分片的方法:
-
elasticsearch 会通过 hash 算法来计算文档应该存储到哪个分片

说明:
_routing:默认是文档的 id- 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改
eg:

- 新增一个 id=1 的文档
- 对 id 做 hash 运算,假如得到的是2,则应该存储到 shard-2
- shard-2 的主分片在 node3 节点,将数据路由到 node3
- 保存文档
- 同步给 shard-2 的副本 replica-2,在 node2 节点
- 返回结果给 coordinating-node 节点
集群分布式查询
elasticsearch 的查询分成两个阶段:
-
scatter phase:分散阶段,coordinating node 会把请求分发到每一个分片 -
gather phase:聚集阶段,coordinating node 汇总 data node 的搜索结果,并处理为最终结果集返回给用户

集群故障转移
集群的 master 节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个叫做 故障转移







