1. 从一次日志查询的困境说起
几年前我接手过一个日志分析模块的维护工作,当时数据量大概每天几百万条,存在关系型数据库里。业务方要求支持按关键词模糊匹配、按时间范围过滤、按多个字段组合筛选,还要做聚合统计。我一开始用LIKE '%keyword%'硬扛,前几个月还行,数据量一上来,查询时间从几百毫秒涨到十几秒,聚合统计更是直接超时。后来加了索引、做了分表,勉强撑住,但每次新增一个查询维度就要改表结构、重建索引,维护成本高得离谱。
那次经历让我开始认真研究专门做搜索和分析的存储系统,也就是后来一直在用的 ElasticSearch。这篇文章是我对 ElasticSearch 核心概念的一次系统梳理,适合刚接触它、或者用过但一直没搞明白内部原理的开发者。我会从它到底解决什么问题讲起,把索引、文档、分片、倒排索引这些概念一个个拆开,配上我实际踩过的坑和验证过的参数,尽量让你看完之后不只是"知道有这么个东西",而是能理解它为什么这么设计、什么时候该用、什么时候别硬上。
需要先说明一点:ElasticSearch 不是一个"更好的数据库",它和关系型数据库解决的是不同问题。把它当数据库用、或者把它当缓存用,都是常见的误用。搞清楚它的定位,后面的概念才立得住。
2. ElasticSearch 到底解决的是哪一类问题
2.1 关系型数据库在搜索场景下的三个硬伤
要理解 ElasticSearch 的价值,得先看清楚传统关系型数据库在搜索场景下到底卡在哪。我把当年遇到的问题归纳成三条,这三条基本覆盖了大多数人的痛点。
第一条是模糊匹配效率极低。关系型数据库的 B+ 树索引擅长的是等值查询和范围查询,比如WHERE id = 100或者WHERE age > 18,这类查询走索引很快。但一旦变成WHERE content LIKE '%error%',前置通配符会让索引完全失效,数据库只能全表扫描。几百万行扫一遍还能忍,上亿行就是灾难。
第二条是多维度组合筛选的索引膨胀。业务上经常要"按用户 + 按时间段 + 按状态 + 按关键词"组合查询,关系型数据库要么为每种组合建联合索引(索引数量爆炸,写入变慢),要么就接受全表扫描。而且索引一旦建好,想加一个新维度就得改表、重建,线上操作风险很大。
第三条是聚合分析能力弱。像"统计每个小时各错误码出现的次数"这种需求,用GROUP BY写出来又长又慢,数据量大了基本跑不动。而搜索场景里,聚合统计恰恰是高频需求。
2.2 倒排索引:把"找文档"变成"查字典"
ElasticSearch 解决上面这些问题的核心武器是倒排索引(Inverted Index)。这个词听起来唬人,其实用查字典类比一下就懂了。
正排索引是"文档 → 内容",你知道文档 ID,去查它的内容,关系型数据库的主键索引就是这个逻辑。倒排索引反过来,是"词项 → 文档列表",你先拿到一个词,直接就能知道哪些文档包含这个词。就像查字典,你不需要把整本字典读一遍找某个字,而是直接翻到那个字所在的页码。
举个具体例子。假设有三条日志:
- 文档1:
error timeout database - 文档2:
error connection refused - 文档3:
timeout warning
倒排索引会建成这样:
| 词项 | 文档列表 |
|---|---|
| error | 1, 2 |
| timeout | 1, 3 |
| database | 1 |
| connection | 2 |
| refused | 2 |
| warning | 3 |
当你要查包含error的文档时,直接拿到[1, 2],不需要扫描任何文档内容。查error AND timeout就是两个列表求交集,得到[1]。这就是为什么 ElasticSearch 的搜索能这么快——它把"扫描"变成了"查表"。
2.3 分布式架构:单机扛不住就横向扩展
光有倒排索引还不够。数据量到了 TB 级别,单机内存放不下整个索引,CPU 也扛不住查询压力。ElasticSearch 从设计之初就是分布式的,它把一个大索引切成多个分片(Shard),分散到多台机器上,查询时并行执行再合并结果。这部分我在第 4 节会详细展开,这里先建立个印象:ElasticSearch 的快,一半靠倒排索引,一半靠分布式并行。
提示:倒排索引不是 ElasticSearch 发明的,Lucene 早就有了。ElasticSearch 的真正贡献是把 Lucene 包装成了分布式、易用、带 REST 接口的系统。理解这一点,你就知道它的底层能力边界在哪。
3. 索引、文档、字段:三个必须分清的基础概念
3.1 索引不是数据库,文档不是行
刚接触 ElasticSearch 的人最容易犯的错,是拿关系型数据库的概念硬套。我见过不少同事张口就是"这个索引相当于一张表",然后按建表那套思路去设计,结果处处别扭。这里必须把概念掰清楚。
索引(Index)是 ElasticSearch 里最大的数据容器,但它和数据库的"库"或"表"都不完全对应。一个索引是一堆文档的集合,这些文档在逻辑上通常属于同一类业务对象,比如"订单日志索引""用户行为索引"。索引这个名字容易误导,它其实更接近"数据集"的概念。
文档(Document)是 ElasticSearch 里最小的数据单元,用 JSON 表示。它相当于关系型数据库的一行,但比行灵活得多——不同文档的字段可以不一样,不需要预先定义严格的表结构。比如同样是订单文档,有的带优惠券字段,有的不带,完全没问题。
字段(Field)是文档里的一个键值对,相当于列。但字段的类型是可以在写入时动态推断的,也可以提前在映射(Mapping)里定义好。
我用一张表把对应关系列出来,但请注意最后一列的"注意点",这些差异才是关键:
| 关系型数据库 | ElasticSearch | 注意点 |
|---|---|---|
| 数据库 Database | 无直接对应 | 通常用索引前缀或别名区分业务 |
| 表 Table | 索引 Index | 索引更接近"数据集",不是严格的表 |
| 行 Row | 文档 Document | 文档是 JSON,字段可不同 |
| 列 Column | 字段 Field | 字段类型可动态推断或预定义 |
| 主键 Primary Key | _id | 可自定义,不填则自动生成 |
| 索引 Index | 倒排索引 | 同名但含义完全不同,别混淆 |
3.2 映射:什么时候该手动定义,什么时候让它自动推断
映射(Mapping)定义了索引里每个字段的类型和索引方式。ElasticSearch 支持动态映射,也就是你写入第一条文档时,它自动根据值推断类型。字符串推断成text加keyword子字段,数字推断成long或float,日期推断成date。
动态映射在开发阶段很方便,但生产环境我强烈建议手动定义映射。原因有三个。
第一,类型推断会出错。比如一个字段第一条写入的是"123",它可能推断成text;后面写入456(数字),就可能报类型冲突。日志类数据尤其容易踩这个坑,因为字段值格式不统一。
第二,默认分词不一定符合业务。字符串默认会被标准分词器拆成一个个词,但有些字段你根本不想分词,比如订单号、身份证号、URL,这些应该用keyword类型整体存储,否则搜"ORDER123"可能被拆成order和123,搜不准。
第三,动态映射会创建大量无用字段。如果写入的 JSON 结构不稳定,ElasticSearch 会为每个新字段建索引,字段数爆炸会拖慢集群,甚至触发字段数上限保护。
手动定义映射的写法大概是这样:
{ "mappings": { "properties": { "order_id": { "type": "keyword" }, "user_name": { "type": "text", "analyzer": "standard" }, "amount": { "type": "scaled_float", "scaling_factor": 100 }, "created_at": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, "status": { "type": "keyword" } } } }这里order_id用keyword是为了精确匹配,user_name用text是为了全文搜索,amount用scaled_float是为了避免浮点精度问题(金额场景很常见),created_at指定了日期格式。每个选择背后都有业务考量,不是随便写的。
3.3 一个真实踩坑:字段类型冲突导致写入失败
说个我亲身踩过的坑。有次做用户行为采集,device_id字段在部分设备上上报的是字符串,在另一部分设备上上报的是数字。动态映射先遇到字符串,把字段定成了text,后来遇到数字就报mapper_parsing_exception,整批数据写入失败。
排查过程是这样的:先看 ElasticSearch 返回的错误信息,定位到具体字段;然后用GET /index_name/_mapping查看当前映射,确认字段类型;最后发现是上游数据格式不统一。修复方案有两个,短期是在写入前统一转成字符串,长期是手动定义映射把device_id固定为keyword,并在采集端做格式校验。
这个坑的教训是:动态映射只适合原型验证,生产环境一定要手动定义映射,并且在上游做数据格式约束。字段类型冲突一旦发生,修复起来很麻烦,因为已经写入的数据类型改不了,只能重建索引。
4. 分片与副本:分布式能力的来源
4.1 分片是什么,为什么需要它
分片(Shard)是 ElasticSearch 把索引水平切分后的子集。一个索引可以分成多个分片,每个分片是一个独立的 Lucene 索引,可以放在不同的节点上。这样做的直接好处是:数据量大了,单机放不下,切成多份分散到多台机器;查询压力大了,多个分片并行查询,再合并结果。
分片分为主分片(Primary Shard)和副本分片(Replica Shard)。主分片负责写入和查询,副本分片是主分片的拷贝,负责查询分担和故障恢复。主分片数在索引创建时确定,之后不能改(除非重建索引);副本分片数可以随时调整。
这里有个关键点很多人不知道:主分片数一旦设定就不能修改。因为分片数量决定了数据如何路由——ElasticSearch 用hash(routing) % 主分片数来决定一条文档去哪个分片。如果主分片数变了,所有文档的路由结果都变了,数据就找不到了。所以创建索引时选主分片数要慎重。
4.2 分片数怎么定:一个可落地的估算方法
分片数定多少合适?这是被问得最多的问题之一。我的经验是分三步估算。
第一步,估算数据总量。假设你每天产生 10GB 原始数据,保留 30 天,加上副本,总存储需求是10GB × 30 × (1 + 副本数)。如果副本数是 1,就是 600GB。
第二步,确定单分片容量。官方建议单个分片大小控制在 10GB 到 50GB 之间。太小了分片数量多,管理开销大;太大了故障恢复慢,查询并行度低。取中间值 30GB 比较稳妥。
第三步,算主分片数。总数据量 / 单分片容量 = 600GB / 30GB = 20,所以主分片数定 20 左右。
但这只是起点,还要考虑节点数量。如果集群只有 3 个节点,20 个主分片加 20 个副本分片,总共 40 个分片,平均每个节点 13 个分片,还算合理。如果节点更少,分片数要相应减少,否则每个节点分片太多,管理开销会拖累性能。
注意:分片不是越多越好。每个分片都有固定的内存和文件句柄开销,分片过多会导致集群元数据膨胀、主节点压力大。我见过有人给一个几 GB 的索引建了 100 个分片,结果集群状态更新缓慢,查询也没快多少。分片数要和数据量、节点数匹配。
4.3 副本的两个作用:高可用和查询分担
副本分片有两个作用,很多人只知道第一个。
第一个是高可用。主分片所在的节点挂了,副本分片可以提升为新的主分片,数据不丢,服务不断。这是副本最直观的价值。
第二个是查询分担。查询请求可以同时打到主分片和副本分片上,副本越多,能并行处理查询的"副本"就越多,查询吞吐量越高。这一点在读多写少的场景下特别有用。
但副本也不是越多越好。每个副本都要占用存储空间,写入时主分片写完还要同步到所有副本,副本越多写入越慢。一般生产环境副本数设为 1 或 2 就够了。单节点集群副本数只能设 0,因为副本不能和主分片在同一节点上,设了也不会分配。
4.4 集群健康状态:绿黄红到底意味着什么
集群健康状态用三种颜色表示,这个一定要会看,因为它是判断集群是否正常的第一手信息。
绿色表示所有主分片和副本分片都正常分配。这是最理想的状态。
黄色表示所有主分片正常,但有副本分片未分配。这通常发生在单节点集群,或者节点数少于副本数加一的情况。黄色状态下数据不丢,查询正常,但没有冗余,一旦主分片所在节点挂了就危险了。
红色表示有主分片未分配,部分数据不可用。这是严重状态,必须立即处理。常见原因是节点宕机且副本不足以恢复,或者磁盘空间不足导致分片无法分配。
我遇到过集群变黄的情况,排查下来是节点数从 3 减到 2,而副本数是 1,导致部分副本无处分配。解决办法要么加回节点,要么临时把副本数调成 0(不推荐长期这样),要么等节点恢复。用GET /_cluster/health可以查看状态,用GET /_cat/shards?v可以看到具体哪些分片未分配。
5. 近实时搜索与文档写入的完整链路
5.1 为什么叫"近实时"而不是"实时"
ElasticSearch 常被描述为"近实时"(Near Real-Time,NRT)搜索引擎,这个"近"字很关键。你写入一条文档,并不是立刻就能搜到,中间有个延迟,默认大约 1 秒。这个延迟来自它的写入机制。
理解这个机制,能帮你解释很多"为什么刚写入的数据搜不到"的困惑。我见过不少新手写完数据立刻查询,发现查不到,以为写入失败了,其实是还没到刷新时间。
5.2 从写入到可搜索:refresh、translog 和 flush
一条文档从写入到最终落盘,经历了好几个阶段,我把关键环节拆开讲。
第一步,写入内存缓冲区。文档先进入内存中的索引缓冲区(In-memory Buffer),同时追加到事务日志(Translog)。此时文档还不可搜索。
第二步,refresh。默认每 1 秒执行一次 refresh,把内存缓冲区的内容生成一个新的段(Segment),段是 Lucene 的底层存储单元,一旦生成就可被搜索。这就是"近实时"的来源——最多等 1 秒就能搜到。refresh 不涉及磁盘同步,只是让数据对搜索可见。
第三步,flush。Translog 会不断增长,达到一定大小或时间后触发 flush,把内存中的段真正同步到磁盘,并清空 Translog。flush 比 refresh 重得多,频率低很多。
第四步,段合并。每次 refresh 都会生成新段,段太多会影响查询性能,所以后台会定期把多个小段合并成大段。合并过程会消耗 IO 和 CPU,这也是为什么写入高峰期集群负载会升高。
用一张表总结这几个概念的区别:
| 概念 | 触发频率 | 作用 | 是否落盘 |
|---|---|---|---|
| refresh | 默认 1 秒 | 让数据可被搜索 | 否 |
| flush | Translog 满或定时 | 持久化并清空 Translog | 是 |
| 段合并 | 后台自动 | 减少段数量,提升查询性能 | 是 |
5.3 写入性能调优:什么时候该调 refresh_interval
既然 refresh 每秒一次,那写入量特别大的时候,频繁 refresh 会生成大量小段,拖慢性能。这时候可以临时调大refresh_interval,比如设成30s甚至-1(关闭自动 refresh)。
我做过一个批量导入的场景,默认配置下导入 1000 万条数据花了将近 20 分钟。把refresh_interval设成-1、number_of_replicas设成0,导入时间降到 6 分钟左右。导入完成后再把这两个参数改回来。这个技巧在初始化索引时特别有用。
PUT /my_index/_settings { "index": { "refresh_interval": "-1", "number_of_replicas": "0" } }导入完成后记得改回来:
PUT /my_index/_settings { "index": { "refresh_interval": "1s", "number_of_replicas": "1" } }提示:关闭 refresh 期间写入的数据搜不到,所以这个操作只适合批量导入阶段,不能在生产查询期间用。改回配置后,ElasticSearch 会自动 refresh 一次,数据就可见了。
6. 分词器:中文搜索绕不开的一道坎
6.1 分词器在搜索链路里的位置
分词器(Analyzer)负责把文本拆成一个个词项,是倒排索引的入口。它由三部分组成:字符过滤器(Character Filter)、分词器(Tokenizer)、词项过滤器(Token Filter)。字符过滤器在分词前处理原始文本,比如去掉 HTML 标签;分词器负责切词;词项过滤器对切出来的词做加工,比如转小写、去停用词、加同义词。
搜索时也会用分词器处理查询词,所以索引时分词和搜索时分词必须一致,否则搜不准。这是很多人踩坑的地方——索引时用了某种分词器,搜索时用了另一种,结果对不上。
6.2 标准分词器处理中文的尴尬
ElasticSearch 默认的标准分词器对英文还行,对中文就尴尬了。它会把中文按字切分,比如"搜索引擎"会被切成"搜""索""引""擎"四个字。这样搜"搜索"能搜到,但搜"引擎"也能搜到,精度很差,而且索引体积大。
我早期做中文搜索时没注意这点,用标准分词器,结果搜"北京大学"能匹配到"北京"和"大学"分开的文档,噪音很大。后来换成专门的中文分词器才解决。
6.3 中文分词的选型与自定义词库
中文分词常用的方案有几种,各有取舍。我列个表对比一下:
| 分词方案 | 特点 | 适用场景 |
|---|---|---|
| 标准分词器 | 按字切分,精度低 | 不推荐用于中文 |
| IK 分词器 | 社区常用,支持自定义词典 | 通用中文搜索 |
| 拼音分词器 | 支持拼音搜索 | 需要拼音检索的场景 |
| 自定义词典 | 补充业务专有名词 | 有领域术语的场景 |
IK 分词器是社区里用得比较多的方案,它有两种模式:ik_smart粗粒度,ik_max_word细粒度。索引时一般用ik_max_word多切一些词,提高召回;搜索时用ik_smart少切一些,提高精度。这个"索引细、搜索粗"的策略是中文搜索的常见实践。
自定义词典很重要。比如你的业务里有"云原生""微服务"这类词,默认词典可能切不对,需要加到自定义词典里。IK 支持通过配置文件扩展词典,改完重启或热加载生效。
PUT /my_index { "settings": { "analysis": { "analyzer": { "my_analyzer": { "type": "custom", "tokenizer": "ik_max_word", "filter": ["lowercase"] } } } } }6.4 验证分词效果:_analyze 接口的用法
分词器配好了,怎么验证效果?用_analyze接口直接看切词结果,这是我最常用的调试手段。
POST /my_index/_analyze { "analyzer": "ik_max_word", "text": "ElasticSearch中文分词测试" }返回结果会列出切出来的每个词项及其位置。如果发现切得不对,就调整分词器或补充词典,再测。这个接口在排查"为什么搜不到"的问题时特别有用——先看索引时分词结果,再看搜索时分词结果,对比一下就知道问题出在哪。
7. 查询与聚合:从能搜到到搜得准
7.1 Query DSL 的两大类:查询上下文和过滤上下文
ElasticSearch 的查询用Query DSL(领域特定语言)描述,是 JSON 格式。查询分两大类:查询上下文(Query Context)和过滤上下文(Filter Context)。
查询上下文会计算相关性得分(_score),回答"这个文档和查询有多匹配";过滤上下文只判断"是或否",不计算得分,而且结果会被缓存,性能更好。
这个区分很实用。比如"标题包含'错误'且状态是'未解决'","标题包含错误"需要算相关性,用查询上下文;"状态是未解决"是精确条件,用过滤上下文。把能过滤的条件都放进 filter,能显著提升性能。
GET /my_index/_search { "query": { "bool": { "must": [ { "match": { "title": "错误" } } ], "filter": [ { "term": { "status": "unresolved" } }, { "range": { "created_at": { "gte": "2024-01-01" } } } ] } } }7.2 match 和 term 的区别:一个高频踩坑点
match和term的区别是新手最容易搞混的地方,我专门说一下。
match会对查询词做分词,然后去倒排索引里匹配。比如match: { title: "错误日志" }会先分词成"错误""日志",再分别匹配。适合全文搜索。
term不做分词,直接拿整个值去精确匹配。比如term: { status: "unresolved" }直接匹配status字段等于unresolved的文档。适合keyword类型字段。
坑在于:如果你对一个text类型字段用term查询,因为text字段被分词了,倒排索引里存的是切好的词,而term拿整个查询值去匹配,往往匹配不上。我见过有人对text字段用term查中文,怎么都查不到,排查半天才发现是类型和查询方式不匹配。
记住一个原则:text字段用match,keyword字段用term。如果确实要对text字段做精确匹配,可以用它的.keyword子字段。
7.3 聚合分析:搜索之外的另一半价值
ElasticSearch 的聚合(Aggregation)能力经常被低估。很多人只把它当搜索引擎用,其实它的聚合分析在日志分析、指标统计场景里非常好用。
聚合分三类:指标聚合(Metrics)算数值,比如平均值、最大值、求和;桶聚合(Bucket)分组,比如按状态分组、按时间段分组;管道聚合(Pipeline)对其他聚合的结果再加工。
举个实际例子,统计每个错误码出现的次数并按降序排列:
GET /logs/_search { "size": 0, "aggs": { "error_codes": { "terms": { "field": "error_code", "size": 10, "order": { "_count": "desc" } } } } }size: 0表示不返回文档,只要聚合结果,这样能省掉文档传输开销。terms是桶聚合,按error_code分组,取前 10 个,按数量降序。这个查询在日志分析里几乎天天用。
注意:聚合字段如果是
text类型,需要开启fielddata才能聚合,但fielddata很吃内存,不推荐。正确做法是用keyword类型字段做聚合。这也是为什么我前面强调字符串字段要区分text和keyword。
8. 我踩过的几个典型坑与应对经验
8.1 深度分页:from + size 的隐形上限
用from和size做分页,翻到后面会越来越慢,而且默认最多只能翻到 10000 条。原因是每个分片都要返回from + size条数据给协调节点,协调节点再排序取前size条。翻到第 1000 页时,每个分片都要返回大量数据,内存和网络开销巨大。
index.max_result_window可以调大这个上限,但治标不治本,翻得越深越慢。正确的做法是用search_after游标分页,它基于上一页最后一条的排序值继续往下取,不需要跳过前面的数据,性能稳定。
GET /my_index/_search { "size": 20, "sort": [ { "created_at": "desc" }, { "_id": "asc" } ], "search_after": ["2024-01-01 12:00:00", "doc_123"] }search_after需要一个稳定的排序字段组合,通常用时间加唯一 ID,避免排序值重复导致漏数据。
8.2 字段数爆炸:动态映射的代价
前面提过动态映射的风险,这里展开说一个真实案例。有个采集程序上报的 JSON 里带了一个extra字段,内容是不定长的键值对,键名每次都不一样。动态映射为每个新键都建了字段,几天下来字段数涨到几千个,集群状态更新变慢,写入也开始报错。
解决办法是把extra字段设成enabled: false,不索引它的内容,只存原始值;或者设成flattened类型,把整个对象当一个字段处理。这两种方式都能避免字段数爆炸。
{ "properties": { "extra": { "type": "flattened" } } }这个坑的教训是:对结构不稳定的字段,要么关闭索引,要么用 flattened 类型,绝不能让动态映射自由发挥。
8.3 集群变红的一次完整排查
有次线上集群突然变红,部分查询报错。我按下面的顺序排查,最后定位到是磁盘空间不足。
第一步,GET /_cluster/health确认是红色,且有主分片未分配。第二步,GET /_cat/shards?v找到具体哪些分片未分配,状态是UNASSIGNED。第三步,GET /_cluster/allocation/explain查看未分配原因,返回信息里明确写了磁盘使用率超过阈值。第四步,清理旧索引、扩容磁盘,分片自动恢复分配,集群转绿。
这个排查链路我后来整理成了 checklist,遇到集群异常就按这个顺序走,基本能快速定位。关键是要会用_cluster/allocation/explain,它直接告诉你分片为什么没分配,比瞎猜高效得多。
8.4 映射冲突导致的数据丢失风险
最后说一个隐蔽的坑。如果索引的映射定义和实际写入的数据类型不一致,ElasticSearch 默认会拒绝写入并报错。但如果配置了ignore_malformed: true,它会忽略格式错误的字段,只写入其他字段。这看起来很宽容,实则危险——你以为数据写进去了,其实部分字段被悄悄丢弃了。
我的建议是:生产环境不要开ignore_malformed,让错误暴露出来。写入失败总比数据静默丢失好,前者你能立刻发现并修复,后者可能几个月后做分析时才发现数据不全,那时候已经无法追溯了。
9. 什么时候该用 ElasticSearch,什么时候别硬上
聊了这么多概念和坑,最后回到选型这个根本问题。ElasticSearch 很强,但不是万能的,用错场景会带来很大维护负担。
适合用的场景:全文搜索、日志分析、多维度组合筛选、聚合统计、需要近实时检索的数据。这些场景它的倒排索引和分布式架构能发挥最大价值。
不适合用的场景:强事务要求的场景(它不支持 ACID 事务)、频繁更新的场景(更新本质是删除加重建,代价高)、作为唯一数据源(它更适合做检索层,原始数据应该有可靠存储)、小数据量简单查询(杀鸡用牛刀,维护成本不划算)。
我个人的经验是,把 ElasticSearch 定位成"检索和分析层",上游有可靠的数据源(比如关系型数据库或消息队列),通过同步机制把数据导入 ElasticSearch 供查询。这样即使 ElasticSearch 出问题,原始数据也不丢,重建索引即可恢复。这个架构思路比把它当主库用要稳妥得多。
至于版本选择,新项目直接用当前稳定版就行,老项目升级要重点看 breaking changes,尤其是映射和查询语法相关的变更,升级前一定要在测试环境验证。分片数、副本数、refresh 间隔这些参数,没有万能值,都要结合自己的数据量和查询模式去调,先按经验值起步,再根据监控数据优化。