4. Mapping与Setting调优:动态映射与显式映射、字段类型选择、索引刷新间隔设置

好,咱们进入第四章。这一章聊的是Mapping和Setting调优。说白了,就是告诉Elasticsearch:你的数据长什么样,该怎么存,多久刷一次盘。

很多新手上来就一把梭,把数据全扔进去,让ES自己猜字段类型。嗯,这在开发环境玩玩还行,上了生产,分分钟让你吃瘪。我个人习惯是:能用显式映射,就别用动态映射。为什么?往下看。

4.1 动态映射 vs 显式映射

动态映射,就是ES根据你第一条数据的格式,自动推断字段类型。比如你传了个"age": 25,它自动给你设成long类型。听起来挺智能对吧?

但我在项目中遇到过这么个坑:有个日志字段,大部分时候传的是数字,偶尔传了个字符串"unknown"。结果ES直接报错,整条数据写不进去。你想想看,就因为一条异常数据,导致整个索引写入失败,这谁受得了?

避坑指南:我曾经在一个电商项目中,因为动态映射把价格字段误判成了text类型,导致聚合查询结果全乱套。排查了一下午才发现是映射问题。从那以后,我所有生产环境都强制使用显式映射。

显式映射的好处很明显:

  • 类型可控:字段是什么类型,你说了算
  • 性能可预期:不会因为类型推断错误导致写入失败
  • 存储可优化:可以针对性地设置分词器、索引选项

那什么时候用动态映射?我个人觉得,只有一种场景:数据量极小,且字段结构完全不可预知的探索性分析。比如你临时导入一批数据看看大概情况,用完就删的那种。

来看个显式映射的例子:

PUT /my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "price": {
        "type": "float"
      },
      "create_time": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss"
      },
      "tags": {
        "type": "keyword"
      }
    }
  }
}

看到没?每个字段我都明确指定了类型。这样ES就知道该怎么处理这些数据,不会自作主张。

4.2 字段类型选择:keyword vs text

这是面试高频题,也是实际开发中最容易踩坑的地方。我简单说下区别:

特性 text keyword
分词 会分词,支持全文搜索 不分词,作为一个整体
聚合 不支持(需要fielddata) 原生支持
排序 不支持 支持
精确匹配 不支持(term查询效率低) 完美支持
典型场景 文章内容、商品描述 标签、ID、状态码、邮箱

说白了,需要全文搜索的用text,需要精确匹配、聚合、排序的用keyword。就这么简单。

但我见过太多人把ID字段设成text类型。你想想看,一个订单ID"ORD-2024-001",被分词器拆成"ORD"、"2024"、"001"三个词,然后你用term查询"ORD-2024-001"反而查不到。这不是给自己挖坑吗?

我的建议:如果你不确定某个字段该用啥类型,先问自己三个问题:
1. 这个字段需要模糊搜索吗? → 用text
2. 这个字段需要做group by吗? → 用keyword
3. 这个字段需要排序吗? → 用keyword
如果三个问题答案都是"否",那大概率用keyword就对了。

还有一种常见做法是multi-fields,即一个字段同时保留text和keyword两种类型:

PUT /my_index
{
  "mappings": {
    "properties": {
      "product_name": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

这样你就可以用product_name做全文搜索,用product_name.keyword做精确匹配和聚合。一举两得。不过要注意,这会增加存储开销,每个文档多存一份数据。

4.3 索引刷新间隔设置

刷新间隔(refresh_interval),是ES里一个容易被忽视但影响巨大的参数。它决定了数据写入后多久可以被搜索到。

默认值是1s,也就是每秒刷新一次。这意味着你写入一条数据,最多等1秒就能搜到。听起来很爽对吧?

但代价是什么?写入性能。每次刷新都会生成一个新的段(segment),频繁刷新会导致段数量暴增,合并压力变大,写入吞吐量下降。

我记得有一次帮一个客户做性能调优,他们的写入TPS只有2000,怎么都上不去。我一看,refresh_interval设的是默认1s。改成30s后,TPS直接飙到8000。你想想看,就改了一个参数,性能翻了4倍。

核心原则:刷新间隔越短,实时性越好,但写入性能越差。反之亦然。你需要根据业务场景做权衡。

常见的设置策略:

  • 日志场景:不需要实时搜索,可以设到30s-60s,甚至更大
  • 电商场景:商品上架后需要尽快被搜到,设到5s-10s
  • 监控告警:需要秒级响应,保持1s默认值
  • 批量导入:可以先设为-1(禁用刷新),导入完成后再恢复

来看怎么动态调整:

// 设置刷新间隔为30秒
PUT /my_index/_settings
{
  "index": {
    "refresh_interval": "30s"
  }
}

// 批量导入时禁用刷新
PUT /my_index/_settings
{
  "index": {
    "refresh_interval": "-1"
  }
}

// 导入完成后恢复
PUT /my_index/_settings
{
  "index": {
    "refresh_interval": "1s"
  }
}
注意:禁用刷新期间写入的数据,在恢复刷新之前是搜不到的。如果你在导入过程中需要查询部分数据,建议不要完全禁用,而是设一个较大的值,比如60s。

还有一个细节:刷新和刷盘是两回事。刷新只是把数据从内存缓冲区移到文件系统缓存,让数据可搜索。但数据还没落盘。真正落盘是flush操作,由translog触发。别搞混了。

嗯,这一章的内容就这些。总结一下:

  • 能用显式映射就别用动态映射,省得给自己找麻烦
  • text和keyword的选择,核心看你要不要分词、要不要聚合
  • 刷新间隔是个性能杠杆,调好了事半功倍

下一章咱们聊聊分片和副本的调优策略,那才是真正考验架构设计能力的地方。