ES 添加字段默认值

业务背景

当我们在使用多维度检索时,mysql显然已经不能满足我的的使用场景,尤其涉及到表之间的join且数据量较大时,mysql的查询性能显得捉襟见肘。
这时候ES的多维检索功能就派上用场了。我们可以将两张或者多张业务表,制作成一个比较宽的索引,监听业务的binlog,并将数据保存到ES中。
这样就可以快速的支持业务检索了。

业务需求

通常情况下,会使用ES的动态模板,之后添加其他的维度过滤会更加方便。
都知道ES底层存储的是文档,当使用POST往动态模板中添加了字段之后,之前的数据不会像mysql一样可以设置默认值。
如果产品侧又需要支持老数据的过滤时,这时候我们就涉及到刷ES索引的问题。

分析

按照数据的组织方式,将数据重新往ES插入一遍的方案肯定是不可行的,那么我们有没有命令可以类似mysql的set default值这样的方式呢?
于是我去翻阅ES的官方文档,看到update是可以支持这种操作的。下面以 es动态索引中增加type类型为例演示解决过程。要实现的将es中原始的doc添加上type=0并支持索引。

现有文档数

GET index_test/_count?pretty
{
  "count" : 2000,
  "_shards" : {
    "total" : 12,
    "successful" : 12,
    "skipped" : 0,
    "failed" : 0
  }
}

可以看到文档又2000条,

使用ES的term查询:

term其实是分桶聚合查询,可以理解为mysql的group by

POST index_test/_search?pretty
{
    "size" : 0,
    "aggs" : {
        "aggType" : {
            "terms" : {
                "field" : "type"
            }
        }
    }
}

查询结果:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 12,
    "successful" : 12,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "aggType" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 1,
          "doc_count" : 5
        },
        {
          "key" : 2,
          "doc_count" : 4
        },
        {
          "key" : 3,
          "doc_count" : 4
        }
      ]
    }
  }
}

可以看到,type=0 的数据没有,只有type = 1,2,3 新生成数据,分别为5,4,4条一共13条,与数据总条数2000差了1987条,这些数据都是老数据,无法支持该字段的检索。

使用update更新

POST index_test/_update/1
{
  "script": {
    "lang": "painless",
    "source": """
      if (ctx._source.type == null) { ctx._source.type=0 }
    """
  }
}

ES的更新支持script,这样我们在更新文档时可以根据其他的一个或几个字段确认新增字段的值,在这里我使用
如果type为空,将type赋值为默认值0。
更新完成后使用term查询结果

```json
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 12,
    "successful" : 12,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "aggType" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
          {
          "key" : 0,
          "doc_count" : 1
        }
        {
          "key" : 1,
          "doc_count" : 5
        },
        {
          "key" : 2,
          "doc_count" : 4
        },
        {
          "key" : 3,
          "doc_count" : 4
        }
      ]
    }
  }
}

发现只增加了一条,重复执行更新命令也不会再增加了,通过分析update语句发现,其命令行update后的1指的是docId。这种方式显然不能使用,我再猜想有没有类似mysql中根据条件update的语句呀?查看官方文档后,返现ES支持
update_by_query的操作

使用update_by_query

使用update_by_query语句,在这里我删除了script中的条件判断,改成使用query

POST index_test/_update_by_query
{
    "script": {
        "lang": "painless",
        "source": "ctx._source.type=0"
    },
    "query": {
        "bool": {
            "must_not": {
                "exists": {
                    "field": "type"
                }
            }
        }
    }
}

其实使用scrpit的脚本判断要比query中使用must_not要慢。我理解使用script要access all 全表扫描。
如果使用了must_not 而且只有一个条件,我理解ES的执行引擎会使用倒排索引,查询出有的type字段的,然后取反,把不存在type字段的doc ID返回。根据id去逐条更新,这样判断的次数从O(n)降到了理论的O(1)。
待更新结果返回后,重新使用term查询结果:

POST index_test/_search?pretty
{
    "size" : 0,
    "aggs" : {
        "aggType" : {
            "terms" : {
                "field" : "type"
            }
        }
    }
}

查询结果:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 12,
    "successful" : 12,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "aggType" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
          {
          "key" : 0,
          "doc_count" : 1987
        }
        {
          "key" : 1,
          "doc_count" : 5
        },
        {
          "key" : 2,
          "doc_count" : 4
        },
        {
          "key" : 3,
          "doc_count" : 4
        }
      ]
    }
  }
}

可以看到聚合结果中key=0的文档相较之前增加了,而且key = 0,1,2,3,4 的枚举加起来正好为2000.
证明更新成功了。

控制更新速度

在更新的过程中,如果要控制更新的速度,可以在更新的语句后添加参数,目前ES更新支持两个方式

按照索引分片更新

POST index_test/_update_by_query?routing=1

其中routing为集群的第几个分片。

  • 优点:单分片更新,如果分片被更新坏了,可以找运维删除分片,副本分片会主动替换主分片,并重新分配副本分片,在这期间索引的状态可能是黄色。
  • 缺点:更新不是原子的,而且需要清楚集群主分片数才能操作。

按照分页更新

POST index_test/_update_by_query?scroll_size=10000

其中scroll_size的最大值为集群配置的允许的最大值,可以通过_settings命令查询。

  • 优点:可以控制集群中数据的更新速度,降低修复数据时,集群的负载。
  • 缺点:需要判断使用合理的分页,一旦集群崩溃就会影响线上环境。

触类旁通

ES集群使用的SSD的硬盘,而且对内存要求较高,当集群的存储超过一半时(超过了一半ES就无法再实现段合并了,高并发写入会产生较多分段Segment)。一半情况下,业务的数据都是按照日期存储的,这时候我们可以把较早的数据备份到HDFS系统上,然后在ES的集群上执行delete_by_query删除部分历史数据,这样可以使ES集群一直处于比较好的性能阶段。

posted @ 2021-11-21 22:18  arax  阅读(1837)  评论(0编辑  收藏  举报