此为笔者学习ES 的第三篇总结,也是最终篇,其他基础相关内容可自行查询:ElasticSearch(一) 、 ElasticSearch(二)
前言
本篇内容为学习ElasticSearch 的课后总结(三),本文大致包括以下内容:
- ES 中的聚合
- ES 实现自动补全
- ES 的数据同步问题
- ES 集群搭建与相关问题讲解
整个过程记录详细,每个步骤亲历亲为,实测可用。
一、ES 聚合
-
在我们常用的MySql数据库中,我们也会常常使用一些聚合函数来对查询的结果进行初步的统计分析,ES中有这个功能吗?
答案当然是肯定的,而且ES 中的聚合功能比MySql 中的聚合做得更完善。(毕竟查询是ES的强项)
聚合(aggregations)可以对文档数据进行统计、分析、运算等。ES中常见的聚合有三种类型:
-
桶(Bucket)聚合:用于做文档分组。
- TermAggregation:按照文档字段值分组。
- Date Histogram:按照日期间隔分组。如:一周一组、一月一组
-
度量(Metric)聚合:用于统计文档数据中的特殊值。
- Avg:求平均值;
- Min:求最小值;
- Max:求最大值
- Stats:同时求Max、Min、Avg、Sum等。
-
管道(Pipeline)聚合:在其他聚合的结果上做二次聚合。如:bucket聚合(分组)后,在统计分组后的最大值等。
此外,参与聚合的字段只能为:keyword、数值、日期、布尔。
为了让大家更深刻的了解ES中的聚合,我们将ES与Mysql做个对比:
MySql ElasticSearch 分组 Group By Bucket Aggregation 统计 聚合函数 Metric Aggregation -
-
DSL实现聚合
a) DSL实现Bucket 聚合
聚合的三要素: 聚合名称、聚合类型以及聚合字段。
例如: 统计所有数据中的酒店品牌。
GET /hotel/_search { "size": 0, // 设置结果中的显示的文档数量。如果为0,则表示只显示聚合结果 "aggs": { // 聚合,与query 同级 "brandAgg": { // 聚合名称 "terms": { // 聚合类型 "field": "brand", // 聚合字段 "size": 20 // 展现多少个聚合结果 } } } }
我们还可以对聚合结果进行排序:
GET /hotel/_search { "size": 0, "aggs": { "brandAgg": { "terms": { "field": "brand", "size": 20, "order": { // 对分组后的计数存放在 doc_count字段,默认对此字段进行降序排序 "_count": "desc" } } } } }
默认情况下,Bucket聚合是对所有文档进行聚合的,这无疑是对内存的巨大消耗。因此,我们可以在Bucket聚合前限定文档的范围(只需要添加一个query 查询条件即可):
# 统计所有数据中的酒店品牌。 GET /hotel/_search { "query": { "range": { "price": { "lte": 200 // 只聚合价格在 200元以下的数据 } } }, "size": 0, "aggs": { "brandAgg": { "terms": { "field": "brand", "size": 20 } } } }
b) DSL实现Metrics 聚合
例如: 对每个品牌的用户评分的min、max、avg等值进行统计分析。
:这里我们使用stats 聚合,且在此之前,我们需要先对品牌进行分组处理。GET /hotel/_search { "size": 0, "aggs": { "brandAgg": { "terms": { "field": "brand", "size": 20, "order": { "_count": "desc" } }, "aggs": { // 注意此处的位置,在上一级的聚合名称 的下一级。 "score_stats": { // 指定聚合的名称 "stats": { // 指定聚合的类型。这里可以是Max、Min、Avg等 "field": "score" // 指定聚合的字段 } } } } } }
c) RestApi实现聚合
@Test void testAggregation() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source().size(0); request.source().aggregation( AggregationBuilders.terms("brandAgg") .field("brand") .size(20) ); // 3. 发起请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4. 解析结果 System.out.println(response); Aggregations aggregations = response.getAggregations(); Terms brandTerms = aggregations.get("brandAgg"); List<? extends Terms.Bucket> buckets = brandTerms.getBuckets(); for (Terms.Bucket bucket : buckets) { String brandName = bucket.getKeyAsString(); System.out.println(brandName); } }
在对返回数据进行解析时,我们可将控制台返回结果做对比:
在上一节中我们大致了解了ES中的聚合,并采用了DSL 语句和Rest Api 实现了ES对文档数据的聚合。在下一小节,我们将学习ES 中一个常用的功能:自动补全!
二、ES 实现自动补全
什么是自动补全呢? 这个问题先不直接用文字回答,我们先去某宝上看看具体实际效果如何:
当我们输入"s"时,系统会自动补全,将以 “s” 开头的文档关键字放置在下面的搜索框中供我们提示选择。这就叫做自动补全。
不难发现,我们是对一个 拼音字母的输入,却能够实现汉字关键字的搜索。 这里是借助另外一个分词器来实现的:拼音分词器
。
1. 安装拼音分词器
这里我们借助 GitHub上的elasticsearch的拼音分词插件。拼音分词器
pinyin 分词器的 安装方式与IK分词器一样,分三步:
- 解压;
- 上传到虚拟机中ES 的插件挂载目录;
- 重启ES
可使用docker volume inspect es-plugins
查看es 管理插件的目录挂载位置:
在将解压后的目录放在此处即可:
重启ES 后我们就可进行相关测试:
仔细观察分词结果,拼音分词倒是实现了,但结果把每个汉字都单独拎出来进行拼音分词。我们肯定更希望相关性的词留在一起进行分词,此时,我们就需要去了解:自定义分词器
2. 自定义分词器
2.1 ES的分词不是一步促成的,ES中的分词器(analyzer)的组成包括三个部分:
- character filters:首先对文档进行处理,进行特定字段的替换、删除等。
- tokenizer:将文本按照指定的规则进行分词处理。(例如:keyword代表不分词,ik_smart 等)
- tokenizer filter:将tokenizer 输出的词条做进一步的处理。例如:大小写转换、拼音处理等。
例如:
我们可以在创建索引库时,通过settings 来配置自定义的分词器(analyzer):
PUT /test
{
"settings": {
"analysis": {
"analyzer": { // 自定义分词器
"my_analyzer": { // 分词器名称
"tokenizer": "ik_max_word",
"filter": "pinyin"
}
}
}
}
}
此外,我们常常需要对pinyiin
分词器做详细的配置,以达到业务需求:
PUT /test
{
"settings": {
"analysis": {
"analyzer": { // 自定义分词器
"my_analyzer": { // 分词器名称
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": { // 自定义tokenizer filter
"py": { // 过滤器名称
"type": "pinyin", // 过滤器类型,这里是pinyin
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
}
}
此时的分词效果就大致符合我们的业务要求了。
注意!!! 我们在创建完包含自定义分词器的索引库时,无法像下面这样进行测试:
# error
POST /_analyze
{
"text": ["今天天气真不错"],
"analyzer": "my_analyzer"
}
因为, 我们在创建自定义分词器时, 是针对一个具体的索引库的, 因此我们在测试时, 需带上索引库名称:
# yes
POST /test/_analyze
{
"text": ["今天天气真不错"],
"analyzer": "my_analyzer"
}
2.2 注意事项
拼音分词器适合在创建倒排索引时使用, 但不建议在搜索的时候使用。
例如: 我们在搜索 “掉到狮子笼咋办,在线等,很急” 时,会出现 “虱子” 相关的内容,这显然是不合适的。
因此,我们可以在创建索引库时,分别指定创建和搜索使用的分词器 :.
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer", // 指定创建倒排索引时的分词器
"search_analyzer": "ik_smart" // 指定搜索时的分词器
}
}
}
}
3. Completion Suggester 查询
3.1 DSL 实现自动补全
ES 中提供了Completion Suggester
查询实现自动补全的功能。这个查询会匹配以用户输入内容开头的词条进行补全。为了提高补全查询的效率,对于文档中的字段的类型存在一定的** 约束:**
- 参与补全查询的字段必须是
completion
类型。 - 字段的内容一般是用于查询补全查询的
多个词条组成的数组
。
例如, 一个这样的索引库:
#创建索引库
PUT /test2
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
插入数据:
// 示例数据
POST test2/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test2/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test2/_doc
{
"title": ["Nintendo", "switch"]
}
查询的DSL语句如下:
# 自动补全查询
GET /test2/_search
{
"suggest": {
"title_suggestion": {
"text": "s", // 关键字(前缀)
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过查询的重复数据
"size": 10 // 获取前10 条数据
}
}
}
}
3.2 RestApi 实现自动补全
@Test
void testSuggest() throws IOException {
// 1. 准备查询
SearchRequest request = new SearchRequest("hotel");
// 2. 准备DSL
request.source().suggest(
new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
)
);
// 3. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 处理响应结果
Suggest suggest = response.getSuggest();
CompletionSuggestion suggestion = suggest.getSuggestion("suggestions");
for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
String text = option.getText().toString();
System.out.println(text);
}
System.out.println(response);
}
为了能够理解更清晰,这里将Rest Api 查询补全语句与DSL 中的查询补全语句做对比:
在上一小节,我们初步了解了自动补全的原理及其实现方法。在下一小节,我们将了解业务中的数据库与ES 之间的同步问题。
三、ES 的数据同步问题
-
重新回顾 引入ES 后的系统架构:
对于读操作,我们交给ES 进行处理;对于写操作,我们交给MySql进行处理。 借助两者的优势,各司其责!但是当我们MySql中的数据发生变动时,此时用户通过ES 读取数据就会存在数据不一致的问题了。
通常我们解决数据同步问题有三种方法:
-
同步调用: 同步调用是指用户先完成对数据库的修改,然后再调用ES 暴露的更新索引库的接口来更新ES,等待所有步骤执行完毕后,再响应 ”修改成功“ 给用户。
-
异步通知: 异步通知是借助 MQ 来实现的。
-
监听binlog: MySql 的所有写操作都可以记录在binlog 日志文件中(默认是关闭的),我们可以借助第三方工具来监视binlog 日志的变化来解决数据同步问题。
为了更加清晰的了解这三种解决方法,我们将这三者各自的优、缺点分别进行对比分析:
同步调用 异步通知 binlog 优点 实现简单,粗暴 低耦合,实现难度一般 完全解除服务间耦合 缺点 业务耦合度高 依赖mq 的可靠性 开启binlog 增加数据库的负担 且实现难度较高 -
-
基于 异步通知 解决数据同步问题的具体实现
a) mq 架构图
b) 导入AMQP
相关依赖:<!-- amqp--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
c) MQ 相关配置:
spring: rabbitmq: host: 192.168.220.137 # mq主机地址 port: 5672 # 开放端口 username: itcast password: 123321 virtual-host: / # 虚拟目录(做用户数据隔离的)
d) 初始化Exchange、Queue和 Binding :
@Configuration public class MqConfig { @Bean public TopicExchange topicExchange(){ /* 第二个参数:持久化,设置后,数据将会存到硬盘中。 第三个参数:autoDelete,当所有与此连接的客户端都断开时,该交换机会被删除 */ return new TopicExchange(MqConstants.HOTEL_EXCHANGE,true,false); } @Bean public Queue insertQueue(){ return new Queue(MqConstants.HOTEL_INSERT_QUEUE,true); } @Bean public Queue deleteQueue(){ return new Queue(MqConstants.HOTEL_DELETE_QUEUE,true); } @Bean public Binding insertQueueBinding(){ return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY); } @Bean public Binding deleteQueueBinding(){ return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY); } }
e) 发送数据:
@PostMapping public void saveHotel(@RequestBody Hotel hotel){ hotelService.save(hotel); // 发送数据到mq 中 rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId()); } @DeleteMapping("/{id}") public void deleteById(@PathVariable("id") Long id) { hotelService.removeById(id); // 发送数据到mq 中 rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_DELETE_KEY,id); }
f) 接收数据:
@Component public class HotelListener { @Autowired private IHotelService iHotelService; /* 监听酒店新增或修改的业务 */ @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE) public void listenHotelInsertOrUpdate(Long id){ iHotelService.insertById(id); } /* 监听酒店删除的业务 */ @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE) public void listenHotelDelete(Long id){ iHotelService.deleteById(id); } }
上一小节的内容呢,我们大概了解ES 存在的数据同步问题,以及对应的解决方案。下一小节,我们将学习ES 集群的搭建,以及分析过程中可能遇到的问题等。
四、ES 集群搭建及相关问题
-
ES 集群结构
单机的ES 做数据存储,必然面临着海量数据存储问题 和 单点故障问题。而ES天然是分布式的
,那么ES 集群是怎么解决这些问题的呢:海量数据存储问题
:将索引库从逻辑上拆分成多个分片(shard),存储到多个节点。单点故障问题
:将分片数据在不同节点备份。(replica)
-
搭建ES 集群
部署es集群 可以直接使用
docker-compose
来完成,不过要求你的Linux虚拟机至少有 4G 的内存空间。(由于笔者电脑配置一般,这里只记录步骤,并没有亲自实践qaq)
docker-compose文件
version: '2.2' services: es01: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: es01 environment: - node.name=es01 - cluster.name=es-docker-cluster - discovery.seed_hosts=es02,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 volumes: - data01:/usr/share/elasticsearch/data ports: - 9200:9200 networks: - elastic es02: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: es02 environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 volumes: - data02:/usr/share/elasticsearch/data networks: - elastic es03: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: es03 environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 volumes: - data03:/usr/share/elasticsearch/data networks: - elastic volumes: data01: driver: local data02: driver: local data03: driver: local networks: elastic: driver: bridge
es运行需要修改一些linux系统权限,修改
/etc/sysctl.conf
文件vi /etc/sysctl.conf
添加下面的内容:
vm.max_map_count=262144
然后执行命令,让配置生效:
sysctl -p
通过docker-compose启动集群:
docker-compose up -d
-
ES 集群的监控
kibana可以监控es集群,不过新版本需要依赖es的x-pack 功能,配置比较复杂。这里使用
cerebro
来监控es集群状态。官方网址:https://github.com/lmenezes/cerebro
在Windows 环境下安装即可,安装成功后,进入
bin
目录,双击cerebro.bat
即可执行。启动成功后,即可通过浏览器查看:
看上去还是非常不错的。如果此处ES 是集群部署的话,效果更加优秀:
-
ES 集群的节点角色
当我们完成ES的集群搭建时,不同的ES 可能担任不同的角色,特别是负责整个集群状态管理的节点应该尽量选择配置较好的节点担任。下面介绍ES 中的三种节点角色:
ES 中的每个节点角色都有自己不同的职责,此时,当用户向ES 发起查询请求时,整个过程大致如下图所示:
-
ES 集群的脑裂
默认情况下,每个节点都是master eligible 节点,一旦master 节点宕机,其他候选节点会选举一个新的节点作为主节点。如果master 节点并非宕机,只是短暂的网络故障,此时就会引起脑裂问题。
脑裂问题是指master 节点网络故障后,剩余节点重新选取新的master 节点。当原来的master 节点网络恢复后,就会同时存在两个master 节点,就会容易出现数据不一致的问题。
为了避免脑裂问题,ES增加了选举master 的条件:要求选票超过(eligible节点数量+1)/ 2。因此,eligible节点数量最好是奇数。对应的配置项是
discovery.zen.minimum_master_nodes
,在es7.0 以后,已经称为默认配置,因此现在一般不会发生脑裂问题。 -
ES 集群的分布式存储
当新增文档时,应该将数据保存到不同的分片中,保证数据均衡,那么coordination node
时如何确定哪条endangered应该放在哪个切片中的呢?ES 会通过一个
hash 算法
来计算文档应该存放到哪个切片中:
Note:
- _routing 默认是文档的id;
- 算法与分片数量相关,因此在索引库创建后,就无法修改分片数量。否则会出现,之前存入的数据查询错误。
此时,我们的ES 集群的新增文档流程就会如下所示:
-
ES 集群的分布式查询
对于ES集群环境的查询,大致分为两个阶段:
scatter phase:分散阶段。
由coordinating node 把请求分发给没有给分片。gather phase:聚集阶段。
coordinating node再汇集data node 的搜索结果,并进行处理后,返回给用户。
这也很好的解释了,集群环境下的分页查询数量不宜过大的原因。(coordinating node 会收到每个节点的数据,再进行汇总处理,数据量过大,内存无法支撑)
-
ES 集群的故障转移
ES集群中的master 节点会监控集群中的节点状况,如果发现节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个过程称为 故障转移。
当之前的节点恢复正常时,master 节点又会把分片数据重新放置在之前的节点上,做到数据均衡存储。
以上就为本篇文章的全部内容啦!
如果对你有帮助的话,请多多点赞支持一下呗!