ES不支持decimal,如何避免丢失精度?

47 阅读5分钟

针对 Elasticsearch 不原生支持 decimal 类型的问题,以下是避免精度丢失的解决方案和最佳实践,结合实际场景分步说明:


1. 使用 scaled_float 类型(推荐方案)

原理:通过缩放因子(scaling_factor)将小数转换为整数存储,避免浮点数精度丢失。
适用场景:金额、比例等需要固定小数位的数值。

操作步骤

  1. 定义 Mapping
    在创建索引时,明确指定 scaled_float 类型,并设置合适的缩放因子(如金额通常放大 100 倍):

    PUT /financial_data
    {
      "mappings": {
        "properties": {
          "amount": {
            "type": "scaled_float",
            "scaling_factor": 100  // 存储时放大100倍(如 123.45 → 12345)
          }
        }
      }
    }
    
  2. 数据写入
    写入数据时,将原始值乘以 scaling_factor 后转为整数:

    POST /financial_data/_doc
    {
      "amount": 12345  // 实际值 123.45
    }
    
  3. 数据查询与聚合

    • 查询时需手动处理缩放因子:
      GET /financial_data/_search
      {
        "query": {
          "range": {
            "amount": {
              "gte": 10000,  // 对应 100.00
              "lte": 20000   // 对应 200.00
            }
          }
        }
      }
      
    • 聚合结果需在应用层还原精度:
      GET /financial_data/_search
      {
        "aggs": {
          "total_amount": {
            "sum": {
              "field": "amount"
            }
          }
        }
      }
      
      结果需除以 100"total_amount": { "value": 12345 } → 123.45

优点

  • 支持数值计算(如聚合、排序)。
  • 存储紧凑(以整数形式存储,节省空间)。

缺点

  • 需在应用层处理缩放逻辑。
  • 缩放因子需提前确定,修改成本高。

2. 存储为字符串(精确但牺牲计算能力)

原理:将数值以字符串格式存储,完全保留精度,但无法直接参与数值运算。
适用场景:仅需精确存储和检索(如订单号、ID),无需计算。

操作步骤

  1. 定义 Mapping

    PUT /financial_data
    {
      "mappings": {
        "properties": {
          "amount": { "type": "keyword" }
        }
      }
    }
    
  2. 数据写入
    直接写入原始字符串:

    POST /financial_data/_doc
    {
      "amount": "123.4567890123456789"
    }
    

优点

  • 绝对精度保留。
  • 简单易用,无需额外处理。

缺点

  • 无法直接进行范围查询、排序或聚合。
  • 内存占用较高(字符串比数值类型更占空间)。

3. 拆分整数和小数部分(灵活但复杂)

原理:将数值拆分为整数部分和小数部分分别存储,通过组合还原原始值。
适用场景:需要高精度且允许复杂查询逻辑的场景(如科学计算)。

操作步骤

  1. 定义 Mapping

    PUT /financial_data
    {
      "mappings": {
        "properties": {
          "integer_part": { "type": "long" },
          "decimal_part": { "type": "integer" },
          "decimal_places": { "type": "short" } // 小数位数
        }
      }
    }
    
  2. 数据写入
    例如存储 123.4567

    POST /financial_data/_doc
    {
      "integer_part": 123,
      "decimal_part": 4567,
      "decimal_places": 4
    }
    
  3. 查询与计算

    • 范围查询需组合字段:
      GET /financial_data/_search
      {
        "query": {
          "script": {
            "script": {
              "source": """
                long total = (doc['integer_part'].value * Math.pow(10, doc['decimal_places'].value)) + doc['decimal_part'].value;
                return total >= params.min && total <= params.max;
              """,
              "params": {
                "min": 1234567,  // 123.4567
                "max": 2000000   // 200.0000
              }
            }
          }
        }
      }
      

优点

  • 灵活控制精度和小数位数。
  • 支持精确计算(需应用层处理)。

缺点

  • 查询复杂度高,性能较差。
  • 数据模型冗余,维护成本高。

4. 结合外部系统(混合架构)

原理:将高精度数据存储在关系型数据库(如 MySQL),仅将搜索相关字段同步到 ES。
适用场景:需要兼顾精确计算和全文搜索的场景(如电商价格+商品描述)。

操作步骤

  1. 数据存储

    • MySQL 存储完整高精度数据:
      CREATE TABLE products (
        id INT PRIMARY KEY,
        price DECIMAL(20, 6), -- 高精度价格
        description TEXT
      );
      
    • ES 存储搜索字段和低精度副本:
      PUT /products
      {
        "mappings": {
          "properties": {
            "id": { "type": "keyword" },
            "price_approx": { "type": "scaled_float", "scaling_factor": 100 }, -- 近似值
            "description": { "type": "text" }
          }
        }
      }
      
  2. 混合查询

    • 使用 ES 进行全文搜索和近似范围过滤:
      GET /products/_search
      {
        "query": {
          "bool": {
            "must": [
              { "match": { "description": "手机" }},
              { "range": { "price_approx": { "gte": 10000 }}}
            ]
          }
        }
      }
      
    • 根据 ES 返回的 ID,从 MySQL 查询精确值:
      SELECT * FROM products WHERE id IN (1, 2, 3) AND price >= 100.00;
      

优点

  • 精确计算由 MySQL 处理,ES 专注搜索。
  • 平衡性能与精度需求。

缺点

  • 系统复杂度高,需维护双数据源同步。
  • 查询延迟增加(需两次查询)。

5. 使用 Painless 脚本处理(ES 7.0+)

原理:在 ES 查询中通过 Painless 脚本直接处理字符串或拆分字段的数值计算。
适用场景:需要动态计算且精度要求高的低频操作。

示例

  1. 存储为字符串

    POST /financial_data/_doc
    {
      "amount_str": "123.4567890123456789"
    }
    
  2. 脚本查询

    GET /financial_data/_search
    {
      "query": {
        "script": {
          "script": {
            "source": """
              BigDecimal value = new BigDecimal(doc['amount_str'].value);
              return value.compareTo(params.min) >= 0 && value.compareTo(params.max) <= 0;
            """,
            "params": {
              "min": 100.00,
              "max": 200.00
            }
          }
        }
      }
    }
    

优点

  • 保留原始精度,支持复杂计算。

缺点

  • 脚本执行性能较差,不适合高频查询。
  • 语法复杂,调试困难。

总结:方案选型建议

方案适用场景优点缺点
scaled_float需要数值计算(如聚合、排序)性能好,存储高效需处理缩放逻辑
字符串存储仅需精确存储和检索(如ID)绝对精度保留不支持计算,内存占用高
拆分整数和小数科学计算等高精度场景灵活控制精度查询复杂,维护成本高
混合架构(ES+MySQL)兼顾搜索与精确计算(如电商)平衡性能与精度系统复杂度高
Painless 脚本低频复杂计算(如审计)动态计算,保留精度性能差,语法复杂

推荐优先级

  1. scaled_float:适合大多数需要数值运算的场景(如金额)。
  2. 混合架构:适合对精度和搜索性能要求均高的系统。
  3. 字符串存储:适合仅需精确存储(如唯一标识符)。

关键注意事项

  1. 明确精度需求:确定需要保留的小数位数(如金融场景通常为 4-6 位)。
  2. 测试边界值:验证最大值、最小值在缩放后是否溢出(如 scaled_float 的整数范围)。
  3. 同步策略:若使用混合架构,需设计可靠的数据同步机制(如 Binlog 监听)。
  4. 文档化:记录字段的缩放因子或存储规则,避免团队误用。