前言
作为程序员入职一家新公司,当你看到前任程序员写的代码的时候,你是不是经常有这样的感觉:我屮艸芔茻!这代码真他喵的烂!
问题
对于程序员来说,代码水平良莠不齐比较常见的事情,之所以代码质量不高,一方面原因是自身不太关注于代码的整体管理,另一方面原因是经验不足。实际上在处理公司业务的时候,这种前人挖坑,后人填坑的事情时有发生。
对,我们今天暂时不讨论代码规范的问题,而是说一说,再处理搜索业务的时候,如果你已经接手了一些无法描述的代码,咱们怎么补救。
首先对于任何问题而言,预防问题发生永远胜于问题发生之后再对其进行补救,这是一定的。不过既然问题已经发生,我们就只能以最小代价或者针对自身情况选择处理方式,比如,我们可以选择是先治标还是先治本,最主要区别在于见效速度。下面我们分别来说一下这两种解决方案的区别和适用场景:
常见场景
场景一
字段类型错误:这种情况一般发生在项目初期对业务的预估错误或者技术负责人对ES本身不够了解。
很多人都知道,为了优化性能,可以适当缩减字段属性。比如确定不需要聚合或者排序的字段,可对其关闭正排索引,不需要对其进行检索的字段可关闭倒排索引,不需要评分的字段可以关闭评分功能以此可节约字段存储空间,提高效率。
另外一种情况是在创建索引的时候,对ES的字段本身不够了解,比如之前我们提到的“yyyy-MM-dd HH:mm:ss”这种格式并非默认支持的时间类型,如果一开始技术员并不知道,而将数据直接写入,有可能的结果就是,时间总段存储成为了“text”类型,而给后期埋下隐患。
场景二
上述问题是字段类型错误,还有一种情况是针对于数据本身。比如:当我们某个索引报错了不同来源或渠道的数据,每个渠道可能采集数据负责的程序员并不相同,最常见的情况比如,我们采集用户行为日志,需要涉及“APP”、“Web”、“PC”、“小程序”等来源,可能负责开发的程序员分别来自不同的部门,如果在最开始没有最好约定,可能就会出现字段或者单位不统一或者一些列其他问题。总之就是协调发生问题而导致数据不统一。这也给后期做数据分析或者统计造成很大影响。
案例
我们来看下面一个例子,创建以下映射并写入一条数据:
PUT twitter { "mappings": { "properties": { "uid": { "enabled": false } } } } POST twitter/_doc { "uid": "1" }
以上代码为我们在项目初期,负责人对将来业务的预估是不会出现通过uid 进行检索的情况,因此错误的把此字段的enabled 设置为了false, 此时数据将不能通过此字段进行检索。注意是不能通过此字段检索而不是不能将此字段检索出来,只是没有创建索引而非删除了元数据。具体体现为:下面代码第一行执行有数据,第二行无数据。
以下代码执行有结果
# 有数据 GET twitter/_search
以下代码执行无结果
# 无数据 GET twitter/_search { "query": { "term": { "uid": { "value": 1 } } } }
痛点及现状
后期经过业务的发展和迭代,逐渐发现了此字段无法检索的问题,而很多公司的选择都是Reindex
,的确,重建索引可以解决很多问题,但是这样未来过于“劳师动众!”,因为基本上ES的应用场景都是基于海量数据的索引,重建索引可能耗费大量时间,无异于杀鸡取卵,得不偿失。
有没有更好的解决方案 ?
那必须有啊!关注我就对了。
同类问题还包括如之前提到的,“yyyy-MM-dd HH:mm:ss”而导致的类型不匹配问题,在处理此一类问题时,皆可采用runtime_fields
来解决。
案例:
我们沿用之前“yyyy-MM-dd HH:mm:ss 是时间类型?别再错下去了!”提到的不是时间类型导致的错误的案例,如果没看过之前的文章,戳:传送门
假如生产环境我们有如下索引,存储了一些地震的经纬度以及发生时间和描述等信息:
需求:
写一个查询满足以下要求
- 1:按星期分桶统计地震数据
- 2:输出星期一至星期日中平均地震等级 没有数据的不显示
- 3:返回平均地震等级最大的一个 是星期几
- 4:进阶问题 每个星期的平均地震等级
- 5:进阶问题 平均地震等级最大的是哪个星期
POST task1/_bulk?refresh=true {"index":{}} {"time":"2011-06-16 12:12:21","magnitude" : 1.4, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-06-16 12:12:21","magnitude" : 1.3, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-06-17 12:12:21","magnitude" : 1.5, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-04-18 12:12:21","magnitude" : 1.6, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-06-19 12:12:21","magnitude" : 1.9, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-06-20 12:12:21","magnitude" : 2.0, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":1308544245123,"magnitude" : 2.1, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":1308717045123,"magnitude" : 2.8, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-06-20 12:12:21","magnitude" : 2.9, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"} {"index":{}} {"time":"2011-06-20 12:12:21","magnitude" : 3.3, "lon" : -116.0902, "lat" : 33.2253, "depth" : 9.98, "area" : " 10km NNE of Ocotillo Wells"}
这是我为Elastic认证考试出的一道模拟题,在此我们简化题目,只看前两个问题
但是,因为不可描述的原因,time字段有的数据存储成为了yyyy-MM-dd HH:mm:ss,有的存储成为了时间戳,最终time字段的类型并没有像预想的那样为date类型他们可能来自于同一个公司的不同部门,所以导致了这种问题。
面临的问题
此时,面对上述需求,存在两个问题
time
字段为text
而非date
类型,而导致我们无法使用日期时间类型的所有函数。- 数据存储的格式不统一,有时间戳,有"yyyy-MM-dd HH:mm:ss",无法统一计算。
解决方案:runtime_fields
运行时字段是在执行查询时动态对字段类型和索引重新定义的字段。runtime_fields
具备以下特点:
- 在不重新索引数据的情况下向现有文档添加字段
- 在不了解数据结构的情况下开始处理数据
- 在查询时覆盖从索引字段返回的值
- 为特定用途定义字段而不修改底层架构
由于运行时字段未编入索引,因此添加运行时字段不会增加索引大小。直接在索引映射中定义运行时字段,可以节省存储成本并提高预处理速度。
如果将运行时字段设为索引字段,则无需修改任何引用运行时字段的查询。而且可以引用字段是运行时字段的一些索引,以及字段是索引字段的其他索引。可以灵活地选择要索引哪些字段以及保留哪些字段作为运行时字段。
就其核心而言,运行时字段最重要的好处是能够在提取文档后将字段添加到文档中。此功能简化了映射决策,所以不需要预先决定如何解析数据,并且可以使用运行时字段随时修改映射。使用运行时字段索引更小且更快的预处理信息,这结合使用更少的资源并降低运营成本。
上述问题,解决方案代码如下:
GET task1/_search { "size": 0, "runtime_mappings": { "day_of_week": { "type": "keyword", "script": "emit(doc['time'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL,Locale.ROOT))" }, "time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" } }, "aggs": { //1:按照周统计地震信息,也就是每周有几天地震了 "week_agg": { "date_histogram": { "field": "time", "calendar_interval": "week" }, "aggs": { "week_avg_magnitude": { "avg": { "field": "magnitude" } } } }, //一周中的每一天的震级 "day_of_week_magnitude":{ "terms": { "field": "day_of_week" }, "aggs": { //2: 一周中每一天的平均地震等级 "day_of_week_avg_magnitude": { "avg": { "field": "magnitude" } } } } } }
此段代码中,其核心代码如下
"runtime_mappings": { "day_of_week": { "type": "keyword", "script": "emit(doc['time'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL,Locale.ROOT))" }, "time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" } }
其在runtime_mappings
中定义了两个“新字段”,即day_of_week
和time
,其中day_of_week
利用运行时字段中执行脚本进行动态计算,从而得出每天分别是一周内的星期几。这种用法可用于各种其他复杂的运算。
而time
字段则是对原有字段进行重新映射,改变其原有字段的类型和其他属性,如format
,使其原本不支持的时间类型变为支持。