一篇文章讲透 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 分析器,会变成 i、love、elasticsearch 三个词条。
中文分词是个大坑 。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,我建议按这个顺序来:
- 先跑起来:装个 ES + Kibana,把官方文档的"入门教程"走一遍;
- 再懂原理:搞清楚倒排索引、分片副本、写读流程这三个核心;
- 然后做项目:用真实数据去设计 mapping、写查询、做聚合,遇到性能问题再回头看原理;
- 最后上规模:学集群运维、监控、调优。
最后送你一句话:搜索不是"把数据翻一遍",而是"提前把数据准备好,查询时直达"。理解了这句话,你就理解了 ES 的一切。
如果这篇对你有帮助,欢迎点赞收藏转发,我们下期见 🚀