一篇文章讲透 Elasticsearch:从倒排索引到实战落地

一篇文章讲透 Elasticsearch:从倒排索引到实战落地

如果你遇到过这种场景------数据库里几百万条数据,用户输入一个关键词,你写了个 LIKE '%关键词%',结果查询慢得让人想砸电脑;或者你想做个"智能搜索",要相关度排序、要纠错、要高亮------那这篇就是为你准备的。

这篇从原理 讲到实战,全程手把手,看完就能上手写查询。


一、为什么需要 ES?

先问一个问题:我们真的需要 ES 吗?关系型数据库难道不能搜索?

能搜,但很痛苦。

sql 复制代码
SELECT * FROM article WHERE title LIKE '%Elasticsearch%';

这个查询在数据量小的时候还能忍,可一旦数据到了千万级:

  • LIKE '%xxx%' 无法走索引,只能全表扫描,慢到怀疑人生;
  • 没有相关度打分,搜"苹果"和搜"苹果手机",结果排序完全一样;
  • 不支持分词,"Elasticsearch" 搜 "elastic" 搜不到;
  • 不支持同义词、拼音、模糊纠错这些"人性化"搜索。

而 ES 的定位,就是为了解决"全文搜索 + 大数据量 + 分布式 + 实时分析"这些问题而生的。

一句话定位 :ES 是一个基于 Lucene 的分布式搜索与分析引擎。它快、能水平扩容、自带分布式、支持近乎实时的搜索和聚合分析。

适用场景 vs 不适用场景

搞清楚边界,才不会用错工具:

适合用 ES 不适合用 ES
全文搜索、站内搜索 需要强事务(ACID)的业务
日志分析、监控大盘 复杂的多表关联查询(ES 关联很弱)
聚合统计、数据分析 频繁的精确更新(每秒百万级写入又要求实时)
自动补全、纠错、推荐 你的数据量 < 几百万且查询简单

实际生产中最常见的做法是 MySQL 做主存储 + ES 做搜索:业务数据落 MySQL,通过同步机制(如 Canal、Logstash、MQ)把数据同步到 ES,搜索走 ES,详情回表查 MySQL。


二、核心概念:先建立世界观

ES 的核心概念不多,但一定要先建立正确的对应关系。很多人一开始被 "Index" 这个词搞晕------在关系型数据库里 "Index" 是索引,在 ES 里 "Index" 却约等于一张

关系型数据库 (MySQL) Elasticsearch
数据库 database 索引 index
表 table 类型 type(7.x 后废弃)
行 row 文档 document
列 column 字段 field
表结构 schema 映射 mapping
SQL Query DSL / SQL

这套数据模型从上到下是分层的,画个图更直观:
#mermaid-svg-XxcKpdsFPF5q9uAv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XxcKpdsFPF5q9uAv .error-icon{fill:#552222;}#mermaid-svg-XxcKpdsFPF5q9uAv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XxcKpdsFPF5q9uAv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XxcKpdsFPF5q9uAv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XxcKpdsFPF5q9uAv .marker.cross{stroke:#333333;}#mermaid-svg-XxcKpdsFPF5q9uAv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XxcKpdsFPF5q9uAv p{margin:0;}#mermaid-svg-XxcKpdsFPF5q9uAv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv .cluster-label text{fill:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv .cluster-label span{color:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv .cluster-label span p{background-color:transparent;}#mermaid-svg-XxcKpdsFPF5q9uAv .label text,#mermaid-svg-XxcKpdsFPF5q9uAv span{fill:#333;color:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv .node rect,#mermaid-svg-XxcKpdsFPF5q9uAv .node circle,#mermaid-svg-XxcKpdsFPF5q9uAv .node ellipse,#mermaid-svg-XxcKpdsFPF5q9uAv .node polygon,#mermaid-svg-XxcKpdsFPF5q9uAv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XxcKpdsFPF5q9uAv .rough-node .label text,#mermaid-svg-XxcKpdsFPF5q9uAv .node .label text,#mermaid-svg-XxcKpdsFPF5q9uAv .image-shape .label,#mermaid-svg-XxcKpdsFPF5q9uAv .icon-shape .label{text-anchor:middle;}#mermaid-svg-XxcKpdsFPF5q9uAv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XxcKpdsFPF5q9uAv .rough-node .label,#mermaid-svg-XxcKpdsFPF5q9uAv .node .label,#mermaid-svg-XxcKpdsFPF5q9uAv .image-shape .label,#mermaid-svg-XxcKpdsFPF5q9uAv .icon-shape .label{text-align:center;}#mermaid-svg-XxcKpdsFPF5q9uAv .node.clickable{cursor:pointer;}#mermaid-svg-XxcKpdsFPF5q9uAv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XxcKpdsFPF5q9uAv .arrowheadPath{fill:#333333;}#mermaid-svg-XxcKpdsFPF5q9uAv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XxcKpdsFPF5q9uAv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XxcKpdsFPF5q9uAv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XxcKpdsFPF5q9uAv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XxcKpdsFPF5q9uAv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XxcKpdsFPF5q9uAv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XxcKpdsFPF5q9uAv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XxcKpdsFPF5q9uAv .cluster text{fill:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv .cluster span{color:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XxcKpdsFPF5q9uAv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XxcKpdsFPF5q9uAv rect.text{fill:none;stroke-width:0;}#mermaid-svg-XxcKpdsFPF5q9uAv .icon-shape,#mermaid-svg-XxcKpdsFPF5q9uAv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XxcKpdsFPF5q9uAv .icon-shape p,#mermaid-svg-XxcKpdsFPF5q9uAv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XxcKpdsFPF5q9uAv .icon-shape .label rect,#mermaid-svg-XxcKpdsFPF5q9uAv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XxcKpdsFPF5q9uAv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XxcKpdsFPF5q9uAv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XxcKpdsFPF5q9uAv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 集群 Cluster
索引 Index (类似数据库)
分片 Shard
文档 Document (类似一行记录)
字段 Field: title
字段 Field: content
字段 Field: author

这里最容易踩坑的三个概念:

  • 文档(Document):存储的最小单元,本质是 JSON。
  • 索引(Index):一堆文档的集合,可以理解为"数据库"。
  • 映射(Mapping):定义每个字段的类型和分词方式,相当于"表结构"。
json 复制代码
{
  "title": "Elasticsearch 入门",
  "content": "这是一篇科普文章",
  "author": "技术大佬",
  "publish_date": "2026-08-05"
}

版本提醒 :ES 6.x 一个索引只允许一个 type;7.x 开始废弃 type ,一个索引就是一个文档集合;8.x 起默认开启安全认证。写这篇时主流生产环境是 7.x 和 8.x,下文代码均以 7.x/8.x 为准。


三、倒排索引:ES 快起来的秘密武器

这是 ES 的灵魂,也是面试必问。理解了倒排索引,你就理解了 ES 一半。

3.1 正排索引 vs 倒排索引

传统数据库用的是正排索引------"文档 → 内容":

复制代码
文档1: "我爱北京天安门"
文档2: "天安门广场看升旗"
文档3: "我爱吃苹果"

你想搜"天安门",就得把三篇文档全读一遍,看哪篇里有这个词------这就是 LIKE 全表扫描。

倒排索引反过来了------"词 → 哪些文档包含这个词":
#mermaid-svg-BzFlPordezNKF3JL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BzFlPordezNKF3JL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BzFlPordezNKF3JL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BzFlPordezNKF3JL .error-icon{fill:#552222;}#mermaid-svg-BzFlPordezNKF3JL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BzFlPordezNKF3JL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BzFlPordezNKF3JL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BzFlPordezNKF3JL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BzFlPordezNKF3JL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BzFlPordezNKF3JL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BzFlPordezNKF3JL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BzFlPordezNKF3JL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BzFlPordezNKF3JL .marker.cross{stroke:#333333;}#mermaid-svg-BzFlPordezNKF3JL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BzFlPordezNKF3JL p{margin:0;}#mermaid-svg-BzFlPordezNKF3JL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BzFlPordezNKF3JL .cluster-label text{fill:#333;}#mermaid-svg-BzFlPordezNKF3JL .cluster-label span{color:#333;}#mermaid-svg-BzFlPordezNKF3JL .cluster-label span p{background-color:transparent;}#mermaid-svg-BzFlPordezNKF3JL .label text,#mermaid-svg-BzFlPordezNKF3JL span{fill:#333;color:#333;}#mermaid-svg-BzFlPordezNKF3JL .node rect,#mermaid-svg-BzFlPordezNKF3JL .node circle,#mermaid-svg-BzFlPordezNKF3JL .node ellipse,#mermaid-svg-BzFlPordezNKF3JL .node polygon,#mermaid-svg-BzFlPordezNKF3JL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BzFlPordezNKF3JL .rough-node .label text,#mermaid-svg-BzFlPordezNKF3JL .node .label text,#mermaid-svg-BzFlPordezNKF3JL .image-shape .label,#mermaid-svg-BzFlPordezNKF3JL .icon-shape .label{text-anchor:middle;}#mermaid-svg-BzFlPordezNKF3JL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BzFlPordezNKF3JL .rough-node .label,#mermaid-svg-BzFlPordezNKF3JL .node .label,#mermaid-svg-BzFlPordezNKF3JL .image-shape .label,#mermaid-svg-BzFlPordezNKF3JL .icon-shape .label{text-align:center;}#mermaid-svg-BzFlPordezNKF3JL .node.clickable{cursor:pointer;}#mermaid-svg-BzFlPordezNKF3JL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BzFlPordezNKF3JL .arrowheadPath{fill:#333333;}#mermaid-svg-BzFlPordezNKF3JL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BzFlPordezNKF3JL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BzFlPordezNKF3JL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BzFlPordezNKF3JL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BzFlPordezNKF3JL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BzFlPordezNKF3JL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BzFlPordezNKF3JL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BzFlPordezNKF3JL .cluster text{fill:#333;}#mermaid-svg-BzFlPordezNKF3JL .cluster span{color:#333;}#mermaid-svg-BzFlPordezNKF3JL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BzFlPordezNKF3JL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BzFlPordezNKF3JL rect.text{fill:none;stroke-width:0;}#mermaid-svg-BzFlPordezNKF3JL .icon-shape,#mermaid-svg-BzFlPordezNKF3JL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BzFlPordezNKF3JL .icon-shape p,#mermaid-svg-BzFlPordezNKF3JL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BzFlPordezNKF3JL .icon-shape .label rect,#mermaid-svg-BzFlPordezNKF3JL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BzFlPordezNKF3JL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BzFlPordezNKF3JL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BzFlPordezNKF3JL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 文档1: 我爱北京天安门
分词器 Analyzer
文档2: 天安门广场看升旗
文档3: 我爱吃苹果
词条 Term 列表
倒排索引表
我 → 文档1, 文档3
爱 → 文档1, 文档3
北京 → 文档1
天安门 → 文档1, 文档2
升旗 → 文档2

当用户搜索"天安门"时,ES 直接查这张表,O(1) 级别就定位到了文档 1、2,不用扫描全文。

可以类比成图书馆的目录卡片:正排索引是"一本书里有哪几个关键词",倒排索引是"某个关键词出现在哪几本书里"。搜索引擎永远选择后者,因为查询词是无限的,而词表是有限的。

3.2 分词:倒排索引的前提

文档进来要先分词 。分词的产出叫 Term(词条),这活儿由 Analyzer(分析器) 干。

一个分析器由三部分组成:

复制代码
Analyzer = Character Filter(字符过滤)
         + Tokenizer(分词器)
         + Token Filter(词项过滤)

ES 内置了很多分析器:

分析器 效果 场景
standard 按空格/标点切分,转小写 英文默认
ik_max_word 中文最大分词,如"天安门"→"天安门" 中文搜索(需装插件)
ik_smart 中文最细粒度切分,切得更少更准 中文搜索(召回少、精度高)
keyword 不分词,整个当做一个词 精确匹配、标签、聚合

比如 "I love Elasticsearch" 经过 standard 分析器,会变成 iloveelasticsearch 三个词条。

中文分词是个大坑 。ES 自带的 standard 对中文只是按字切,效果极差。生产环境中文搜索几乎必装 IK 分词器analysis-ik)。如果再做电商搜索,还常叠加拼音分词器(analysis-pinyin)实现"打错拼音也能搜"。


四、分布式架构:一台装不下,就一群

单机版的 Lucene 再快,也扛不住海量数据和并发。ES 的解法是横向扩展------把数据切碎,撒到多台机器上。

四个核心概念:

  • 集群(Cluster):一群节点的集合,有统一的集群名。
  • 节点(Node):一台运行 ES 的机器,本质是一个 Java 进程。
  • 主分片(Primary Shard):一个索引被切成的片段,负责数据"写入",主分片数在建索引时定死。
  • 副本分片(Replica Shard):主分片的拷贝,负责"读"和高可用,副本数随时可调。

打个比方:一本 1000 页的书,拆成 5 份(每个主分片 200 页)分给 5 个人看,速度自然快 5 倍;再复印一份(副本)放别处,一人坏了也不影响。
#mermaid-svg-aZgkRwMLJsu5Li6T{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aZgkRwMLJsu5Li6T .error-icon{fill:#552222;}#mermaid-svg-aZgkRwMLJsu5Li6T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aZgkRwMLJsu5Li6T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aZgkRwMLJsu5Li6T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aZgkRwMLJsu5Li6T .marker.cross{stroke:#333333;}#mermaid-svg-aZgkRwMLJsu5Li6T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aZgkRwMLJsu5Li6T p{margin:0;}#mermaid-svg-aZgkRwMLJsu5Li6T .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T .cluster-label text{fill:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T .cluster-label span{color:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T .cluster-label span p{background-color:transparent;}#mermaid-svg-aZgkRwMLJsu5Li6T .label text,#mermaid-svg-aZgkRwMLJsu5Li6T span{fill:#333;color:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T .node rect,#mermaid-svg-aZgkRwMLJsu5Li6T .node circle,#mermaid-svg-aZgkRwMLJsu5Li6T .node ellipse,#mermaid-svg-aZgkRwMLJsu5Li6T .node polygon,#mermaid-svg-aZgkRwMLJsu5Li6T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aZgkRwMLJsu5Li6T .rough-node .label text,#mermaid-svg-aZgkRwMLJsu5Li6T .node .label text,#mermaid-svg-aZgkRwMLJsu5Li6T .image-shape .label,#mermaid-svg-aZgkRwMLJsu5Li6T .icon-shape .label{text-anchor:middle;}#mermaid-svg-aZgkRwMLJsu5Li6T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aZgkRwMLJsu5Li6T .rough-node .label,#mermaid-svg-aZgkRwMLJsu5Li6T .node .label,#mermaid-svg-aZgkRwMLJsu5Li6T .image-shape .label,#mermaid-svg-aZgkRwMLJsu5Li6T .icon-shape .label{text-align:center;}#mermaid-svg-aZgkRwMLJsu5Li6T .node.clickable{cursor:pointer;}#mermaid-svg-aZgkRwMLJsu5Li6T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aZgkRwMLJsu5Li6T .arrowheadPath{fill:#333333;}#mermaid-svg-aZgkRwMLJsu5Li6T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aZgkRwMLJsu5Li6T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aZgkRwMLJsu5Li6T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aZgkRwMLJsu5Li6T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aZgkRwMLJsu5Li6T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aZgkRwMLJsu5Li6T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aZgkRwMLJsu5Li6T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aZgkRwMLJsu5Li6T .cluster text{fill:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T .cluster span{color:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aZgkRwMLJsu5Li6T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aZgkRwMLJsu5Li6T rect.text{fill:none;stroke-width:0;}#mermaid-svg-aZgkRwMLJsu5Li6T .icon-shape,#mermaid-svg-aZgkRwMLJsu5Li6T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aZgkRwMLJsu5Li6T .icon-shape p,#mermaid-svg-aZgkRwMLJsu5Li6T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aZgkRwMLJsu5Li6T .icon-shape .label rect,#mermaid-svg-aZgkRwMLJsu5Li6T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aZgkRwMLJsu5Li6T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aZgkRwMLJsu5Li6T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aZgkRwMLJsu5Li6T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 集群 Cluster (cluster: my-es)
节点 Node-2
节点 Node-1
互为备份
互为备份
主分片 1
主分片 0
副本分片 0
副本分片 1

注意上面的图:主分片 0 和它的副本 0 永远不在同一台机器上,这是 ES 高可用的基本保障。

4.1 路由:一条数据该去哪个分片?

ES 通过一个简单的哈希算法决定文档落在哪个主分片:

复制代码
shard = hash(routing) % number_of_primary_shards

默认 routing 就是文档的 _id。这个公式决定了主分片数一旦确定就不可更改------因为改动了分片数,所有数据的落位就全变了,这涉及重新分片。

实战提醒 :主分片数建索引前一定要想清楚。生产经验是单个分片控制在 30GB ~ 50GB,再根据预估总数据量反推分片数。比如预估 500GB 数据,那么 10~16 个主分片比较合理。分片过多会导致查询扇出太多,过少会导致单分片过大、merge 变慢。


五、实战一:环境搭建与首次写入

5.1 推荐快速起步方式

生产环境用 Docker 最省心(也最贴合 K8s 部署形态):

bash 复制代码
# 单节点 Docker 方式
docker network create es-net

# 拉镜像(8.x 默认带安全认证)
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.10.2
docker pull docker.elastic.co/kibana/kibana:8.10.2

# 启动 ES
docker run -d \
  --name es \
  --net es-net \
  -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
  docker.elastic.co/elasticsearch/elasticsearch:8.10.2

# 启动 Kibana(浏览器访问 http://localhost:5601)
docker run -d \
  --name kibana \
  --net es-net \
  -p 5601:5601 \
  -e "ELASTICSEARCH_HOSTS=http://es:9200" \
  docker.elastic.co/kibana/kibana:8.10.2

验证集群健康:

bash 复制代码
curl -X GET "localhost:9200/_cluster/health?pretty"

返回 "status" : "green" 表示健康。三色含义:green (主副都正常)、yellow (主分片正常,副本没就位,单节点常见)、red(有主分片未分配,数据有风险)。

5.2 安装 IK 分词器(中文搜索必备)

bash 复制代码
# 进入容器
docker exec -it es bash

# 在容器内执行(注意版本号要和 ES 一致)
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.10.2/elasticsearch-analysis-ik-8.10.2.zip

# 重启 ES 容器
exit
docker restart es

验证 IK 是否生效:

bash 复制代码
curl -X POST "localhost:9200/_analyze" -H "Content-Type: application/json" -d '{
  "analyzer": "ik_max_word",
  "text": "我爱北京天安门"
}'

你会看到输出被切分成 我爱北京天安门 等多个词条。

5.3 首次写入一条文档

bash 复制代码
# 往 blog 索引写入一条文档(索引不存在会自动创建)
curl -X PUT "localhost:9200/blog/_doc/1" -H "Content-Type: application/json" -d '{
  "title": "Elasticsearch 实战入门",
  "content": "从倒排索引到分布式搜索,一文带你实战落地",
  "author": "技术大佬",
  "tags": ["es", "搜索"],
  "publish_date": "2026-08-05",
  "views": 1280
}'

响应里的 "_shards": {"total": 2, "successful": 1} 说明主分片写入成功(单节点无副本)。再查询验证:

bash 复制代码
curl -X GET "localhost:9200/blog/_doc/1"

六、实战二:Mapping 设计(决定成败)

建索引前先设计 Mapping,是 ES 实战最重要的一步。 很多人图省事让 ES 自动映射,结果 keyword/text 分不清、聚合失败、日期格式错乱,后面全要重建索引。

6.1 字段类型选择口诀

业务场景 字段类型 原因
全文搜索(标题、正文) text + 指定 analyzer 要分词、要相关度
精确匹配 / 排序 / 聚合(状态、作者、标签) keyword 不分词,可排序可聚合
数值(价格、数量) integer / long / double 范围查询、聚合
时间 date 时间范围查询、按时间聚合
布尔 boolean 过滤条件
地理坐标 geo_point 附近的人 / 门店

经典坑 :给标签字段配了 text,然后 terms 聚合想统计每个标签的文章数,结果发现标签被拆成一个个词------因为 text 会被分词。凡是需要聚合/排序/精确匹配的字段,一定要用 keyword

6.2 一个完整的 Mapping 示例

json 复制代码
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": {
          "keyword": { "type": "keyword", "ignore_above": 256 }
        }
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "author": { "type": "keyword" },
      "tags":   { "type": "keyword" },
      "status": { "type": "keyword" },
      "publish_date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" },
      "views":  { "type": "integer" },
      "price":  { "type": "double" },
      "is_top": { "type": "boolean" },
      "location": { "type": "geo_point" }
    }
  },
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  }
}

这里有个很实用的技巧:title.keyword ------ 给 title 同时建一个 keyword 子字段。这样 title 用来做全文搜索,title.keyword 用来做精确匹配、排序、聚合,一个字段两种用法,官方推荐的做法。

6.3 Mapping 建错了怎么办?

ES 的 mapping 一旦字段建立就不能修改类型,只能新增字段。改类型只有一条路:
#mermaid-svg-EY0gyMoNofl283fd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EY0gyMoNofl283fd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EY0gyMoNofl283fd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EY0gyMoNofl283fd .error-icon{fill:#552222;}#mermaid-svg-EY0gyMoNofl283fd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EY0gyMoNofl283fd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EY0gyMoNofl283fd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EY0gyMoNofl283fd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EY0gyMoNofl283fd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EY0gyMoNofl283fd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EY0gyMoNofl283fd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EY0gyMoNofl283fd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EY0gyMoNofl283fd .marker.cross{stroke:#333333;}#mermaid-svg-EY0gyMoNofl283fd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EY0gyMoNofl283fd p{margin:0;}#mermaid-svg-EY0gyMoNofl283fd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EY0gyMoNofl283fd .cluster-label text{fill:#333;}#mermaid-svg-EY0gyMoNofl283fd .cluster-label span{color:#333;}#mermaid-svg-EY0gyMoNofl283fd .cluster-label span p{background-color:transparent;}#mermaid-svg-EY0gyMoNofl283fd .label text,#mermaid-svg-EY0gyMoNofl283fd span{fill:#333;color:#333;}#mermaid-svg-EY0gyMoNofl283fd .node rect,#mermaid-svg-EY0gyMoNofl283fd .node circle,#mermaid-svg-EY0gyMoNofl283fd .node ellipse,#mermaid-svg-EY0gyMoNofl283fd .node polygon,#mermaid-svg-EY0gyMoNofl283fd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EY0gyMoNofl283fd .rough-node .label text,#mermaid-svg-EY0gyMoNofl283fd .node .label text,#mermaid-svg-EY0gyMoNofl283fd .image-shape .label,#mermaid-svg-EY0gyMoNofl283fd .icon-shape .label{text-anchor:middle;}#mermaid-svg-EY0gyMoNofl283fd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EY0gyMoNofl283fd .rough-node .label,#mermaid-svg-EY0gyMoNofl283fd .node .label,#mermaid-svg-EY0gyMoNofl283fd .image-shape .label,#mermaid-svg-EY0gyMoNofl283fd .icon-shape .label{text-align:center;}#mermaid-svg-EY0gyMoNofl283fd .node.clickable{cursor:pointer;}#mermaid-svg-EY0gyMoNofl283fd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EY0gyMoNofl283fd .arrowheadPath{fill:#333333;}#mermaid-svg-EY0gyMoNofl283fd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EY0gyMoNofl283fd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EY0gyMoNofl283fd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EY0gyMoNofl283fd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EY0gyMoNofl283fd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EY0gyMoNofl283fd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EY0gyMoNofl283fd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EY0gyMoNofl283fd .cluster text{fill:#333;}#mermaid-svg-EY0gyMoNofl283fd .cluster span{color:#333;}#mermaid-svg-EY0gyMoNofl283fd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EY0gyMoNofl283fd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EY0gyMoNofl283fd rect.text{fill:none;stroke-width:0;}#mermaid-svg-EY0gyMoNofl283fd .icon-shape,#mermaid-svg-EY0gyMoNofl283fd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EY0gyMoNofl283fd .icon-shape p,#mermaid-svg-EY0gyMoNofl283fd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EY0gyMoNofl283fd .icon-shape .label rect,#mermaid-svg-EY0gyMoNofl283fd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EY0gyMoNofl283fd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EY0gyMoNofl283fd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EY0gyMoNofl283fd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 旧索引 my_index
新索引 my_index_v2

(正确 Mapping)
数据拷贝

_reindex
索引别名指向新索引
删除旧索引

即:建新索引 → _reindex 拷贝数据 → 用索引别名无缝切换 → 删旧索引。这也是生产环境常用的"无感知索引重建"套路,强烈建议上线前先设计好 Mapping。


七、实战三:查询 DSL 全家桶

ES 的查询语法叫 Query DSL,本质是 JSON。下面从最常用的到进阶的,逐个实战。

7.1 全文搜索:match

match 是全文搜索的核心,会把查询词分词后再匹配:

json 复制代码
{ "query": { "match": { "title": "Elasticsearch 入门" } } }

匹配时,"Elasticsearch" 和 "入门" 都会被分词,只要命中其中一个就有结果,默认按相关度打分排序。

7.2 精确匹配:term / terms

term 不分词,把整个值当做一个词去精确匹配(对 keyword 字段):

json 复制代码
// 单个精确值
{ "query": { "term": { "author": "技术大佬" } } }

// 多个精确值(IN 的效果)
{ "query": { "terms": { "status": ["published", "draft"] } } }

注意:term 查询 text 字段经常搜不到,因为 text 被分成了多个词条。term 只用于 keyword 字段

7.3 范围查询:range

json 复制代码
{
  "query": {
    "range": {
      "views": { "gte": 1000, "lte": 10000 }
    }
  }
}

gt 大于、gte 大于等于、lt 小于、lte 小于等于,数值和日期通用。

7.4 组合查询:bool(最常用)

真实业务几乎都是多条件组合,全靠 bool 搞定:

json 复制代码
{
  "query": {
    "bool": {
      "must":     [ { "match": { "title": "Elasticsearch" } } ],
      "filter":   [ { "term": { "status": "published" } } ],
      "should":   [ { "match": { "content": "分布式" } } ],
      "must_not": [ { "term": { "author": "匿名" } } ]
    }
  }
}

四个子句的区别必须记住:

子句 语义 影响打分?
must 必须匹配,相当于 AND ✅ 参与打分
filter 必须匹配,相当于 AND ❌ 不参与打分,能走缓存,性能好
should 至少匹配一个才加分,相当于 OR ✅ 加分
must_not 必须不匹配,相当于 NOT ❌ 不参与打分

性能要点 :凡是"过滤条件"(如 status、时间范围、author)都放 filter,不要放 must。filter 不计算相关度分数,还能被 ES 缓存,性能碾压。

7.5 模糊与纠错:fuzzy / wildcard

json 复制代码
// 拼写纠错:搜索 "elasticsearc" 也能命中 "elasticsearch"
{ "query": { "fuzzy": { "title": "elasticsearc" } } }

// 通配符(慎用,性能差)
{ "query": { "wildcard": { "author": "技术*" } } }

7.6 高亮:highlight

搜索结果关键字高亮,做站内搜索必用:

json 复制代码
{
  "query": { "match": { "content": "分布式" } },
  "highlight": {
    "fields": { "content": {} },
    "pre_tags": ["<b style='color:red'>"],
    "post_tags": ["</b>"]
  }
}

返回结果里会多一个 highlight 字段,把命中的关键字包上 <b style='color:red'>

7.7 分页与排序

json 复制代码
{
  "query": { "match_all": {} },
  "from": 0,
  "size": 10,
  "sort": [
    { "views": "desc" },
    { "publish_date": "asc" }
  ]
}

深分页警告from + size 超过 10000 会报错(默认 index.max_result_window 限制)。大量分页场景要用 search_after(下一节讲)。

7.8 深分页的正确姿势:search_after

json 复制代码
// 第一次查询,记录返回的 sort 值
{
  "query": { "match_all": {} },
  "size": 10,
  "sort": [ { "views": "desc" }, { "_id": "asc" } ]
}

// 第二次查询,把上一页最后一条的 sort 值带进来
{
  "query": { "match_all": {} },
  "size": 10,
  "search_after": [ 1280, "1" ],
  "sort": [ { "views": "desc" }, { "_id": "asc" } ]
}

search_after 本质上就是"从这个游标位置继续往后取",每一页之间没有偏移计算,性能稳定,是"下一页"场景的标准方案(不适合跳页)。

7.9 排序 + 相关度兼得

想"热门的排前面,但相关度也重要",可以给分数加权:

json 复制代码
{
  "query": {
    "bool": {
      "must": [ { "match": { "title": "Elasticsearch" } } ],
      "should": [ { "range": { "views": { "gte": 10000 } } } ]
    }
  },
  "sort": [ "_score", { "views": "desc" } ]
}

或者更精细地,用 function_score 给"浏览量高"的文章加权:

json 复制代码
{
  "query": {
    "function_score": {
      "query": { "match": { "title": "Elasticsearch" } },
      "functions": [
        {
          "field_value_factor": {
            "field": "views",
            "factor": 0.001
          }
        }
      ]
    }
  }
}

八、实战四:聚合分析(GROUP BY 的替代品)

聚合是 ES 的另一大杀器,统计大盘、报表全靠它。先看个最简单的桶聚合 (相当于 GROUP BY author):

json 复制代码
{
  "size": 0,
  "aggs": {
    "by_author": {
      "terms": { "field": "author", "size": 10 }
    }
  }
}

返回每个作者的文章数:

json 复制代码
{
  "aggregations": {
    "by_author": {
      "buckets": [
        { "key": "技术大佬", "doc_count": 42 },
        { "key": "隔壁老王", "doc_count": 18 }
      ]
    }
  }
}

8.1 多层嵌套:先分组再统计

json 复制代码
{
  "size": 0,
  "aggs": {
    "by_status": {
      "terms": { "field": "status" },
      "aggs": {
        "avg_views": { "avg": { "field": "views" } },
        "total_views": { "sum": { "field": "views" } },
        "max_views": { "max": { "field": "views" } }
      }
    }
  }
}

相当于 SQL:SELECT status, AVG(views), SUM(views), MAX(views) FROM blog GROUP BY status

8.2 时间桶聚合:按天统计

json 复制代码
{
  "size": 0,
  "aggs": {
    "daily_views": {
      "date_histogram": {
        "field": "publish_date",
        "calendar_interval": "day"
      }
    }
  }
}

相当于 SQL:SELECT DATE(publish_date), COUNT(*) FROM blog GROUP BY DATE(publish_date),画趋势图必备。

8.3 聚合 + 过滤 + 分桶组合

json 复制代码
{
  "size": 0,
  "query": { "match": { "status": "published" } },
  "aggs": {
    "by_tag": { "terms": { "field": "tags" } }
  }
}

先过滤出 published 的文章,再按标签分桶。Query 负责圈定范围,Aggs 负责统计,这是 ES 分析的基本套路。


九、实战五:常见业务场景落地

9.1 场景一:站内搜索(最经典)

需求:标题模糊搜 + 标签过滤 + 发布时间范围 + 浏览量排序 + 关键字高亮 + 分页。

json 复制代码
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "ES 入门" } }
      ],
      "filter": [
        { "term":  { "tags": "java" } },
        { "range": { "publish_date": { "gte": "2026-01-01" } } }
      ]
    }
  },
  "highlight": {
    "fields": { "title": {} },
    "pre_tags": ["<b>"], "post_tags": ["</b>"]
  },
  "sort": [ { "views": "desc" } ],
  "from": 0,
  "size": 10
}

9.2 场景二:电商商品搜索

需求:品牌过滤 + 价格区间 + 规格属性筛选 + 销量排序。

json 复制代码
{
  "query": {
    "bool": {
      "must": [ { "match": { "name": "手机" } } ],
      "filter": [
        { "term":  { "brand": "苹果" } },
        { "range": { "price": { "gte": 3000, "lte": 8000 } } },
        { "term":  { "attrs.storage": "256G" } }
      ]
    }
  },
  "sort": [ { "sales": "desc" } ]
}

9.3 场景三:附近的人 / 门店

先用 mapping 把坐标存成 geo_point,再查附近:

json 复制代码
{
  "query": {
    "bool": {
      "filter": [
        {
          "geo_distance": {
            "distance": "5km",
            "location": { "lat": 31.23, "lon": 121.47 }
          }
        }
      ]
    }
  }
}

9.4 场景四:自动补全(搜索联想)

需求:用户输入"els",联想出 "Elasticsearch"。用 completion 类型字段:

json 复制代码
{
  "mappings": {
    "properties": {
      "suggest": { "type": "completion" }
    }
  }
}
json 复制代码
// 查询联想词
{
  "suggest": {
    "search-suggest": {
      "prefix": "els",
      "completion": { "field": "suggest" }
    }
  }
}

9.5 场景五:日志查询与大盘(ELK)

最成熟的组合是 ELK 三件套:
#mermaid-svg-SfqWpN8Vprqcsmjo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SfqWpN8Vprqcsmjo .error-icon{fill:#552222;}#mermaid-svg-SfqWpN8Vprqcsmjo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SfqWpN8Vprqcsmjo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SfqWpN8Vprqcsmjo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SfqWpN8Vprqcsmjo .marker.cross{stroke:#333333;}#mermaid-svg-SfqWpN8Vprqcsmjo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SfqWpN8Vprqcsmjo p{margin:0;}#mermaid-svg-SfqWpN8Vprqcsmjo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo .cluster-label text{fill:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo .cluster-label span{color:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo .cluster-label span p{background-color:transparent;}#mermaid-svg-SfqWpN8Vprqcsmjo .label text,#mermaid-svg-SfqWpN8Vprqcsmjo span{fill:#333;color:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo .node rect,#mermaid-svg-SfqWpN8Vprqcsmjo .node circle,#mermaid-svg-SfqWpN8Vprqcsmjo .node ellipse,#mermaid-svg-SfqWpN8Vprqcsmjo .node polygon,#mermaid-svg-SfqWpN8Vprqcsmjo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SfqWpN8Vprqcsmjo .rough-node .label text,#mermaid-svg-SfqWpN8Vprqcsmjo .node .label text,#mermaid-svg-SfqWpN8Vprqcsmjo .image-shape .label,#mermaid-svg-SfqWpN8Vprqcsmjo .icon-shape .label{text-anchor:middle;}#mermaid-svg-SfqWpN8Vprqcsmjo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SfqWpN8Vprqcsmjo .rough-node .label,#mermaid-svg-SfqWpN8Vprqcsmjo .node .label,#mermaid-svg-SfqWpN8Vprqcsmjo .image-shape .label,#mermaid-svg-SfqWpN8Vprqcsmjo .icon-shape .label{text-align:center;}#mermaid-svg-SfqWpN8Vprqcsmjo .node.clickable{cursor:pointer;}#mermaid-svg-SfqWpN8Vprqcsmjo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SfqWpN8Vprqcsmjo .arrowheadPath{fill:#333333;}#mermaid-svg-SfqWpN8Vprqcsmjo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SfqWpN8Vprqcsmjo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SfqWpN8Vprqcsmjo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SfqWpN8Vprqcsmjo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SfqWpN8Vprqcsmjo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SfqWpN8Vprqcsmjo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SfqWpN8Vprqcsmjo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SfqWpN8Vprqcsmjo .cluster text{fill:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo .cluster span{color:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SfqWpN8Vprqcsmjo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SfqWpN8Vprqcsmjo rect.text{fill:none;stroke-width:0;}#mermaid-svg-SfqWpN8Vprqcsmjo .icon-shape,#mermaid-svg-SfqWpN8Vprqcsmjo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SfqWpN8Vprqcsmjo .icon-shape p,#mermaid-svg-SfqWpN8Vprqcsmjo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SfqWpN8Vprqcsmjo .icon-shape .label rect,#mermaid-svg-SfqWpN8Vprqcsmjo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SfqWpN8Vprqcsmjo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SfqWpN8Vprqcsmjo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SfqWpN8Vprqcsmjo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用日志
Logstash

(采集/清洗/转换)
Elasticsearch

(存储 + 索引)
Kibana

(可视化大盘)

Kibana 上建一个 Discover 视图,配合 date_histogram 聚合,就能画出"每分钟请求量"、"错误率 TOP 接口"这种大盘。日志场景通常按天建索引 log-2026.08.05,配合索引生命周期管理(ILM)自动清理过期数据。


十、原理进阶:写入流程 & 查询流程

10.1 一条数据是怎么写进去的?

写入流程非常经典,画个时序图:
副本分片 主分片 协调节点 客户端 副本分片 主分片 协调节点 客户端 #mermaid-svg-hHeOorYNZyMjyvWr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hHeOorYNZyMjyvWr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hHeOorYNZyMjyvWr .error-icon{fill:#552222;}#mermaid-svg-hHeOorYNZyMjyvWr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hHeOorYNZyMjyvWr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hHeOorYNZyMjyvWr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hHeOorYNZyMjyvWr .marker.cross{stroke:#333333;}#mermaid-svg-hHeOorYNZyMjyvWr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hHeOorYNZyMjyvWr p{margin:0;}#mermaid-svg-hHeOorYNZyMjyvWr .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hHeOorYNZyMjyvWr text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hHeOorYNZyMjyvWr .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hHeOorYNZyMjyvWr .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hHeOorYNZyMjyvWr .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hHeOorYNZyMjyvWr .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hHeOorYNZyMjyvWr #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hHeOorYNZyMjyvWr .sequenceNumber{fill:white;}#mermaid-svg-hHeOorYNZyMjyvWr #sequencenumber{fill:#333;}#mermaid-svg-hHeOorYNZyMjyvWr #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hHeOorYNZyMjyvWr .messageText{fill:#333;stroke:none;}#mermaid-svg-hHeOorYNZyMjyvWr .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hHeOorYNZyMjyvWr .labelText,#mermaid-svg-hHeOorYNZyMjyvWr .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hHeOorYNZyMjyvWr .loopText,#mermaid-svg-hHeOorYNZyMjyvWr .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hHeOorYNZyMjyvWr .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hHeOorYNZyMjyvWr .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hHeOorYNZyMjyvWr .noteText,#mermaid-svg-hHeOorYNZyMjyvWr .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hHeOorYNZyMjyvWr .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hHeOorYNZyMjyvWr .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hHeOorYNZyMjyvWr .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hHeOorYNZyMjyvWr .actorPopupMenu{position:absolute;}#mermaid-svg-hHeOorYNZyMjyvWr .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hHeOorYNZyMjyvWr .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hHeOorYNZyMjyvWr .actor-man circle,#mermaid-svg-hHeOorYNZyMjyvWr line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hHeOorYNZyMjyvWr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 提交文档② 计算 routing,定位主分片③ 转发到主分片④ 写入内存 buffer + translog⑤ 并行同步副本⑥ 写入确认⑦ 返回结果⑧ 201 Created

这里有两个关键词必须懂:

  • translog(事务日志):防丢数据的兜底。内存 buffer 还没落盘前,如果机器挂了,靠 translog 恢复。
  • refresh(刷新) :默认每 1 秒把 buffer 里的数据"可见"。这就是为什么 ES 是近实时(Near Real Time)而不是实时------刚写入的数据,最多 1 秒后才能被搜索到。

顺便说一句:删除/更新文档也不是真正的物理删除,而是写入一条带版本号的"墓碑"记录,标记旧文档不可见。ES 里没有就地修改,一切写入都是"新增"。

10.2 一次搜索是怎么跑起来的?

查询时,任意节点都可以充当协调节点(Coordinating Node)
分片 1 分片 0 协调节点 客户端 分片 1 分片 0 协调节点 客户端 #mermaid-svg-iS1KAxU7fzgFLnJx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iS1KAxU7fzgFLnJx .error-icon{fill:#552222;}#mermaid-svg-iS1KAxU7fzgFLnJx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iS1KAxU7fzgFLnJx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iS1KAxU7fzgFLnJx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iS1KAxU7fzgFLnJx .marker.cross{stroke:#333333;}#mermaid-svg-iS1KAxU7fzgFLnJx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iS1KAxU7fzgFLnJx p{margin:0;}#mermaid-svg-iS1KAxU7fzgFLnJx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-iS1KAxU7fzgFLnJx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-iS1KAxU7fzgFLnJx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-iS1KAxU7fzgFLnJx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-iS1KAxU7fzgFLnJx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-iS1KAxU7fzgFLnJx .sequenceNumber{fill:white;}#mermaid-svg-iS1KAxU7fzgFLnJx #sequencenumber{fill:#333;}#mermaid-svg-iS1KAxU7fzgFLnJx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-iS1KAxU7fzgFLnJx .messageText{fill:#333;stroke:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-iS1KAxU7fzgFLnJx .labelText,#mermaid-svg-iS1KAxU7fzgFLnJx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .loopText,#mermaid-svg-iS1KAxU7fzgFLnJx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-iS1KAxU7fzgFLnJx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-iS1KAxU7fzgFLnJx .noteText,#mermaid-svg-iS1KAxU7fzgFLnJx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-iS1KAxU7fzgFLnJx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-iS1KAxU7fzgFLnJx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-iS1KAxU7fzgFLnJx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-iS1KAxU7fzgFLnJx .actorPopupMenu{position:absolute;}#mermaid-svg-iS1KAxU7fzgFLnJx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-iS1KAxU7fzgFLnJx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-iS1KAxU7fzgFLnJx .actor-man circle,#mermaid-svg-iS1KAxU7fzgFLnJx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-iS1KAxU7fzgFLnJx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 发起查询② 并行转发到所有分片② 并行转发到所有分片③ 本地查询并返回 Top 10③ 本地查询并返回 Top 10④ 合并结果,全局打分排序⑤ 返回全局 Top 10

关键点:每个分片先各自算出自己的 Top N,再由协调节点合并、重新排序 。所以如果你的查询带了 from: 0, size: 10000 这种大分页,协调节点要分别向所有分片取 10000 条再合并,压力巨大------这就是"深分页慢"的根源。

10.3 相关度是怎么算的?(BM25)

ES 默认用 BM25 算法打分。它综合考虑:

  • 关键词在文档中出现频率越高,得分越高(TF);
  • 关键词在全库出现越频繁(如"的""了"),得分越低(IDF);
  • 文档越短,命中越加分。

所以在 ES 里,排序默认是**"谁最相关谁排前面"**,而不是谁后写入谁排前面。


十一、性能调优与避坑指南

11.1 Segment 合并(性能之根)

ES 底层的存储单元叫 Segment(段) 。写入时数据先进内存,refresh 后变成一个个不可变的 Segment ,小 Segment 多了会触发 merge(合并)
#mermaid-svg-poobABKquFFzLEzR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-poobABKquFFzLEzR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-poobABKquFFzLEzR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-poobABKquFFzLEzR .error-icon{fill:#552222;}#mermaid-svg-poobABKquFFzLEzR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-poobABKquFFzLEzR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-poobABKquFFzLEzR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-poobABKquFFzLEzR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-poobABKquFFzLEzR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-poobABKquFFzLEzR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-poobABKquFFzLEzR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-poobABKquFFzLEzR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-poobABKquFFzLEzR .marker.cross{stroke:#333333;}#mermaid-svg-poobABKquFFzLEzR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-poobABKquFFzLEzR p{margin:0;}#mermaid-svg-poobABKquFFzLEzR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-poobABKquFFzLEzR .cluster-label text{fill:#333;}#mermaid-svg-poobABKquFFzLEzR .cluster-label span{color:#333;}#mermaid-svg-poobABKquFFzLEzR .cluster-label span p{background-color:transparent;}#mermaid-svg-poobABKquFFzLEzR .label text,#mermaid-svg-poobABKquFFzLEzR span{fill:#333;color:#333;}#mermaid-svg-poobABKquFFzLEzR .node rect,#mermaid-svg-poobABKquFFzLEzR .node circle,#mermaid-svg-poobABKquFFzLEzR .node ellipse,#mermaid-svg-poobABKquFFzLEzR .node polygon,#mermaid-svg-poobABKquFFzLEzR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-poobABKquFFzLEzR .rough-node .label text,#mermaid-svg-poobABKquFFzLEzR .node .label text,#mermaid-svg-poobABKquFFzLEzR .image-shape .label,#mermaid-svg-poobABKquFFzLEzR .icon-shape .label{text-anchor:middle;}#mermaid-svg-poobABKquFFzLEzR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-poobABKquFFzLEzR .rough-node .label,#mermaid-svg-poobABKquFFzLEzR .node .label,#mermaid-svg-poobABKquFFzLEzR .image-shape .label,#mermaid-svg-poobABKquFFzLEzR .icon-shape .label{text-align:center;}#mermaid-svg-poobABKquFFzLEzR .node.clickable{cursor:pointer;}#mermaid-svg-poobABKquFFzLEzR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-poobABKquFFzLEzR .arrowheadPath{fill:#333333;}#mermaid-svg-poobABKquFFzLEzR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-poobABKquFFzLEzR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-poobABKquFFzLEzR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-poobABKquFFzLEzR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-poobABKquFFzLEzR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-poobABKquFFzLEzR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-poobABKquFFzLEzR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-poobABKquFFzLEzR .cluster text{fill:#333;}#mermaid-svg-poobABKquFFzLEzR .cluster span{color:#333;}#mermaid-svg-poobABKquFFzLEzR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-poobABKquFFzLEzR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-poobABKquFFzLEzR rect.text{fill:none;stroke-width:0;}#mermaid-svg-poobABKquFFzLEzR .icon-shape,#mermaid-svg-poobABKquFFzLEzR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-poobABKquFFzLEzR .icon-shape p,#mermaid-svg-poobABKquFFzLEzR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-poobABKquFFzLEzR .icon-shape .label rect,#mermaid-svg-poobABKquFFzLEzR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-poobABKquFFzLEzR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-poobABKquFFzLEzR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-poobABKquFFzLEzR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Segment 1
后台 Merge
Segment 2
Segment 3
新的大 Segment

Segment 合并是后台异步进行的,但如果 merge 速度赶不上写入速度,就会出现"慢查询 + IO 打满"。

11.2 常见性能坑自查表

后果 解法
from+size 深分页 协调节点内存爆炸 search_after
过滤条件放 must 不放 filter 白算分 + 不能缓存 过滤条件一律 filter
大量小分片 查询扇出太多、merge 频繁 单分片 30-50GB,控制分片数
集群无副本 单点故障直接丢数据 至少 1 个副本
一次性灌入海量历史数据 refresh/merge 拖垮集群 批量写入时调大 refresh_interval 或关掉
wildcard 开头通配符 性能极差 ngram/前缀匹配替代
text 字段做聚合 结果错乱(被分词) 用 keyword 字段或 keyword 子字段
索引无别名直接裸用 重建索引时服务要停 全程用别名,底层索引可随意替换

11.3 写入性能调优三板斧

大批量导入场景(比如初始化数据、日志灌库):

json 复制代码
// ① 暂时调大 refresh 间隔,减少刷盘频率
PUT /blog/_settings
{ "index": { "refresh_interval": "30s" } }

// ② 用 bulk 批量接口,一次提交多条
POST /_bulk
{"index": {"_index": "blog"}}
{"title": "文档1", "content": "..."}
{"index": {"_index": "blog"}}
{"title": "文档2", "content": "..."}

常见客户端(Java/Go/Python)都有 bulk API,单次 bulk 500~5000 条是经验值,具体用压测找最优。

json 复制代码
// ③ 导完恢复默认
PUT /blog/_settings
{ "index": { "refresh_interval": "1s" } }

11.4 内存与部署建议

  • JVM 堆内存 :不要超过物理内存的一半,且最大不超过 31GB(超过后压缩指针失效)。留一半给操作系统做文件缓存。
  • 不要部署在 NFS 等共享存储上:ES 依赖本地磁盘 IO,建议本地 SSD。
  • 设置 swap 关闭bootstrap.memory_lock: true,避免内存被换出导致 GC 抖动。
  • 读写分离 :大集群里分数据节点协调节点,查询压力走协调节点,避免互相影响。

十二、实战六:一个完整的前后端搜索示例

前面讲了一堆片段,最后拼一个完整的 Java Spring Boot + ES 搜索示例,感受一下真实项目里怎么写。

12.1 引入依赖

xml 复制代码
<!-- 选择与 ES 版本一致的客户端 -->
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.10.2</version>
</dependency>

12.2 初始化客户端

java 复制代码
@Configuration
public class EsConfig {

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200, "http")
        ).build();
        return new ElasticsearchClient(
            new RestClientTransport(restClient, new JacksonJsonpMapper())
        );
    }
}

12.3 Service 里的搜索方法

java 复制代码
@Service
public class ArticleSearchService {

    @Autowired
    private ElasticsearchClient client;

    public List<Article> search(String keyword, String status, int page, int size) {
        try {
            SearchResponse<Article> response = client.search(s -> s
                .index("blog")
                .query(q -> q
                    .bool(b -> b
                        // 标题全文搜索
                        .must(m -> m.match(mm -> mm
                            .field("title").query(keyword)))
                        // 状态过滤(不进打分)
                        .filter(f -> f.term(t -> t
                            .field("status").value(status)))
                    ))
                // 浏览量降序
                .sort(so -> so.field(f -> f.field("views").order(SortOrder.Desc)))
                .from((page - 1) * size)
                .size(size),
                Article.class);

            return response.hits().hits().stream()
                .map(hit -> hit.source())
                .collect(Collectors.toList());

        } catch (IOException e) {
            throw new RuntimeException("搜索失败", e);
        }
    }
}

12.4 前端调用(简化版)

javascript 复制代码
async function search() {
  const resp = await fetch('/api/article/search?keyword=' + keyword + '&page=1&size=10');
  const data = await resp.json();
  renderList(data);   // 渲染文章列表
}

到这里,一个"输入关键词 → 后台查 ES → 前端渲染结果"的最小闭环就通了。剩下的就是加上高亮、分页组件、搜索建议这些锦上添花的东西。


十三、写在最后

学习 ES,我建议按这个顺序来:

  1. 先跑起来:装个 ES + Kibana,把官方文档的"入门教程"走一遍;
  2. 再懂原理:搞清楚倒排索引、分片副本、写读流程这三个核心;
  3. 然后做项目:用真实数据去设计 mapping、写查询、做聚合,遇到性能问题再回头看原理;
  4. 最后上规模:学集群运维、监控、调优。

最后送你一句话:搜索不是"把数据翻一遍",而是"提前把数据准备好,查询时直达"。理解了这句话,你就理解了 ES 的一切。

如果这篇对你有帮助,欢迎点赞收藏转发,我们下期见 🚀


相关推荐
ApacheSeaTunnel1 小时前
Apache SeaTunnel 提交一个任务都经过了什么?
大数据·开源·数据集成·seatunnel·技术分享·数据同步
jkyy20142 小时前
以科技赋能运动康养!健康有益×泰康养老,打造智能运动新体系
大数据·人工智能·健康医疗
lvts_cs2 小时前
高青原料药及上下游:全链协同,打造医药制造新增长极
大数据·人工智能·制造
ZCBUS实时计算2 小时前
政务海量分库分表汇聚实战:基于 3 节点 ZCBUS 集群完成医保 10TB 数据实时整合
大数据·数据库·数据仓库·sql·dba·etl·政务
九硕智慧建筑一体化厂家2 小时前
直流照明|无尘风淋室照明,高均匀无频闪,适配洁净车间高频合规工况
大数据·人工智能·笔记·智慧城市
Databend2 小时前
Databend 产品更新:从 Spatial Index Join 到 Eval 数据管道
大数据·数据库·sql
Elasticsearch3 小时前
Kibana Dashboards API:适用于所有面板类型的稳定接口,在正式发布前经过 50 多个团队验证
elasticsearch
ifenxi爱分析3 小时前
GEO市场规模有多大?2026—2030年中国GEO市场规模预测
大数据·人工智能
Elasticsearch3 小时前
Elasticsearch ES|QL 将全文搜索带到你从未建立索引的数据中
elasticsearch