4.3.1 你们项目中主要使用ElasticSearch 干什么
ElasticSearch在我们项目中的使用场景主要有二种场景 :
第一种场景 : 关键词的分词查询, 例如 : 文章的搜索, 动态的搜索, 商品的搜索 , 话题的搜索等功能
第二种场景 : 使用ElasticSearch实现系统中的部分统计功能 , 例如 : 订单的相关数据统计
使用ElasticSearch的主要原因是ES的分词检索的效率和海量数据聚合统计的效率要比MYSQL数据库要高很多
4.3.2 什么是正向索引?什么是倒排索引?
正向索引就是先获取文档内容, 再判断文档内容中是否包含所需的关键词 , 这种查询方式就是正向索引 , MYSQL的模糊查询就是正向索引
而倒排索引,是通过分词策略,形成了词条和文档的映射关系表,这种词条+映射表的结构即为倒排索引
ES的倒排索引底层使用B+树的结构进行组织 , 每一个词条即为B+树的一个节点 , 叶子节点挂的就是词条所在文档的ID , 查询的时候先根据关键词查询文档ID , 在根据文档ID获取文档详情 , 因为走索引查询所以查询效率比正向索引要高的多
4.3.3 分词器的作用是什么
分词器是ES中的一个组件 , 通俗意义上理解就是将一段文本按照一定的逻辑 , 分析成多个词语,ES会将text类型的字段按照分词器进行分词,并编排成倒排索引 , 因为有了倒排索引所以ES的查询效率才比较快
elasticsearch中分词器(analyzer)的组成包含三部分:
character filters
:在tokenizer之前对文本进行处理。例如删除字符、替换字符tokenizer
:将文本按照一定的规则切割成词条(term)。例如 keyword , 就是不分词;还有 ik_smarttokenizer filter
:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
4.3.4 说一说ElasticSearch深度分页问题
ElasticSearch默认采用的分页方式是from+ size
的形式,在深度分页的情况下,这种使用方式效率是非常低的 , 在分布式查询的时候 , ElasticSearch需要在各个分片上匹配排序并得到from+ size
条数据,协调节点拿到这些数据再进行汇总然后进行排序等处理,然后结果集中取最后10条数据返回。
这样当页码比较大的时候 , 假如说10000页 , 每页展示10条数据 , 就需要去每个节点查询100010条数据 , 然后再汇总排序 , 获取最后10条数据 , 效率非常低
所以ElasticSearch为了性能,限制了我们分页的深度,es目前支持的最大的 max_result_window = 10000;也就是说我们不能分页到10000条数据以上 , 出国from+size
值超过10000就会报深度分页问题
解决方案主要有三种 :
- 从业务上进行限制 , 只让用户访问前100或者200页数据 , 像百度和JD都是这样的 , 翻到一定的页码后就不允继续查询了
- 采用
Search After
机制 , 对业务数据进行排序 , 每次查询下一页的时候, 根据上一页的最后一条结果的值开始向下进行查询 , 这样就通过条件将前面的数据过滤掉了 ,起始索引永远是0 - 使用scroll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来scroll会将排序后的文档id形成快照,保存在内存 , 目前官方已经不建议使用
我们项目中采用的是Search After
机制
4.3.5 ElasticSearch中的数据类型有哪些
ElasticSearch底层基于Lucene , Lucene底层又是基于Java的 , 所以ElasticSearch的数据类型跟Java比较像
ElasticSearch中的文档存储的格式是JSON格式 , JSON中可以存储很多个字段 , 字段的类型又有很多中 , 例如 :
- 字符串类型:text(需要分词的文本)、keyword(精确值,不需要分词的内容)
- 数值类型:long、integer、short、byte、double、float、
- 布尔类型:boolean
- 日期类型:date
- 对象类型:object
- 地理位置类型 : geo-point , geo-shape
- 自动补全类型 : completion
4.3.6 query 和 filter 的区别?
不管是query
还是filter
在使用的时候都代表要满足条件 , 但是又有些区别 :
- filter : 只根据搜索条件过滤出符合的文档, 将这些文档的评分固定为1, 不计算相关度分数
- query : 先查询符合搜索条件的文档, 然后计算每个文档对于搜索条件的相关度分数, 再根据评分倒序排序
从效率来说 :
- filter 性能更好, 不计算相关度分数, 不用根据相关度分数进行排序, 同时ES内部还会缓存比较常用的filter的数据 , 所以效率比较高
- query 性能较差, 要计算相关度分数, 要根据相关度分数进行排序, 并且没有缓存功能
使用场景 :
- 如果对搜索结果有排序的要求, 要将最匹配的文档排在最前面, 就用 query
- 如果只是根据一定的条件筛选出部分数据, 不关注结果的排序, 就用 filter
4.3.7 text 和 keyword类型的区别?
text类型代表需要分词的文本 , 支持模糊查询 , 不支持排序和聚合查询
keyword代表不需要分词的文本 , 支持精确查询 , 支持排序和聚合
4.3.8 Elasticsearch中常用的查询方式有哪些
Elasticsearch中提供的查询方式有很多 , 例如 :
- termsQuery : 词条查询, 一般用于keyword类型 , 直接根据条件不分词查询
- matchQuery : 分词查询 , 一般用于text类型 , 会对用户输入的条件先进行分词 ,分成词条之后再进行查询
- multiMatchQuery : 分词查询 , 一般用于text类型 , 根据多个字段分词查询
- prefixQuery : 前缀查询 , 查询指定前缀开头的数据
- regexpQuery : 正则查询 , 查询匹配正则表达式的数据
- rangeQuery : 范围查询 , 一般用于数值 , 日期等类型 ,, 查询范围之内的数据
- wildcardQuery : 模糊查询, 类似于mysql中的like
- queryStringQuery : 和match类似,但是match需要指定字段名,query_string不需要指定字段名,是在所有字段中搜索,范围更广泛
- boolQuery : 布尔查询 , 将多个基础查询条件拼接一起查询
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
- functionScoreQuery : 算分函数查询 , 指定算分函数规则 , 会对匹配条件的数据进行重新算分, 一般用于竞价排名
- geoDistanceQuery : 地理位置 , 半径范围查询 , 指定中心点和半径, 查询在半径范围内的数据
- geoBoundingBoxQuery : 地理位置矩阵范围查询 , 指定左上坐标和右下坐标 , 查询在矩阵范围内的数据
4.3.9 term和match的区别
- term:代表完全匹配,也就是精确查询,搜索前不会再对搜索词进行分词,直接对搜索词进行查找
- match:代表分词匹配,搜索前会对搜索词进行分词,然后按搜索词匹配查找
4.3.10 描述一下 Elasticsearch 搜索的过程
Elasticsearch的查询分成两个阶段 :
- scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
- gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
4.3.11 描述⼀下Elasticsearch索引文档的过程
Elasticsearch是一个分片集群架构 , 在新增文档的时候会通过hash算法来计算文档应该存储到哪个分片:
- _routing默认是文档的id
- 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!
举个例子 : 新增一个id=1的文档 , 他的流程如下 :
- 新增请求发送给协调节点
- 协调节点对id做hash运算,得到分片编号 , 获取该分片主分片所在节点
- 请求路由到主分片所在节点
- 主分片所在节点保存文档数据到主分片
- 同步给副本分片,副本分片保存文档数据
- 返回结果给协调节点 , 协调节点返回新增结果给客户端
4.3.12 说一说ES的脑裂问题
在ElasticSearch集群初始化或者主节点宕机的情况下,由候选主节点中选举其中一个作为主节点
当主节点负载压力过大,或者集群环境中的网络问题,导致其他节点与主节点通讯的时候,主节点没来及响应,这样的话,某些节点就认为主节点宕机,重新选择新的主节点,集群中就出现了两个主节点的情况,这种情况就称之为脑裂现象
出现脑裂问题的原因一般有如下几种 :
- 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master
- 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
- 内存回收:data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应
知道了问题 , 解决方案也就知道了 , 常用的解决方案有以下几种 :
- 角色分离:即master节点与data节点分离,限制角色;数据节点时需要承担存储和搜索的工作的,压力会很大。所以如果该节点同时作为候选主节点和数据节点,那么一旦选上它作为主节点了,这时主节点的工作压力将会非常大,出现脑裂现象的概率就增加了
- 减少误判:配置主节点的响应时间,在默认情况下,主节点3秒没有响应,其他节点就认为主节点宕机了,那我们可以把该时间设置得长一点,该配置是:discovery.zen.ping_timeout : 5
- 选举触发:设置选票超过 ( 候选主节点数量 + 1 )/ 2 才能当选为主,因此候选主节点节点数量最好是奇数。对应配置项是
discovery.zen.minimum_master_nodes
,在es7.0以后,已经成为默认配置
4.3.13 说一说ES的集群节点职责
Elasticsearch中集群节点有不同的职责划分:
4.3.14 说一说ES的集群架构
单机的Elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。Elasticsearch集群采用的是索引分片和副本机制解决了这两个问题 , 还利用集群实现了自动故障恢复
- 索引分片:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
- 副本机制:将分片数据在不同节点备份(replica ) , 而且为了避免集群出现故障, 主分片数据和副本分片数据受损, 副本采用的是交叉备份的形式 , 副本分片和主分片分离 , 在其他节点备份
- 自动故障恢复 : 当主分片所在节点宕机 , 会自动从副本分片中选举一个副本升级为master , 并重新在其他节点新建副本 ,保证数据安全 , 如果副本分片节点宕机 , 会重新在其他分片新建副本
先根据节点的clusterStateVersion比较,clusterStateVersion越大,代表数据越完整 , 优先级越高
clusterStateVersion相同时,其内部按照节点的Id比较(Id为节点第一次启动时随机生成)
4.3.15 Elasticsearch如何实现分组聚合
Elasticsearch中聚合常见的有三类:
- 桶(Bucket)聚合:用来对文档做分组
-
- TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
-
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求max、min、avg、sum等
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
聚合在DSL中的关键字为aggs
, 需要制定聚合名称, 聚合方式 , 聚合字段 , 聚合结果集数量等数据
GET /hotel/_search
{
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { //给聚合起个名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选择term
"field": "brand", // 参与聚合的字段
"size": 20 // 希望获取的聚合结果数量
}
}
}
}
在Java代码中我们使用的是RestHighLevelClient
实现的聚合查询 , API方法叫做aggregation
, 一般使用就是request.source().aggregation()
, 里面通过AggregationBuilders
来构建聚合参数 , 通过SearchResponse
获取到聚合结果
4.3.16 自定义排序(函数因子排序)怎么用
默认情况下使用Elasticsearch
搜索 , 返回的结果集会按照相关性算分得到的分值 , 从高到低排序 , 也就是关联性越高的排名越靠前 , 如果想指定排序规则, 主要有二种方式 :
- 基础排序 : 使用
Elasticsearch
提供的sort
关键字排序, 可以指定排序的字段和方式 , 也支持地理位置的距离排序 , 但是使用sort
排序, 会导致相关性算分失效 - 使用
function score
算分函数 , 可以按照规则重新对文档的相关性进行算分 , 得到结果后 , 按照算分结果排序返回
4.3.17 如何修改Elasticsearch索引结构
Elasticsearch索引一旦创建是不允许修改结构的 , 但是可以新增字段 , 如果确实需要修改索引结构 , 可以考虑删除索引 , 然后重建索引 , 但是删除之后索引中的数据也就没有了 , 如果需要保留数据 , 并且让数据按照新索引的规则进行存储 , 那么就需要做一些额外的事情了 :
首先新建一个新的索引 , 使用的是最新的索引结构和映射
优化 : 将refresh_interval(刷新周期)设置为-1,将number_of_replicas(副本数量)设置为0,可以提高索引效率
其次使用reindex
指令将旧索引的数据复制到新索引
POST _reindex
{
"source": {
"index": "twitter"
},
"dest": {
"index": "new_twitter",
"version_type": "internal"
}
}
将refresh_interval和number_of_replicas重置为旧索引中使用的值
最后删除旧索引 , 将旧索引名的别名添加到新索引中即可
POST _aliases
{
"actions": [
{
"add": {
"index": "employee",
"alias": "emp"
}
}
]
}
4.3.18 ElasticSearch性能优化 ?
面试问题 :
- 现在有10亿的数据,让你去做查询, 你怎么去设计呢
- ES在数据量很大的情况下(数十亿级别)如何提高查询效率
文件系统缓存
我们往ES中写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动加载到文件系统缓存(filesystem cache)
ES的搜索引擎非常依赖于底层的文件系统缓存( filesystem cache),如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的索引数据文件,那么在搜索的时候就基本都是走内存的,性能会非常高
性能差距究竟可以有多大?
我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1秒、5秒、10秒。但如果是走 filesystem cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等
根据我们自己的生产环境实践经验,最佳的情况下,是仅仅在 ES 中就存少量的数据,就是我们要用来搜索的那些索引,如果内存留给文件系统缓存的是 20G,那么你就将索引数据控制在 20G 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内
比如说我们现在有一行数据。id,name,age .... 30 个字段。但是搜索只需要根据 id,name,age 三个字段来搜索
如果我们往ES里写入一行数据所有的字段,就会导致 90% 的数据是不用来搜索的,结果硬是占据了ES机器上的文件系统缓存的空间,单条数据的数据量越大,就会导致文件系统缓存能缓存的数据就越少。
其实,仅仅写入ES中要用来检索的少数几个字段就可以了,比如说就写入id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql里 , 先从ES中获取到满足条件的这些数据的ID , 再根据ID取数据库中查询详情 , 走主键查询数据库的效率也非常不错
数据预热
提前将系统中的热点数据加载到文件系统缓存 , 对于那些我们觉得比较热门的,经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能会好一些
冷热分离
ES可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据(冷数据),单独写一个索引,然后将访问很频繁的热数据单独写一个索引
冷热数据分离实现 :
ElasticSearch在部署节点的时候能够指定该节点的类型 node.roles: [ data_content ]
- data_content : 内容数据节点
- data_hot : 热数据节点
- data_warm : 温数据节点
- data_cold : 冷数据节点
- data_frozen : 冻结数据节点
在实现冷热数据分离的时候
首先需要分配冷热数据节点 : node1 node2 node3 是热数据节点 node4 , node5 , node6是冷数据节点
其次 , 创建索引, 指定冷热数据区分规则, 例如 : 按照时间区分冷热数据 , 按周 , 按月
最后通过定时任务将历史索引分配到 cold 节点下。最新索引保存在hot节点实现冷热数据分离
如果做测彻底最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在文件系统缓存里,别让冷数据给冲刷掉 , 通过定时任务将热数据索引中的数据刷新到冷数据索引中
文档设计
对于 MySQL,我们经常有一些复杂的关联查询。在 ES里也支持类似的操作,但是ES里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。
最好是先在 Java 系统里就完成关联查询 , 数据封装,将关联好的数据直接写入ES 中。搜索的时候,就不需要利用ES的搜索语法来完成 join 之类的关联搜索了。
document 文档设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。ES能支持的操作就是那么多,不要考虑用ES做一些它不好操作的事情。
如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差
分页性能优化
ElasticSearch默认采用的分页方式是from+ size
的形式,在深度分页的情况下,这种使用方式效率是非常低的 , 在分布式查询的时候 , ElasticSearch需要在各个分片上匹配排序并得到from+ size
条数据,协调节点拿到这些数据再进行汇总然后进行排序等处理,然后结果集中取最后10条数据返回。
这样当页码比较大的时候 , 假如说10000页 , 每页展示10条数据 , 就需要去每个节点查询100010条数据 , 然后再汇总排序 , 获取最后10条数据 , 效率非常低
所以ElasticSearch为了性能,限制了我们分页的深度,es目前支持的最大的 max_result_window = 10000;也就是说我们不能分页到10000条数据以上 , 出国from+size
值超过10000就会报深度分页问题
解决方案主要有三种 :
- 从业务上进行限制 , 只让用户访问前100或者200页数据 , 像百度和JD都是这样的 , 翻到一定的页码后就不允继续查询了
- 采用
Search After
机制 , 对业务数据进行排序 , 每次查询下一页的时候, 根据上一页的最后一条结果的值开始向下进行查询 , 这样就通过条件将前面的数据过滤掉了 ,起始索引永远是0 - 使用scroll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来scroll会将排序后的文档id形成快照,保存在内存 , 目前官方已经不建议使用
我们项目中采用的是Search After
机制