Elasticsearch【6.7】 使用float类型字段聚合时出现精度丢失

3,126 阅读2分钟

起因

使用float类型字段去做聚合时会出现精度丢失,与在 _source 中看到的不符

复现

PUT /term-test
{
  "settings": {
    "refresh_interval": "1s",
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "demo_type": {
      "properties": {
        "float_field": {
          "type": "float"
        }
      }
    }
  }
}

POST term-test/demo_type
{
  "float_field": 0.06
}
POST term-test/demo_type
{
  "float_field": 0.06000004
}

GET term-test/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "aa": {
      "terms": {
        "field": "float_field"
      }
    }
  }
}

返回结果
"hits" : [
      {
        "_source" : {
          "float_field" : 0.06
        }
      },
      {
        "_source" : {
          "float_field" : 0.06000004
        }
      }
    ]
    
"aggregations" : {
    "aa" : {
      "buckets" : [
        {
          "key" : 0.05999999865889549,
          "doc_count" : 1
        },
        {
          "key" : 0.060000039637088776,
          "doc_count" : 1
        }
      ]
    }
  }

原因

  1. _soucre 返回的是插入时的原值,不会做更改,即使字段在mapping类型与插入的实际类型不一致,也不会做任何处理,所以实际做聚合的值与 _source 里的值有可能会不一样。
  2. mapping里有一个字段属性 doc_values,默认值为true,具体作用可见文档(doc-values),下面引用部分:

Doc values are the on-disk data structure, built at document index time, which makes this data access pattern possible. They store the same values as the _source but in a column-oriented fashion that is way more efficient for sorting and aggregations. Doc values are supported on almost all field types, with the notable exception of analyzed string fields.

简单来说ES为了聚合或排序时效率更好,把字段值以另外的方案存储起来,做聚合或者排序等操作时会使用这个值,这个值可以通过下面的方式查看(Doc value Fields

GET term-test/_search
{
  "docvalue_fields": ["float_field"], 
  "query": {
    "match_all": {}
  },
  "aggs": {
    "aa": {
      "terms": {
        "field": "float_field"
      }
    }
  }
}

返回结果,注意观察 _source 和 fields 里的字段值
"hits" : [
      {
        "_source" : {
          "float_field" : 0.06
        },
        "fields" : {
          "float_field" : [
            0.05999999865889549
          ]
        }
      },
      {
        "_source" : {
          "float_field" : 0.06000004
        },
        "fields" : {
          "float_field" : [
            0.060000039637088776
          ]
        }
      }
    ]

"aggregations" : {
    "aa" : {
      "buckets" : [
        {
          "key" : 0.05999999865889549,
          "doc_count" : 1
        },
        {
          "key" : 0.060000039637088776,
          "doc_count" : 1
        }
      ]
    }
  }

解决方案

目前发现有两种解决方案来保证聚合结果的精度

  1. 字段改为使用 double 类型
  2. 字段改为使用 scaled_float 类型,同时还要设置都一个字段 scaling_factor,用来配置需要保留的精度,可以查看文档 scaled_float

但是以上的两种方式相对 float 类型都会增加存储空间,并且效率不如 float 类型,这也是es的Dynamic mapping里默认浮点数对应的类型是 float 的原因,Dynamic field mapping

拓展

查阅资料时还获取到了一些额外的信息

  1. _source里的内容不受mapping的影响,只会跟插入时一致
  2. doc_values会受mapping影响,比如说mapping的类型是integer,但是插入的值是浮点型,例如是1.7,实际保存的会是 1
  3. 倒排索引储存的字段值也会受mapping影响,所以如果query条件是大于1,那么也不会返回这条 document
  4. mapping字段有一个属性store,默认是false,如果设置为true时会额外存储这个字段的值,同样存储的值也会受mapping影响,文档:store,可以通过这种方式去获取这个值:stored_fields

下面是一些关于希望ES支持 BigDecimal 来保证浮点数精度的讨论

ps: 太长了有些没看完

ES支持的数值类型

SearchHit returns 'Double' value type instead 'Float'

Add BigDecimal data type

Support for BigInteger and BigDecimal