JAVA八股文面试必会-分布式框架-4.3 分布式搜索

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_smart
  • tokenizer 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就会报深度分页问题

解决方案主要有三种 :

  1. 从业务上进行限制 , 只让用户访问前100或者200页数据 , 像百度和JD都是这样的 , 翻到一定的页码后就不允继续查询了
  2. 采用Search After机制 , 对业务数据进行排序 , 每次查询下一页的时候, 根据上一页的最后一条结果的值开始向下进行查询 , 这样就通过条件将前面的数据过滤掉了 ,起始索引永远是0
  3. 使用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在使用的时候都代表要满足条件 , 但是又有些区别 :

  1. filter : 只根据搜索条件过滤出符合的文档, 将这些文档的评分固定为1, 不计算相关度分数
  2. query : 先查询符合搜索条件的文档, 然后计算每个文档对于搜索条件的相关度分数, 再根据评分倒序排序

从效率来说 :

  1. filter 性能更好, 不计算相关度分数, 不用根据相关度分数进行排序, 同时ES内部还会缓存比较常用的filter的数据 , 所以效率比较高
  2. query 性能较差, 要计算相关度分数, 要根据相关度分数进行排序, 并且没有缓存功能

使用场景 :

  1. 如果对搜索结果有排序的要求, 要将最匹配的文档排在最前面, 就用 query
  2. 如果只是根据一定的条件筛选出部分数据, 不关注结果的排序, 就用 filter

4.3.7 text 和 keyword类型的区别?

text类型代表需要分词的文本 , 支持模糊查询 , 不支持排序和聚合查询

keyword代表不需要分词的文本 , 支持精确查询 , 支持排序和聚合

4.3.8 Elasticsearch中常用的查询方式有哪些

Elasticsearch中提供的查询方式有很多 , 例如 :

  1. termsQuery : 词条查询, 一般用于keyword类型 , 直接根据条件不分词查询
  2. matchQuery : 分词查询 , 一般用于text类型 , 会对用户输入的条件先进行分词 ,分成词条之后再进行查询
  3. multiMatchQuery : 分词查询 , 一般用于text类型 , 根据多个字段分词查询
  4. prefixQuery : 前缀查询 , 查询指定前缀开头的数据
  5. regexpQuery : 正则查询 , 查询匹配正则表达式的数据
  6. rangeQuery : 范围查询 , 一般用于数值 , 日期等类型 ,, 查询范围之内的数据
  7. wildcardQuery : 模糊查询, 类似于mysql中的like
  8. queryStringQuery : 和match类似,但是match需要指定字段名,query_string不需要指定字段名,是在所有字段中搜索,范围更广泛
  9. boolQuery : 布尔查询 , 将多个基础查询条件拼接一起查询
  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分
  1. functionScoreQuery : 算分函数查询 , 指定算分函数规则 , 会对匹配条件的数据进行重新算分, 一般用于竞价排名

  1. geoDistanceQuery : 地理位置 , 半径范围查询 , 指定中心点和半径, 查询在半径范围内的数据
  2. geoBoundingBoxQuery : 地理位置矩阵范围查询 , 指定左上坐标和右下坐标 , 查询在矩阵范围内的数据

4.3.9 term和match的区别

  • term:代表完全匹配,也就是精确查询,搜索前不会再对搜索词进行分词,直接对搜索词进行查找
  • match:代表分词匹配,搜索前会对搜索词进行分词,然后按搜索词匹配查找

4.3.10 描述一下 Elasticsearch 搜索的过程

Elasticsearch的查询分成两个阶段 :

  1. scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
  2. gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户

4.3.11 描述⼀下Elasticsearch索引文档的过程

Elasticsearch是一个分片集群架构 , 在新增文档的时候会通过hash算法来计算文档应该存储到哪个分片:

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

举个例子 : 新增一个id=1的文档 , 他的流程如下 :

  1. 新增请求发送给协调节点
  2. 协调节点对id做hash运算,得到分片编号 , 获取该分片主分片所在节点
  3. 请求路由到主分片所在节点
  4. 主分片所在节点保存文档数据到主分片
  5. 同步给副本分片,副本分片保存文档数据
  6. 返回结果给协调节点 , 协调节点返回新增结果给客户端

4.3.12 说一说ES的脑裂问题 

在ElasticSearch集群初始化或者主节点宕机的情况下,由候选主节点中选举其中一个作为主节点

当主节点负载压力过大,或者集群环境中的网络问题,导致其他节点与主节点通讯的时候,主节点没来及响应,这样的话,某些节点就认为主节点宕机,重新选择新的主节点,集群中就出现了两个主节点的情况,这种情况就称之为脑裂现象

出现脑裂问题的原因一般有如下几种 :

  1. 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master
  2. 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  3. 内存回收:data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应

知道了问题 , 解决方案也就知道了 , 常用的解决方案有以下几种 :

  1. 角色分离:即master节点与data节点分离,限制角色;数据节点时需要承担存储和搜索的工作的,压力会很大。所以如果该节点同时作为候选主节点和数据节点,那么一旦选上它作为主节点了,这时主节点的工作压力将会非常大,出现脑裂现象的概率就增加了
  2. 减少误判:配置主节点的响应时间,在默认情况下,主节点3秒没有响应,其他节点就认为主节点宕机了,那我们可以把该时间设置得长一点,该配置是:discovery.zen.ping_timeout : 5
  3. 选举触发:设置选票超过 ( 候选主节点数量 + 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搜索 , 返回的结果集会按照相关性算分得到的分值 , 从高到低排序 , 也就是关联性越高的排名越靠前 , 如果想指定排序规则, 主要有二种方式 :

  1. 基础排序 : 使用Elasticsearch提供的 sort关键字排序, 可以指定排序的字段和方式 , 也支持地理位置的距离排序 , 但是使用sort排序, 会导致相关性算分失效
  2. 使用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就会报深度分页问题

解决方案主要有三种 :

  1. 从业务上进行限制 , 只让用户访问前100或者200页数据 , 像百度和JD都是这样的 , 翻到一定的页码后就不允继续查询了
  2. 采用Search After机制 , 对业务数据进行排序 , 每次查询下一页的时候, 根据上一页的最后一条结果的值开始向下进行查询 , 这样就通过条件将前面的数据过滤掉了 ,起始索引永远是0
  3. 使用scroll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来scroll会将排序后的文档id形成快照,保存在内存 , 目前官方已经不建议使用

我们项目中采用的是Search After机制

  • 40
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吉迪恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值