前言

作为程序员入职一家新公司,当你看到前任程序员写的代码的时候,你是不是经常有这样的感觉:我屮艸芔茻!这代码真他喵的烂!

在这里插入图片描述

问题

对于程序员来说,代码水平良莠不齐比较常见的事情,之所以代码质量不高,一方面原因是自身不太关注于代码的整体管理,另一方面原因是经验不足。实际上在处理公司业务的时候,这种前人挖坑,后人填坑的事情时有发生。

对,我们今天暂时不讨论代码规范的问题,而是说一说,再处理搜索业务的时候,如果你已经接手了一些无法描述的代码,咱们怎么补救。

首先对于任何问题而言,预防问题发生永远胜于问题发生之后再对其进行补救,这是一定的。不过既然问题已经发生,我们就只能以最小代价或者针对自身情况选择处理方式,比如,我们可以选择是先治标还是先治本,最主要区别在于见效速度。下面我们分别来说一下这两种解决方案的区别和适用场景:

常见场景

场景一

字段类型错误:这种情况一般发生在项目初期对业务的预估错误或者技术负责人对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_weektime,其中day_of_week利用运行时字段中执行脚本进行动态计算,从而得出每天分别是一周内的星期几。这种用法可用于各种其他复杂的运算。

time字段则是对原有字段进行重新映射,改变其原有字段的类型和其他属性,如format,使其原本不支持的时间类型变为支持。