数据库与缓存 列表全部文章
数据库与缓存

Elasticsearch 笔记:倒排索引、Mapping 设计、查询与写入调优

Elasticsearch 从原理到生产的笔记:倒排索引与分词、Mapping 字段类型怎么定、term 与 match 的区别、深分页问题、写入调优(refresh/bulk/副本)、集群健康与分片规划,附一次检索超时和一次分片失控的排查。

更多

ES 的定位是检索与分析引擎,不是数据库。用它做「搜索、模糊匹配、聚合分析」很爽,用它做「主数据存储 + 事务」很痛苦。

一、倒排索引:为什么它搜得快

正排:文档 → 内容 doc1: 小程序开发多少钱 doc2: 小程序备案流程 倒排:词 → 文档列表 小程序 → [doc1, doc2] 开发 → [doc1] 多少钱 → [doc1] 备案 → [doc2] 查询过程 查询词分词 查倒排表取交集 按 BM25 算分排序 返回 TopN 文档 代价:索引膨胀(通常是原始数据的 1–3 倍),且写入后要等 refresh_interval(默认 1s)才能被搜到 —— 这就是 ES 的近实时。

二、Mapping 设计

Mapping 一旦写入就不能改字段类型,只能加字段或重建索引。所以设计阶段要慎重。

PUT /article
{
  "mappings": {
    "properties": {
      "title":    { "type": "text", "analyzer": "ik_max_word",
                    "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } },
      "status":   { "type": "integer" },
      "tags":     { "type": "keyword" },
      "amount":   { "type": "scaled_float", "scaling_factor": 100 },
      "content":  { "type": "text", "analyzer": "ik_smart" },
      "createTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" },
      "location": { "type": "geo_point" }
    }
  },
  "settings": { "number_of_shards": 3, "number_of_replicas": 1 }
}

关键决策:

字段 类型选择 理由
需要分词的文本 text 会被分词,用于全文检索
需要精确匹配/聚合/排序 keyword 不分词,整体作为一个 term
两者都要 text + keyword 子字段(如上 title) 最常用的写法
金额 scaled_float 或 long(存分) float 有精度问题
枚举/状态 keyword 或 integer 不要用 text
大文本(只存不搜) "index": false 省索引空间

中文分词必须装 IK 插件(ik_max_word 细粒度用于索引,ik_smart 粗粒度用于查询)。

三、查询:term vs match

// term:精确匹配,不分词。用于 keyword / 数值 / 状态
{ "query": { "term": { "status": 1 } } }

// match:会先对查询词分词,再查。用于 text 字段
{ "query": { "match": { "title": "小程序开发" } } }

// bool 组合:filter 不打分可缓存,must 参与打分
{ "query": { "bool": {
    "must":   [ { "match": { "title": "小程序" } } ],
    "filter": [ { "term": { "status": 1 } },
               { "range": { "createTime": { "gte": "2026-01-01" } } } ]
} } }

性能铁律:能用 filter 就不用 must。filter 不计算相关性分数,且结果会被缓存,快很多。

四、深分页问题

from + size 的分页在深翻时是灾难:from=10000 时,每个分片都要先取出 10010 条,协调节点汇总再排序,成本和内存都爆炸。ES 默认限制 from + size <= 10000(max_result_window)。

方案 适用 说明
from + size 浅分页(前 1000 条) 简单,深翻会 OOM
search_after 深翻、导出 用上一页的排序值做游标,推荐
scroll 全量导出(已不推荐) 占用资源,7.x 后改用 PIT
PIT + search_after 全量导出 7.10+ 推荐组合
// search_after:排序字段必须唯一(加 _id 兜底)
{ "size": 20, "sort": [{ "createTime": "desc" }, { "_id": "asc" }],
  "search_after": [1696000000000, "abc123"] }

五、写入调优

// bulk 批量写(单批 5–15MB 最佳,别一次几万条)
POST _bulk
{ "index": { "_index": "article", "_id": "1" } }
{ "title": "...", "status": 1 }
{ "index": { "_index": "article", "_id": "2" } }
{ "title": "...", "status": 1 }

导入大量数据时的三板斧:

PUT /article/_settings
{ "refresh_interval": "-1",        // 关闭自动刷新
  "number_of_replicas": 0 }        // 先不要副本
// 导完再改回来
PUT /article/_settings
{ "refresh_interval": "1s", "number_of_replicas": 1 }

其它要点:

  • 用 bulk,不要一条条 index —— 网络往返是主要开销。
  • ID 自己指定且避免随机 UUID(用业务主键,ES 要判断 ID 是否存在)。
  • translog 设为 async(index.translog.durability: async)能提吞吐,但宕机可能丢几秒数据。
  • 按时间建索引(log-2026-09),过期直接删整个索引,比按条删除快得多。

六、集群与分片规划

# 健康状态(green / yellow / red)
GET _cluster/health
# 分片分布
GET _cat/shards?v
# 节点
GET _cat/nodes?v
# 索引大小
GET _cat/indices?v&s=store.size:desc
# 热点线程(排查 CPU 高)
GET _nodes/hot_threads

分片规划原则:

  • 单个分片大小控制在 20–50GB(日志类可以到 100GB)。
  • 分片数 = 数据总量 / 30GB,不要拍脑袋设 1000 个。
  • 分片数创建后不能改(除非 reindex)。
  • 分片太多会导致:元数据膨胀、查询要合并更多结果、集群恢复变慢。

七、生产实践

案例:搜索接口偶发超时

背景:某财税 SaaS 公司的知识库检索(AI 诊断助手的召回层)。

现象:搜索接口 P99 从 100ms 涨到 3s,偶发超时,监控显示 ES CPU 不高。

排查:

  1. GET _nodes/hot_threads 没发现明显热点。
  2. 用 _profile 分析慢查询,发现大量时间花在一个 must 里的通配符查询上。
  3. 看代码:有个查询用了 wildcard 且以 * 开头(*关键字*)—— 前置通配符会扫描全部 term,非常慢。

处理:

  • 把 *关键字* 改成 match_phrase + IK 分词,或者用 ngram 分词器在建索引时就切好。
  • 把不参与打分的条件从 must 改成 filter。
  • 加 timeout 参数(如 ?timeout=2s)避免个别慢查询拖垮线程池。
  • 对热点查询加缓存(Redis,TTL 60s)。

案例:分片失控导致集群变黄

现象:集群状态长期 yellow,新索引创建后一直不健康。

排查:GET _cat/shards 发现分片数量已经 3000+,且每个索引都设了 5 分片 2 副本 —— 索引是按天建的日志,一年下来分片爆炸。

处理:

  • 日志索引改成单分片(日志不需要并行度),副本 1。
  • 老索引通过 _shrink 收缩分片,或干脆 reindex 到新索引后删除。
  • 加 ILM(索引生命周期管理):热节点 7 天 → 温节点 30 天 → 删除。
  • 设 cluster.max_shards_per_node 上限防止再次失控。

八、几条硬规则

  1. ES 不是主数据源,数据以数据库为准,ES 挂了能重建。
  2. Mapping 上线前定好,改类型只能 reindex。
  3. 禁用 from+size 深翻,用 search_after。
  4. 避免前置通配符和过多 should。
  5. 给索引配生命周期(ILM),否则日志类索引会吃满磁盘。
  6. 写入用 bulk,批量导入先关副本和 refresh。
  7. ES 的 JVM 堆不超过 31GB( compressed oops 的临界点),剩余内存留给 OS 页缓存。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人