Elasticsearch

Elasticsearch
mengnankkzhou介绍
Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。
单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。
Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。
所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。
下面的命令可以查看当前节点的所有 Index。
1 | curl -X GET 'http://localhost:9200/_cat/indices?v' |
Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。
Document 使用 JSON 格式表示,下面是一个例子。
1
2
3
4
5 {
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}
同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。
Document 可以分组,比如weather
这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。
不同的 Type 应该有相似的结构(schema),举例来说,id
字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如products
和logs
)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。
下面的命令可以列出每个 Index 所包含的 Type。
1 $ curl 'localhost:9200/_mapping?pretty=true'
根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。
新建 Index,可以直接向 Elastic 服务器发出 PUT 请求。下面的例子是新建一个名叫weather
的 Index。
1 $ curl -X PUT 'localhost:9200/weather'
服务器返回一个 JSON 对象,里面的acknowledged
字段表示操作成功。
1
2
3
4 {
"acknowledged":true,
"shards_acknowledged":true
}
然后,我们发出 DELETE 请求,删除这个 Index。
1 $ curl -X DELETE 'localhost:9200/weather'
需要指定中文分词器,不能使用默认的英文分词器。
Elastic 的分词器称为 analyzer。我们对每个字段指定分词器。
操作
新增:
1 | $ curl -X PUT 'localhost:9200/accounts/person/1' -d ' |
新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。
1 |
|
不进行指定的话_id
字段就是一个随机字符串。
查询:
向/Index/Type/Id
发出 GET 请求,就可以查看这条记录。
1 $ curl 'localhost:9200/accounts/person/1?pretty=true'
删除:
删除记录就是发出 DELETE 请求。
1 $ curl -X DELETE 'localhost:9200/accounts/person/1'
这里先不要删除这条记录,后面还要用到。
更新:
更新记录就是使用 PUT 请求,重新发送一次数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 $ curl -X PUT 'localhost:9200/accounts/person/1' -d '
{
"user" : "张三",
"title" : "工程师",
"desc" : "数据库管理,软件开发"
}'
{
"_index":"accounts",
"_type":"person",
"_id":"1",
"_version":2,
"result":"updated",
"_shards":{"total":2,"successful":1,"failed":0},
"created":false
}
返回所有记录:
使用 GET 方法,直接请求/Index/Type/_search
,就会返回所有记录。
1 $ curl 'localhost:9200/accounts/person/_search'
全文搜索:
Elastic 的查询非常特别,使用自己的查询语法,要求 GET 请求带有数据体。
1
2
3
4 $ curl 'localhost:9200/accounts/person/_search' -d '
{
"query" : { "match" : { "desc" : "软件" }}
}'
指定的匹配条件是desc
字段里面包含”软件”这个词。返回结果如下。
Elastic 默认一次返回10条结果,可以通过size
字段改变这个设置。
1 | $ curl 'localhost:9200/accounts/person/_search' -d ' |
还可以通过from
字段,指定位移。
1
2
3
4
5
6 $ curl 'localhost:9200/accounts/person/_search' -d '
{
"query" : { "match" : { "desc" : "管理" }},
"from": 1,
"size": 1
}'
上面代码指定,从位置1开始(默认是从位置0开始),只返回一条结果。
如果有多个搜索关键字, Elastic 认为它们是or
关系。
如果要执行多个关键词的and
搜索,必须使用布尔查询。
1 | $ curl 'localhost:9200/accounts/person/_search' -d ' |
Java中使用
首先需要在 pom.xml
文件中添加 Elasticsearch 和 Jackson 的依赖:
创建客户端,在Config类里面
1 | RestClient restClient = RestClient |
索引文档:
1 | Person person = new Person(20, "Mark Doe", new Date(1471466076564L)); |
查询:
1 | String searchText = "John"; |
模糊匹配:
1 | Query fuzzyQuery = FuzzyQuery.of(f -> f |
fuzziness
参数控制允许的编辑距离,可以设置为整数值或者 “AUTO”,后者会根据搜索词的长度自动调整
布尔查询:组合 match
查询(用于精确匹配)和 fuzzy
查询(用于模糊匹配)
1 | Query matchQuery = MatchQuery.of(m -> m.field("fullName").query(searchText))._toQuery(); |
使用 N-Grams 提高模糊匹配,通过索引文本的 n-gram 来提高模糊匹配的性能和准确性的技术
可以使用 Lucene 查询解析器语法构建更复杂的查询,包括通配符、布尔运算符等。
1 | Query simpleStringQuery = SimpleQueryStringQuery.of(q -> q.query("*Doe"))._toQuery(); |
优化
- 恰当的索引配置:合理配置分片数量和副本数量是提高查询性能的关键。分片数量应根据数据量和查询负载合理配置,副本数量则可以提高数据的可用性和查询性能。
- 合理的分词器选择:选择合适的分词器和分析器可以显著提高搜索精确度和性能。
- 高效的缓存使用:合理配置和使用不同层级的缓存,可以显著提高查询性能,减少响应时间。定期监控和调优缓存配置,确保缓存的高效利用和系统的健康运行。
- 查询语句优化:根据具体的查询需求,选择合适的分页查询策略,如 from-size、Scroll、Search_after 和 Search_after (PIT)。不同的分页查询策略有各自的优缺点,合理选择可以提高查询性能和用户体验。同时自定义评分函数,可调整搜索结果的相关性评分,确保返回的结果更符合业务需求。
- 慢查询瓶颈分析:需关注的CPU 使用率,内存使用率以及磁盘IO,当其中一项达到瓶颈,查询性能就可能上不去了。
面试问题
1.ES 的分布式架构原理能说一下么
ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。
ES 中存储数据的基本单位是索引,比如说你现在要在 ES 中存储一些订单数据,你就应该在 ES 中创建一个索引 order_idx
,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一个数据库。
1 | index -> type -> mapping -> document -> field |
一个 index 里可以有多个 type,每个 type 的字段都是差不多的
差不多是这么个对应关系
index->一类表
type->表
mapping->表结构
ducument->行
field->值
你搞一个索引,这个索引可以拆分成多个 shard
,每个 shard 存储部分数据。拆分多个 shard 是有好处的,一是支持横向扩展,比如你数据量是 3T,3 个 shard,每个 shard 就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 shard 的索引,将数据导进去;二是提高性能,数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。
接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 primary shard
,负责写入数据,但是还有几个 replica shard
。 primary shard
写入数据之后,会将数据同步到其他几个 replica shard
上去。
通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。
ES 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。
如果是非 master 节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。接着你要是修复了那个宕机机器,重启了之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。
说得更简单一点,就是说如果某个非 master 节点宕机了。那么此节点上的 primary shard 不就没了。那好,master 会让 primary shard 对应的 replica shard(在其他机器上)切换为 primary shard。如果宕机的机器修复了,修复后的节点也不再是 primary shard,而是 replica shard。
其实上述就是 ElasticSearch 作为分布式搜索引擎最基本的一个架构设计。
其实都是差不多的,这些分布式的构建
2.ES 写入数据的工作原理是什么啊?ES 查询数据的工作原理是什么啊?底层的 Lucene 介绍一下呗?倒排索引了解吗?
写数据:
- 客户端选择一个 node 发送请求过去,这个 node 就是
coordinating node
(协调节点)。 coordinating node
对 document 进行路由,将请求转发给对应的 node(有 primary shard)。- 实际的 node 上的
primary shard
处理请求,然后将数据同步到replica node
。 coordinating node
如果发现primary node
和所有replica node
都搞定之后,就返回响应结果给客户端。
读数据:
可以通过 doc id
来查询,会根据 doc id
进行 hash,判断出来当时把 doc id
分配到了哪个 shard 上面去,从那个 shard 去查询。
- 客户端发送请求到任意一个 node,成为
coordinate node
。 coordinate node
对doc id
进行哈希路由,将请求转发到对应的 node,此时会使用round-robin
随机轮询算法,在primary shard
以及其所有 replica 中随机选择一个,让读请求负载均衡。- 接收请求的 node 返回 document 给
coordinate node
。 coordinate node
返回 document 给客户端。
搜索数据:
你根据 java
关键词来搜索,将包含 java
的 document
给搜索出来。es 就会给你返回:java 真好玩儿啊,java 好难学啊。
- 客户端发送请求到一个
coordinate node
。 - 协调节点将搜索请求转发到所有的 shard 对应的
primary shard
或replica shard
,都可以。 - query phase:每个 shard 将自己的搜索结果(其实就是一些
doc id
)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。 - fetch phase:接着由协调节点根据
doc id
去各个节点上拉取实际的document
数据,最终返回给客户端。
底层lucene:
简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。
通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构
倒叙索引:
在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。
那么,倒排索引就是关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。
另外,实用的倒排索引还可以记录更多的信息,比如文档频率信息,表示在文档集合中有多少个文档包含某个单词。
那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 Facebook
,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。
- 倒排索引中的所有词项对应一个或多个文档;
- 倒排索引中的词项根据字典顺序升序排列
底层实现:
3.ES 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?
- es 生产集群我们部署了 5 台机器,每台机器是 6 核 64G 的,集群总内存是 320G。
- 我们 es 集群的日增量数据大概是 2000 万条,每天日增量数据大概是 500MB,每月增量数据大概是 6 亿,15G。目前系统已经运行了几个月,现在 es 集群里数据总量大概是 100G 左右。
- 目前线上有 5 个索引(这个结合你们自己业务来,看看自己有哪些数据可以放 es 的),每个索引的数据量大概是 20G,所以这个数据量之内,我们每个索引分配的是 8 个 shard,比默认的 5 个 shard 多了 3 个 shard。
4.ES 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?
使用filesystem cache
往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache
里面去。
es 的搜索引擎严重依赖于底层的 filesystem cache
,你如果给 filesystem cache
更多的内存,尽量让内存可以容纳所有的 idx segment file
索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。
写入 es 的数据最好小于等于,或者是略微大于 es 的 filesystem cache 的内存容量。
数据预热
最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache
里面去。这样下次别人访问的时候,性能一定会好很多。
冷热分离
es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache
里,别让冷数据给冲刷掉。
document模型设计
最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
分页性能优化
假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。
前面的几页速度挺快,后面的就不行了。
解决办法:
1.不允许深度分页
2.类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api
scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id
移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。
但是他不能随意跳到任何一页的场景。
初始化时必须指定 scroll
参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
除了用 scroll api
,你也可以用 search_after
来做, search_after
的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。