几个elasticsearch使用经验

今天写几个近期ES项目的经验,供大家参考。

1,建库

强调一下,一定要用alias!

这样当mapping有不兼容的升级时,可以实现线上0停机切换alias指向新库。

使用alias后,一定不要随便delete alias!因为你一旦把alias删了,那么index请求就会自动创建与alias同名的mapping出来。正确做法应该是update alias先指向新库,然后再删除旧的index。

2,索引mapping

这块主要谈一下表结构规划的思路。

我的项目涉及到多种类型的文章,它们混合索引在一个mapping中。

对于各个类型通用的字段,可以直接放在mapping最外层。

2.1,object字段集合

对于相关性很高的一组字段,可以放在一个object字段里聚集起来,这样数据之间的关系就一目了然了。

比如A类型文章的独有属性,可以放在一个a_detail的object中;B类型文章的独有属性,可以放在一个b_detail的object中。

2.2,嵌套nested

如果需要保存object数组,则大胆的使用nested类型即可。

根据我的经验,它对query和aggregation的支持比较完善,没有遇到无法实现的功能。

2.3,禁用自动mapping

一定要给type(表)、object(对象)、nested(嵌套)三种类型配置dynamic属性:

  • 建议dynamic: false,当出现mapping未知字段时,不会更新mapping,但是index请求可以成功,未知字段会出现在_source中。
  • 超严格则dynamic: strict,对于未知字段,index请求将直接失败,抛出异常。

2.4,时间格式

用date字段,一定要指定format:

这样就可以存储字符串或者毫秒的时间进去了。

但是注意,如果传的是字符串格式,那么ES会按UTC时区转成毫秒时间戳,然后建立倒排索引,这样索引中的时间戳实际上少了8个小时(中国时区的偏移量)。

在这种情况下,后续的查询一定要用字符串格式的时间,保持”将错就错”仍旧可以正常工作。

此时,_source中的内容仍旧是你索引时Json中的时间字符串,并不会受到时区的影响。

date最佳实践参考:https://blog.csdn.net/feifantiyan/article/details/54669583

2.5,索引时间

因为数据同步到ES可能有丢失率,为了便于排查,请在mapping中维护一个index_time字段,记录为每次更新document的时间点,便于排查数据丢失问题。

3,查询

几乎所有的查询都走了bool的filter,在filter中可以嵌套子bool,多多利用filter有利于ES缓存结果。

3.1,索引时计算 or 查询时计算

因为有多种类型的文章,业务中有很多混合流,对不同类型文章的过滤规则各不相同,非常具体。

一开始曾试图将不同类型文章进行业务策略的梳理,即索引时计算出策略字段(相当于离线计算),这样查询时只需要直接过滤策略字段即可得到结果。但后来发现,混合流的业务规则非常灵活,后期产品策略随时会调整,所以决定基于业务属性查询时进行实时过滤。

在这种情况下,最好的建表方式就是把通用的字段提取出来,不通用的字段分离存储到不同的object字段中。

这样在实现混合流时,可以通过where (type=a and xxx=1 and yyy=2) or (type=b and mmm=1 and nnn=2)这种嵌套bool,实现对不同类型的各自过滤规则。

从ES语法角度来说,即外层bool should中嵌套多个子bool实现or关系,每个子bool中通过filter实现多个字段and关系,从而对不同类型文章做不同的过滤条件。

3.2,短语匹配

类似于mysql中的”%xxx%”这种模糊查询,实际上不是全文检索,而是短语匹配,即xxx需要完整的出现在目标字段中,而不是某个分词出现过。

所以,这种情况不要用match,用match_phrase一般是可以的,如果搜索词不分词也可以用term query。

3.3,painless打分脚本

混合流除了普通时间排序,大多需要策略排序。

策略排序是PM给规则,往往都需要写painless脚本实现打分的计算逻辑。

使用painless的时候比较坑的就是类型问题。

painless是动态语言,强类型,并不是弱类型,并且提供了def关键字,类似于c++中的auto,可以容纳任何类型的数据。

我们index索引json时,如果字段是Integer而你放进去的是”123″,那么ES会在构建倒排时做类型转换为integer,而在_source中仍旧是”123″。

此时,doc[“xxx”]取出来的一定是123,而params._source[“xxx”]则是”123″。

具体的关系我写在另外一篇博客中,大家特别注意即可:《painless获取doc字段的方式》

3.4,自定义排序

在混合流中,业务策略要求A类文章按发布时间,B类文章按更新时间,综合排序。

即不同类型的文章,排序依据的字段不同。

这种情况用不到script field,只能用script sort实现,目前就我所知一旦使用script sort,就不能传多个sort条件了(多个条件是指:order by a asc, b desc),所以复杂排序比较鸡肋,可能还是得走相关性打分脚本解决。

顺便一提,script sort的type只支持数字和字符串,即你的排序脚本只能return这两种类型的值,供ES排序用。

4,聚合

聚合主要分清概念即可,有bucket agg与metric agg之分,可以无限嵌套下去。

比较新鲜的是nested bucket agg用来处理nested字段的分桶,filtered bucket agg用来在agg阶段做数据过滤(不同于query阶段的过滤)。

根据业务特点,尽量对聚合结果做一定时间的cache,因为这种聚合统计的计算量真的很大。

5,业务 -> 搜索 数据同步

这一块有2种选择:

  • 业务按照mapping拼好json,推给搜索服务,搜索服务直接入库
  • 业务将变化的数据ID通知给搜索,搜索拿着ID主动去业务拉取需要的数据,拼装出json

建议选择后者,原因有2个:

  • 最终一致性:业务仅仅发出通知,搜索主动拉取数据,这样搜索总是可以拿到最新的数据。
  • 控制权:毕竟是搜索服务直接与ES交互,因此搜索服务应该对数据具有绝对控制权,去各个业务系统获取所需数据拼装json。

即便采取了拉模式,ES仍旧有可能出现旧数据覆盖新数据的情况:2个并发拉取,谁后写到ES就算谁的。

这种问题可以用ES的版本控制解决,需要业务提供数据的版本号,不是太重要的业务数据可以忽略这个竞争问题。

6,搜索接口设计

其实给业务提供接口,一般是给某个页面提供具体服务。

感觉下来的话,还是很难在开始阶段就设计出一些通用的灵活接口,所以我并不建议在共性不明显的情况下做通用大接口的设计,先解决业务需求。

暂时能想到的就这些,希望对大家有所帮助。

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~