ElasticSearch面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 00:51

Elasticsearch 的 suggest 功能如何实现自动补全?

ES 提供四种 suggester:Completion Suggester 基于内存 FST(有限状态转换器)做前缀匹配,延迟极低,适合自动补全;Term Suggester 基于编辑距离做拼写纠错;Phrase Suggester 在 Term 基础上加 n-gram 语言模型优化整句建议;Context Suggester 为 Completion 增加分类/地理上下文过滤。自动补全场景用 Completion Suggester:索引时将建议词存入 completion 类型字段构建 FST,查询时用 prefix 参数匹配,毫秒级返回。追问Completion Suggester 为什么快?它将建议词构建为 FST 结构常驻内存,前缀匹配是 O(k) 复杂度(k 为前缀长度),不涉及倒排索引扫描和评分计算,所以延迟在毫秒级。Completion 和 searchasyou_type 有什么区别?Completion 是独立建议通道,返回建议词而非文档;searchasyoutype 是特殊 text 子类型,本质还是全文检索返回文档。Completion 更适合输入框补全,searchasyoutype 更适合边输边搜文档内容。拼写纠错用哪个?Term Suggester。它基于倒排索引中的词项做编辑距离计算,返回相似词建议。需要指定 suggest_mode:missing(仅缺词建议)、popular(热门词优先)、always(始终建议)。如何给建议词加权重?在 completion 字段中设置 weight 属性,权重高的建议优先返回。常用于热门搜索词提权:"suggest": {"input": "laptop", "weight": 100}。FST 内存开销大吗?FST 是高度压缩的有向无环图,百万级建议词通常只占几十 MB。但建议词总量达亿级时需评估内存,可用 context 过滤减少单次匹配范围。写段代码PUT /search-suggest{ "mappings": { "properties": { "title": { "type": "keyword" }, "suggest": { "type": "completion", "analyzer": "standard" } } }}GET /search-suggest/_search{ "suggest": { "auto": { "prefix": "elas", "completion": { "field": "suggest", "size": 5 } } }}
服务端阅读 05月29日 00:51

Elasticsearch 如何实现地理空间搜索?

ES 通过 geopoint 和 geoshape 两种类型支持地理搜索。geopoint 存储经纬度坐标点,支持 geodistance(圆形范围)、geoboundingbox(矩形范围)、geopolygon(多边形范围)查询;geoshape 存储复杂几何形状(线、多边形),支持相交、包含等空间关系查询。底层使用 geohash 编码将二维坐标映射为一维字符串,利用 BKD tree 索引加速范围检索。查询时先通过 geohash 前缀粗筛,再计算精确距离过滤。追问geopoint 和 geoshape 怎么选?存储门店位置等点数据用 geopoint,存储配送区域等面数据用 geoshape。geopoint 查询更快,geoshape 支持更复杂的空间关系但索引开销更大。geo_distance 查询性能如何优化?先用 geoboundingbox 缩小候选集,再在结果上做精确距离计算。也可设置 geopoint 的 geohashprecision 控制索引精度。经纬度顺序容易搞混怎么办?ES 的 geo_point 支持多种格式:字符串 "lat,lon"、数组 [lon,lat](GeoJSON 标准)、对象 {lat,lon}。数组格式是 lon 在前,容易出错,推荐用对象格式避免歧义。geohash 精度怎么选?精度越高定位越准但索引越大。常见选择:1km 精度用 geohash_precision=5(约 4.9km 边长),100m 用 6,10m 用 7。业务精度需求决定精度设置。geo 查询能和普通查询组合吗?可以,放在 bool query 的 filter 子句中。geo 查询不计算评分,适合做过滤条件配合全文检索使用。写段代码PUT /stores{ "mappings": { "properties": { "name": { "type": "keyword" }, "location": { "type": "geo_point" } } }}GET /stores/_search{ "query": { "bool": { "filter": { "geo_distance": { "distance": "5km", "location": { "lat": 39.9, "lon": 116.4 } } } } }}
服务端阅读 05月29日 00:51

Elasticsearch 的 fielddata 和 doc_values 有什么区别?

fielddata 是基于 JVM 堆内存的倒排索引补充结构,仅在搜索时按需加载到内存,专用于 text 字段的聚合和排序,内存消耗不可控,易引发 OOM;docvalues 是基于磁盘的列式存储,索引时随文档写入持久化,默认对 keyword 和数值类型启用,不占堆内存,ES 5.x 后成为默认方案。核心区别:存储位置(堆 vs 磁盘)、加载时机(搜索时 vs 索引时)、内存风险(高 vs 低)、适用类型(text vs keyword/numeric)。生产环境中应优先用 docvalues,若字段不需聚合/排序可关闭 doc_values 节省磁盘。追问为什么 fielddata 容易 OOM?fielddata 将字段的全部唯一词项加载到 JVM 堆,高基数字段(如 UUID)可能占用数 GB。且被 LRU 缓存持有,不会主动释放,直到触发 circuit breaker。text 字段需要聚合怎么办?推荐用 multi-fields:主字段 text 做全文检索,子字段 keyword 启用 doc_values 做聚合。避免在 text 上开启 fielddata: true。关闭 doc_values 有什么影响?该字段将无法用于聚合、排序和脚本访问。如果字段只做全文检索不需要这些操作,关闭可节省约 10-15% 磁盘空间。doc_values 的列式存储如何加速聚合?列式存储按字段值连续排列,对同一字段的遍历只需顺序读磁盘,CPU 缓存命中率高。行式存储需跳行读取,I/O 散乱。ES 7.x 后还有必要了解 fielddata 吗?需要。排查旧索引的内存问题时仍需检查 fielddata 占用,GET _nodes/stats/indices?fielddata 可定位高消耗字段。写段代码PUT /products{ "mappings": { "properties": { "title": { "type": "text", "fields": { "keyword": { "type": "keyword", "doc_values": true } } }, "status": { "type": "keyword", "doc_values": true } } }}
服务端阅读 05月29日 00:50

Elasticsearch 如何优化写入性能?

核心思路是减少刷新频率、批量提交、降低副本开销。具体操作:将 refreshinterval 设为 -1 禁用自动刷新,写入完成后手动 refresh;使用 Bulk API 批量提交文档(建议 5-15MB 一批);写入期间将 numberofreplicas 设为 0,写完后恢复;调大 translog.flushthreshold_size 减少 flush 次数;合理路由使热点数据集中写入少数分片,避免跨节点协调开销。追问bulk 请求多大合适?建议单次 bulk 请求体 5-15MB,文档数不超 10000。过大易触发 GC 甚至 OOM,过小则网络开销占比高。用 BulkProcessor 可自动攒批提交。refresh_interval=-1 写完忘改回来怎么办?新写入文档对搜索不可见,但不丢数据。生产中可在索引模板中设置正常值,仅写入任务启动时临时覆盖。副本数为 0 风险多大?单节点故障会丢分片数据。建议仅在大批量初始导入时使用,写完立即恢复副本并等待 allocation 完成。translog 配置怎么调?将 translog.durability 改为 async,translog.syncinterval 设为 30s,flushthreshold_size 从默认 512MB 调到 1GB,减少磁盘 fsync 次数。冷热数据如何隔离写入?用 ILM 策略将新数据写入热节点(SSD),rollover 后迁移到冷节点(HDD)。hot 阶段设短 refresh_interval,cold 阶段恢复默认。写段代码PUT /logs-write{ "settings": { "refresh_interval": "-1", "number_of_replicas": 0, "translog": { "durability": "async", "sync_interval": "30s" } }}
服务端阅读 05月28日 02:00

Elasticsearch 如何实现近实时搜索?

Elasticsearch 实现近实时搜索的核心机制是 refresh 操作:写入的数据先进入内存缓冲区(Memory Buffer),默认每秒执行一次 refresh,将缓冲区中的文档生成新的 Lucene Segment 并写入文件系统缓存(Filesystem Cache),此时数据即可被搜索——无需等待刷盘,这就是"近实时"的来源。为什么 Elasticsearch 是"近实时"而不是"实时"?数据从写入到可搜索,中间存在约 1 秒的延迟(默认 refresh interval),这是性能与实时性的权衡:如果要求完全实时:每次写入都立即 fsync 到磁盘,I/O 开销巨大,写入吞吐量会急剧下降如果延迟过大:数据长时间不可搜索,用户体验差Elasticsearch 选择了 1 秒的折中方案,既保证了写入性能,又让数据几乎"立即可搜"。数据写入的完整流程一条文档从写入到可搜索,经历三个关键阶段:阶段一:写入 Memory Buffer + Translog写入请求到达 → 文档写入 Memory Buffer(此时不可搜索) → 同时写入 Translog(保证可靠性)Memory Buffer 是 JVM 堆内存中的一块区域,暂存未 refresh 的文档Translog(事务日志)同步写磁盘(可配置),防止宕机丢数据此时文档不可被搜索阶段二:Refresh(默认每秒一次)Refresh 触发 → Memory Buffer 中的文档生成新 Segment → Segment 写入 Filesystem Cache(OS 缓存,非磁盘) → 清空 Memory Buffer → 新 Segment 立即可被搜索Segment 是 Lucene 的倒排索引文件,一旦生成就是不可变的关键点:Segment 只需进入 OS 的文件系统缓存即可被读取,不必等到 fsync 刷盘这就是"近实时"的直接原因阶段三:Flush(默认每 30 分钟或 Translog 超 512MB)Flush 触发 → 执行 commit,将所有 Segment fsync 到磁盘 → 清空 Translog → 生成新的 commit pointFlush 是真正的持久化操作,但频率远低于 Refresh,避免频繁磁盘 I/O。Translog 如何保证数据不丢?Translog 是 Elasticsearch 数据可靠性的核心保障,有两种同步策略:| 配置 | 行为 | 可靠性 | 性能 ||------|------|--------|------|| async(默认) | 每 5 秒 fsync 一次 | 可能丢 5 秒数据 | 高 || request | 每次写请求都 fsync | 不丢数据 | 较低 |// 配置每次请求都同步 TranslogPUT /my_index/_settings{ "index.translog.durability": "request", "index.translog.sync_interval": "5s"}大多数场景用默认的 async 即可。如果业务对数据丢失零容忍(如金融交易),建议设为 request。Refresh Interval 调优实战场景一:批量导入数据时关闭 refresh// 批量导入前关闭 refreshPUT /my_index/_settings{ "index.refresh_interval": "-1"}// 导入完成后恢复PUT /my_index/_settings{ "index.refresh_interval": "1s"}关闭 refresh 后,批量导入速度可提升 2-5 倍,因为省去了每秒生成 Segment 的开销。场景二:对实时性要求不高的场景增大间隔日志分析场景中,数据延迟 30 秒可搜索完全可接受:PUT /log_index/_settings{ "index.refresh_interval": "30s"}这能显著降低 Segment 生成频率,减少 Segment Merge 压力。Segment Merge:不可忽视的后台开销每次 refresh 都会生成一个新 Segment。Segment 数量过多会严重影响搜索性能,Elasticsearch 后台会自动执行 Segment Merge:小 Segment 合并为大 Segment,减少文件数Merge 过程消耗 CPU 和磁盘 I/O如果 refresh 过于频繁(如 100ms),Segment 数量暴增,Merge 压力大这就是为什么 refresh interval 不能无限缩短——看似搜索更快了,实际上 Merge 开销可能拖垮整个集群。常见踩坑1. 写入后立即搜索不到这是最常见的问题。解决方案:// 手动触发 refreshPOST /my_index/_refresh// 或在写入时指定 refresh 参数PUT /my_index/_doc/1?refresh=true{ "title": "test"}?refresh=true 会在写入后立即执行 refresh,但会显著降低写入性能,不建议在批量写入时使用。2. Translog 过大导致恢复慢如果 Translog 积累过多(如关闭了 flush),节点重启时回放 Translog 会很慢。监控 Translog 大小:GET /my_index/_stats?filter_path=indices.*.translog3. Refresh 间隔设太短有人将 refresh_interval 设为 100ms 追求"更实时",结果 Segment 数量暴增,搜索性能反而下降。1 秒已经是工程实践的最佳平衡点。追问:能否做到真正的实时搜索?Elasticsearch 8.x 引入了索引排序(index sorting)等优化,但 refresh 机制的本质没有变。如果业务确实需要毫秒级延迟,可以考虑:使用 _refresh API 手动刷新(牺牲写入吞吐量)使用 Elasticsearch 的 Point-in-Time (PIT) API 保证查询一致性对实时性要求极高的场景,考虑用 Redis 等内存数据库做前置缓存但归根结底,Elasticsearch 的定位就是"近实时"搜索引擎,1 秒延迟是架构层面的权衡结果,并非缺陷。理解 refresh、translog、flush 三者的协作关系,是掌握 Elasticsearch 写入机制的关键。
服务端阅读 05月28日 02:00

Elasticsearch 集群架构中分片和副本的作用是什么?

Elasticsearch 的分布式能力建立在两个核心机制之上:分片(Shard) 和 副本(Replica)。分片解决"一台机器存不下、算不快"的问题,副本解决"一台机器挂了数据丢了"的问题。理解这两者的工作方式,是掌握 Elasticsearch 集群架构的关键。分片(Shard):水平拆分,并行提速分片是将一个索引拆分为多个独立存储单元的机制。每个分片本质上是一个完整的 Lucene 索引,可以独立存储和检索数据,分布在集群的不同节点上。水平扩展存储容量单节点存储有上限。假设一个索引有 60GB 数据,设置 number_of_shards=5,则每个分片存储约 12GB,可分散到 5 个节点上。数据量增长时,通过增加节点即可承载更多分片。并行提升查询性能搜索请求到达后,协调节点将查询分发到所有相关分片并行执行,各分片返回局部结果后由协调节点合并排序。5 个分片意味着 5 路并行,查询延迟显著降低。分片数量在索引创建时确定,不可修改这是一个常考的面试点。主分片数一旦设定就无法更改(因为文档路由公式依赖分片数)。如果需要调整,只能通过 Reindex 重建索引。设置时需预估数据规模:PUT /my_index{ "settings": { "number_of_shards": 3, "number_of_replicas": 1 }}副本(Replica):高可用与读扩展副本是主分片的完整拷贝,与主分片存储在不同节点上。它的核心价值有两个:故障容错和读性能提升。故障容错——节点宕机数据不丢失当持有主分片的节点故障时,集群会自动将对应的副本提升为主分片,保证数据可继续读写。故障节点恢复后,集群会重新同步数据,恢复副本。整个过程中,用户无感知。读请求负载均衡读操作(搜索、聚合)可以在主分片和副本上同时执行。3 个主分片 + 1 副本 = 6 个可读分片,查询吞吐量翻倍。写操作仍然只发生在主分片上,之后异步复制到副本。副本数量可以随时修改与分片不同,副本数可动态调整,不需要重建索引:PUT /my_index/_settings{ "number_of_replicas": 2}分片与副本的协作机制文档写入流程客户端发送写请求到协调节点协调节点根据文档 ID 计算目标分片:shard = hash(routing) % number_of_primary_shards请求转发到主分片所在节点,主分片完成写入主分片将数据同步到所有副本分片所有副本确认后,返回成功响应给客户端文档读取流程协调节点收到读请求根据路由信息确定目标分片组(主分片 + 副本)采用轮询策略在主分片和副本间选择一个执行查询,实现负载均衡返回结果故障转移流程当节点宕机时,集群执行以下步骤:主节点检测到节点离线,将集群状态标记为 yellow(有副本丢失)或 red(有主分片丢失)若主分片丢失,将对应副本提升为新主分片在剩余节点上重新分配分片,恢复副本数量数据通过副本重新同步,最终恢复为 green 状态配置原则与最佳实践分片数设置单分片建议大小 30-50GB,不超过 50GB分片数 = 预估数据量 / 单分片目标大小避免过度分片:每个分片消耗内存和文件句柄,分片过多导致性能下降官方建议:每个节点分片数不超过 堆内存(GB) × 20副本数设置生产环境至少 1 个副本,保证单节点故障不丢数据对读吞吐要求高的场景可增加到 2 个副本副本会增加存储开销和写入延迟(写操作需同步到所有副本),需要权衡常见配置错误小索引设置过多分片(如 1GB 数据设 5 个分片),浪费资源副本数为 0 且只有单节点,节点故障即数据丢失分片大小不均,部分热点分片成为瓶颈面试常见追问Q: 分片数为什么创建后不能改?文档路由公式 hash(routing) % primary_shards 依赖分片数。如果分片数变化,已有文档的路由结果改变,查询时无法定位到正确的分片。Q: 副本同步是同步还是异步?写入时是同步的——主分片写入后等待所有副本确认才返回成功(可通过 consistency 参数调整)。副本之间的分段合并等操作则是异步进行。Q: 如何选择分片数量?基于数据量和节点数预估。核心公式:分片数 = 总数据量 / 30GB(单分片建议上限)。同时确保分片数不超过 节点数 × 每节点分片上限。实际场景中需结合写入频率和查询复杂度调整。
服务端阅读 05月28日 01:59

Elasticsearch 有哪些字段类型?如何正确选择?

Elasticsearch 的字段类型直接决定了索引的存储方式、查询性能和分析能力。选错类型会导致分词异常、聚合失败、存储膨胀,甚至需要重建索引。下面从类型分类、核心类型详解、选型原则三个层面系统梳理。字段类型总览Elasticsearch 的字段类型可分为以下几类:| 类别 | 主要类型 | 典型场景 ||------|---------|---------|| 文本 | text、keyword | 全文搜索 / 精确匹配 || 数值 | integer、long、float、double、scaled_float | 范围查询、排序、聚合 || 日期 | date | 时间范围过滤 || 布尔 | boolean | 状态标记 || 复杂结构 | object、nested、flattened | 嵌套文档 || 地理 | geo_point、geo_shape | 位置搜索 || 语义搜索 | dense_vector、sparse_vector | 向量检索 || 排序特征 | rank_feature、rank_features | 影响相关性评分 || 自动补全 | search_as_you_type、completion | 搜索建议 || 其他 | ip、alias、constant_keyword、wildcard | 特殊用途 |核心类型详解text 与 keyword:最常混淆的一对text 和 keyword 是 Elasticsearch 中最基础也最容易被误用的两种类型。text 会经过分词器处理,拆分为多个 token 后建立倒排索引,适合 match 查询:"title": { "type": "text", "analyzer": "ik_max_word"}在 text 字段上执行 term 查询是常见错误——分词后的 token 与原始值不一致,term 查询会返回空结果。keyword 不分词,原样存储,适合 term 查询、排序和聚合:"status": { "type": "keyword", "ignore_above": 256}多字段模式是生产环境的标准做法,同一个业务字段同时提供 text 和 keyword 两种能力:"title": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword" } }}这样 title 用于全文搜索,title.keyword 用于精确匹配和聚合。数值类型:够用就行,不必贪大integer(4字节)、long(8字节)、float(4字节)、double(8字节)是最常用的数值类型。选型原则很简单:能满足需求的最小类型优先。scaled_float 适合货币等固定精度场景,它存储时自动乘以 scaling_factor 取整,查询时还原:"price": { "type": "scaled_float", "scaling_factor": 100}这样 99.99 实际存储为 9999(long),既省空间又避免浮点精度问题。注意:存储标识符(如数据库主键 ID)应使用 keyword 而非 long。数字类型的 term 查询会做数值比较,而标识符需要的是精确字符串匹配。date 类型:格式必须显式声明"created_at": { "type": "date", "format": "yyyy-MM-dd'T'HH:mm:ss.SSSZ||epoch_millis"}format 支持用 || 分隔多种格式。不指定时 Elasticsearch 会猜测,一旦数据中混入不同格式就会导致解析失败。始终显式声明 format 是最佳实践。object 与 nested:嵌套数据的两种处理方式object 类型会将嵌套字段扁平化存储。当数组中包含多个对象时,扁平化会导致跨字段的关联丢失:// 文档内容{"users": [{"name": "张三", "age": 25}, {"name": "李四", "age": 30}]}// object 扁平化后实际存储{"users.name": ["张三", "李四"], "users.age": [25, 30]}此时查询 name=张三 AND age=30 会误匹配,因为扁平化后张三和 30 没有关联。nested 类型为每个数组元素建立独立文档,保证字段间关联:"users": { "type": "nested", "properties": { "name": { "type": "keyword" }, "age": { "type": "integer" } }}查询时必须使用 nested 查询:{ "query": { "nested": { "path": "users", "query": { "bool": { "must": [ { "term": { "users.name": "张三" } }, { "term": { "users.age": 25 } } ] } } } }}nested 的代价是查询更复杂、索引更大。如果数组元素之间不需要跨字段关联查询,用 object 即可。flattened:动态元数据的轻量选择当日志或事件中包含大量不确定 key 的元数据字段时,逐个定义 mapping 既繁琐又浪费。flattened 将整个对象作为一个 keyword 存储:"metadata": { "type": "flattened"}支持对内部字段的查询和聚合,但不支持全文搜索。适合标签、注解等结构不固定的场景。geopoint 与 geoshape:地理信息处理"location": { "type": "geo_point"}geo_point 支持距离查询、范围过滤和聚合:{ "query": { "geo_distance": { "distance": "5km", "location": { "lat": 39.9, "lon": 116.4 } } }}geo_shape 用于复杂地理区域(多边形、线段),支持空间关系查询(相交、包含等)。dense_vector:语义搜索的基础"embedding": { "type": "dense_vector", "dims": 768, "index": true, "similarity": "cosine"}dense_vector 存储向量数据,配合 kNN 查询实现语义搜索。index: true(ES 8.0+)启用向量索引以加速近似最近邻检索。rank_feature:影响相关性但不参与过滤"page_rank": { "type": "rank_feature"},"hot_score": { "type": "rank_features"}rank_feature 存储单个正数,rank_features 存储 key-value 对。它们只能用于 rank_feature 查询中提升相关性评分,不能用于过滤或聚合。适合引入外部信号(如热度、权威度)影响排序。其他实用类型ip:存储 IPv4/IPv6,支持 CIDR 范围查询alias:字段别名,指向另一个字段,查询时自动转发constant_keyword:所有文档该字段值相同,优化存储wildcard:优化 wildcard 和 prefix 查询的性能searchasyou_type:专为即输即搜优化,自动构建 ngram 子字段completion:用于 suggester API 的自动补全runtime fields:查询时动态计算运行时字段不在索引时生成,而在查询时通过脚本计算,适合临时分析或验证逻辑:"runtime": { "price_gte_100": { "type": "boolean", "script": { "source": "emit(doc['price'].value >= 100)" } }}优点是无需 reindex 即可添加字段;缺点是每次查询都要计算,大量数据下性能损耗明显。生产环境中高频查询的字段应转为索引字段。选型原则第一步:确认查询方式| 查询方式 | 推荐类型 ||---------|---------|| 全文搜索 | text || 精确匹配 / 过滤 / 聚合 | keyword || 范围查询 | 数值类型 / date || 地理距离 | geo_point || 语义搜索 | dense_vector || 嵌套关联查询 | nested |第二步:考虑存储与性能keyword 比 text 索引开销小,高频过滤字段优先用 keywordscaled_float 在货币场景下比 double 更精确且更省空间nested 查询比 object 慢,只在需要字段关联时使用flattened 减少 mapping 膨胀,但牺牲全文搜索能力第三步:避免常见错误不要在 text 字段上执行 term 查询,用 match 或改用 keyword标识符字段用 keyword,不要用数值类型日期字段始终显式声明 format需要字段关联的嵌套数组必须用 nested,不能用 object运行时字段不要用于高频过滤或大规模聚合完整 mapping 示例{ "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword" } } }, "status": { "type": "keyword" }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "is_active": { "type": "boolean" }, "created_at": { "type": "date", "format": "yyyy-MM-dd'T'HH:mm:ss.SSSZ||epoch_millis" }, "location": { "type": "geo_point" }, "ip_address": { "type": "ip" }, "users": { "type": "nested", "properties": { "name": { "type": "keyword" }, "age": { "type": "integer" } } }, "metadata": { "type": "flattened" }, "embedding": { "type": "dense_vector", "dims": 768, "index": true, "similarity": "cosine" }, "hot_score": { "type": "rank_feature" } } }}选对字段类型是 Elasticsearch 索引设计的根基。先明确查询方式,再权衡存储与性能,最后对照常见错误排查——三步走基本覆盖绝大多数场景。如果业务演进导致类型需要变更,通过 reindex + alias 切换的方式在线完成,无需停服。
服务端阅读 05月27日 23:53

Elasticsearch 更新和删除操作的底层原理是什么?

Elasticsearch 底层基于 Lucene,而 Lucene 的段(segment)是不可变的。这意味着已写入段的文档无法原地修改或删除。Elasticsearch 的更新和删除操作都建立在这一约束之上,通过标记删除 + 重新索引的方式实现,再由段合并完成物理清理。更新操作:标记删除 + 重新索引Elasticsearch 的更新并不是原地修改文档。当你更新一个文档时,实际发生的是两步操作:旧文档在 .del 文件中被标记为 deleted新文档被索引到一个新的段中也就是说,更新 = 删除旧版本 + 插入新版本。这是由倒排索引的不可变性决定的——段一旦写入就无法修改,只能追加。PUT /products/_doc/1{ "name": "MacBook Pro", "price": 14999, "updated_at": "2025-01-15"}上述请求如果文档 ID=1 已存在,旧文档会被标记删除,新文档写入新段。如果不指定 ID,则直接作为新文档插入。部分更新(Partial Update)全量替换需要发送完整文档,网络开销大。部分更新通过 _update API 只修改指定字段,但底层仍然是标记删除 + 重新索引——只是服务端帮你完成了合并旧文档和新字段的步骤:POST /products/_update/1{ "doc": { "price": 12999 }}脚本更新对于需要动态计算的场景,可以用脚本更新:POST /products/_update/1{ "script": { "source": "ctx._source.price += params.delta", "params": { "delta": 500 } }}upsert 操作当不确定文档是否存在时,upsert 可以在文档不存在时插入、存在时更新:POST /products/_update/1{ "doc": { "price": 12999 }, "upsert": { "name": "MacBook Pro", "price": 12999 }}删除操作:逻辑删除与段合并清理删除文档时,Elasticsearch 不会立即从磁盘移除数据。而是在 .del 文件中标记该文档为 deleted 状态。被标记的文档仍然存在于段中,但查询时会被过滤掉。DELETE /products/_doc/1物理删除何时发生?物理删除发生在段合并(segment merge)过程中。Lucene 后台会定期将多个小段合并为大段,此时被标记为 deleted 的文档不会被写入新段,从而实现真正的磁盘空间回收。你也可以手动触发合并清理:POST /products/_forcemerge?only_expunge_deletes=trueonly_expunge_deletes=true 表示只合并含有删除文档的段,不影响无删除标记的段。按条件批量删除对于需要按查询条件删除的场景,使用 delete_by_query:POST /products/_delete_by_query{ "query": { "range": { "price": { "lte": 100 } } }}注意:delete_by_query 是先扫描再逐个标记删除,大数量下耗时长,建议在低峰期执行并设置 wait_for_completion=false 异步执行。版本控制与乐观并发_version 字段每个文档都有一个 _version 字段,每次写操作(index、update、delete)都会使版本号递增。这用于防止旧版本覆盖新版本——如果一个更新请求基于的版本号已过期,操作会被拒绝。乐观并发控制Elasticsearch 使用 if_seq_no 和 if_primary_term 实现乐观并发控制(OCC)。在读取文档时获取当前的 seqno 和 primaryterm,更新时带上这两个值,如果文档已被其他操作修改(seq_no 已变),则返回 409 冲突:PUT /products/_doc/1?if_seq_no=5&if_primary_term=1{ "name": "MacBook Pro", "price": 13999}如果不做并发控制,两个请求同时更新同一文档,后到的请求会覆盖先到的结果——这在电商库存扣减等场景下是严重问题。近实时搜索与 refresh 机制文档写入后并不是立即可搜索。Elasticsearch 的写入流程是:文档先写入内存缓冲区(index buffer)同时写入 translog(事务日志,保证持久性)每隔 refresh_interval(默认 1s)执行一次 refresh,将内存缓冲区的数据写入新段,文档变为可搜索这意味着更新和删除操作也有近一秒的延迟才对搜索可见。生产环境中,可以适当调大 refresh_interval(如 30s)来提升写入吞吐量,代价是搜索可见延迟增加。性能优化要点更新场景:优先使用部分更新而非全量替换,减少网络传输和 _source 重写开销高频更新使用 Bulk API 批量提交避免在热索引上频繁单条更新,考虑异步队列聚合后批量写入删除场景:大批量删除用 delete_by_query 而非逐条 DELETE删除后若段膨胀明显,执行 force_merge 回收空间(只对只读索引执行,否则可能产生超大段)删除大量数据后关注磁盘水位,段合并需要额外磁盘空间通用建议:监控 GET /_nodes/stats/indexing 中的索引吞吐和删除计数调整 index.merge.policy 控制段合并策略和频率更新和删除都会产生 translog 和段碎片,定期评估索引是否需要 reindex面试中回答这个问题,核心要讲清楚三点:段不可变导致更新是删除+插入、删除是逻辑标记物理清理靠段合并、并发控制靠 seqno/primaryterm 实现乐观锁。理解这三层,就能应对追问。
服务端阅读 05月27日 23:52

Elasticsearch 如何实现高可用和容灾备份?

Elasticsearch 在日志分析、全文检索、可观测性等场景中承担核心存储角色,一旦集群不可用,下游查询和写入全部中断。高可用保证单节点/单机房故障后服务继续运行,容灾备份保证数据在区域性灾难后可恢复。两者机制不同,缺一不可。高可用:集群内故障自愈分片与副本——数据冗余的基石Elasticsearch 将每个索引拆分为多个主分片(primary shard),每个主分片可配置若干副本分片(replica shard)。主分片与副本分片分布在不同节点上:主分片故障:副本自动提升为新主分片,数据零丢失,查询不中断。副本分片故障:主分片仍在,集群自动在其他节点重建副本。动态调整:副本数可在索引运行时修改,主分片数创建后不可更改,需提前规划。PUT /my_index{ "settings": { "number_of_shards": 3, "number_of_replicas": 1 }}生产环境建议 number_of_replicas >= 1,关键业务设为 2,可容忍单节点故障且仍有冗余。但副本越多写入吞吐越低(每个写操作需同步到所有副本),需在可用性与性能间取舍。节点角色分离生产集群至少 3 节点,建议按角色分离:| 角色 | 配置 | 职责 | 最低数量 ||------|------|------|----------|| 专用主节点 | node.master: true, node.data: false | 集群管理、元数据维护 | 3 || 数据节点 | node.master: false, node.data: true | 存储分片数据、执行 CRUD | 按数据量扩容 || 协调节点 | node.master: false, node.data: false | 请求路由、结果聚合 | 2+ |专用主节点不存数据、不处理查询,资源占用低但保障选主稳定。只有 2 个候选主节点时容易出现选不出 master 的问题,必须保证奇数个候选节点。脑裂防护网络分区可能导致两个子集群各自选主,产生"脑裂",数据不一致。Elasticsearch 7.x+ 已废弃 discovery.zen.minimum_master_nodes,改为自动计算法定人数(quorum),但理解其原理仍然关键:7.x 之前:手动设置 discovery.zen.minimum_master_nodes 为 (候选主节点数 / 2) + 1,确保只有多数派能选主。7.x+:由集群自动管理,但前提是正确配置 cluster.initial_master_nodes,首次启动时指定初始主节点列表。# elasticsearch.yml — 首次启动配置discovery.seed_hosts: ["es-node1", "es-node2", "es-node3"]cluster.initial_master_nodes: ["es-node1", "es-node2", "es-node3"]集群健康与故障恢复集群状态直观反映可用性:green:所有主分片和副本分片正常。yellow:主分片正常,部分副本缺失(单节点故障时常见,服务仍可用)。red:部分主分片不可用,数据有丢失风险。# 查看集群健康curl -XGET "http://localhost:9200/_cluster/health?pretty"# 查看分片分配情况curl -XGET "http://localhost:9200/_cat/shards?v"节点故障后,集群自动执行分片重平衡:提升副本为主分片 → 在存活节点重建副本 → 数据重新均衡。此过程对应用透明,但重平衡期间查询性能可能下降。容灾备份:跨机房/跨区域数据保护高可用解决的是集群内单点故障,但整个机房故障(断电、网络中断、自然灾害)需要容灾方案。Elasticsearch 提供两条路径:快照恢复(冷备份)和跨集群复制 CCR(热备份)。快照与恢复(Snapshot & Restore)快照将索引数据备份到外部存储(本地磁盘、S3、HDFS 等),支持增量备份和按时间点恢复。1. 注册快照仓库PUT /_snapshot/my_backup{ "type": "fs", "settings": { "location": "/var/backups/elasticsearch" }}S3 仓库需要安装 repository-s3 插件:PUT /_snapshot/s3_backup{ "type": "s3", "settings": { "bucket": "my-backup-bucket", "region": "us-east-1", "base_path": "es-snapshots" }}2. 创建快照curl -XPUT "http://localhost:9200/_snapshot/my_backup/snapshot-20260527" \ -H "Content-Type: application/json" -d '{ "indices": "*,-.monitoring*,-.security*", "ignore_unavailable": true, "include_global_state": false}'注意排除系统索引(.monitoring*、.security*、.ds* 等),避免恢复时覆盖集群安全配置。3. 自动定期备份通过 SLM(Snapshot Lifecycle Management,8.x 内置)自动执行:PUT /_slm/policy/daily-snapshots{ "schedule": "0 30 2 * * ?", "name": "<daily-snap-{now/d}>", "repository": "my_backup", "config": { "indices": ["*", "-.monitoring*", "-.security*"], "ignore_unavailable": true, "include_global_state": false }, "retention": { "expire_after": "30d", "min_count": 5, "max_count": 50 }}4. 从快照恢复POST /_snapshot/my_backup/snapshot-20260527/_restore{ "indices": "my_index", "include_aliases": true}恢复时目标索引必须不存在(或使用 rename_pattern 重命名)。整个集群不可用时,需先重建集群再恢复快照。跨集群复制 CCR(Cross-Cluster Replication)CCR 是 Elasticsearch 白金版功能,实现主集群到从集群的近实时索引复制,适用于异地容灾和读写分离。工作流程:配置远程集群:在从集群中声明主集群的连接信息。创建 Follower 索引:从集群以只读方式持续拉取主集群的变更(先全量复制 segment,再增量同步 translog)。灾难切换:主集群不可用时,将 Follower 索引转为普通索引(POST /follower_index/_ccr/unfollow),接管读写流量。PUT /_cluster/settings{ "persistent": { "cluster": { "remote": { "leader-cluster": { "seeds": ["10.0.1.10:9300"] } } } }}PUT /follower_index/_ccr/follow{ "remote_cluster": "leader-cluster", "leader_index": "leader_index"}关键限制:需要白金版许可证。从集群版本必须 >= 主集群版本。Follower 索引只读,需 unfollow 后才可写入。ccr.indices.recovery.max_bytes_per_sec 控制复制带宽(默认 40MB/s)。快照 vs CCR 对比| 维度 | 快照恢复 | CCR ||------|----------|-----|| 数据延迟 | 分钟~小时级(取决于备份频率) | 秒级近实时 || 恢复速度 | 需重建索引,分钟~小时级 | 秒级切换 || 成本 | 低(对象存储) | 高(需独立集群 + 白金许可) || 适用场景 | 数据归档、时间点恢复、开发测试 | 异地热备、业务连续性要求高 || 许可证 | 基础版即可 | 白金版 |生产环境建议两者结合:CCR 保障实时容灾,快照提供长期归档和时间点回溯能力。生产环境关键配置清单防止数据丢失# elasticsearch.yml# 每个索引默认至少 1 个副本index.number_of_replicas: 1# 刷新间隔,写入密集场景可适当增大index.refresh_interval: 1s# Translog 持久化策略:每次写操作后 fsyncindex.translog.durability: request索引生命周期管理(ILM)ILM 自动管理索引的分片数、副本数、迁移和删除,避免冷数据无限膨胀:PUT /_ilm/policy/hot-warm-delete{ "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_age": "7d", "max_primary_shard_size": "50gb" } } }, "warm": { "min_age": "30d", "actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 }, "allocate": { "require": { "data": "warm" } } } }, "delete": { "min_age": "90d", "actions": { "delete": {} } } } }}热节点用 SSD 存储近期活跃数据,温节点用 SATA 存储历史数据,ILM 自动将索引从热节点迁移到温节点,90 天后自动删除。冷热分层可降低 40%~60% 存储成本。容灾演练容灾方案不演练等于没有。建议每季度执行:节点级:关闭一个数据节点,观察副本提升和集群重平衡。索引级:删除一个索引,从快照恢复,验证数据完整性(对比文档数 _count)。集群级:主集群断网,将 CCR Follower unfollow 接管,验证读写正常。# 验证恢复后文档数一致curl -XGET "http://localhost:9200/my_index/_count"面试追问方向RPO 和 RTO 分别是什么? RPO(Recovery Point Objective)是可接受的数据丢失量,RTO(Recovery Time Objective)是可接受的服务中断时长。快照方案的 RPO 取决于备份频率,CCR 的 RPO 为秒级。副本数设为 2 写入性能下降多少? 通常下降 30%~40%,因为每次写操作需同步到主分片 + 2 个副本。写密集场景可设为 1 个副本,读密集场景增加副本数提升吞吐。主分片数为什么不能改? 主分片数决定了文档的路由公式 shard = hash(routing) % number_of_primary_shards,修改后所有文档的路由全部失效。扩容只能通过创建新索引 + reindex 实现。CCR 和 Snapshot 能否替代彼此? 不能。CCR 是实时热备但无法回溯历史时间点,Snapshot 是冷备但支持时间点恢复和长期归档。两者互补。
服务端阅读 05月27日 23:51

Elasticsearch scroll 滚动查询和搜索上下文有哪些核心特点?

scroll 滚动查询是什么?为什么需要它?Elasticsearch 的标准分页(from + size)在深度分页时性能急剧下降——获取第100页时,每个分片都要检索前1000+条数据,协调节点再做全局排序。ES 默认限制 from + size 不超过 10000(index.max_result_window)。scroll 滚动查询就是为解决这个问题设计的:它发起一次查询后,在服务端创建一个搜索上下文快照,后续通过 scroll_id 逐批拉取数据,无需重复排序。核心机制:快照语义:scroll 返回的是发起查询时刻的索引快照,之后的文档增删改不会影响结果两阶段搜索:首次请求执行 Query(获取文档ID列表)+ Fetch(拉取文档内容),后续滚动请求只做 Fetch有状态:scroll_id 在服务端持久化,直到超时或显式清除// 1. 初始化 scroll 查询GET /products/_search?scroll=5m{ "size": 1000, "query": { "match_all": {} }}// 2. 使用 scroll_id 继续拉取GET /_search/scroll{ "scroll": "5m", "scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlk..."}// 3. 清除 scroll 上下文(重要!)DELETE /_search/scroll{ "scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlk..."}适用场景: 数据导出、reindex 重建索引、ETL 批量处理等离线任务。不适用场景: 实时分页请求——scroll 上下文占用堆内存,长时间不清理会导致资源泄漏。搜索上下文(search context)是什么?每次 _search 请求都会创建搜索上下文,它维护了查询生命周期内的状态,包括:Query 阶段的匹配文档ID列表排序、聚合、高亮等操作所需的中间状态请求级别的缓存信息关键特征:普通搜索的上下文在请求结束后自动销毁scroll 查询的上下文会持续存活直到超时上下文数量受 search.max_open_scroll_context 限制(默认500)搜索上下文本身不是一种"查询方式",而是 scroll、聚合、高亮等功能的底层支撑。面试中常把"搜索上下文"和"scroll 上下文"混谈,核心区别在于生命周期:前者随请求结束而销毁,后者由 scroll 参数控制存活时间。scroll、search_after、from+size 三种分页怎么选?| 对比维度 | from + size | scroll | search_after ||---|---|---|---|| 原理 | 偏移量跳过 | 快照 + 游标批量拉取 | 排序值游标逐页前进 || 状态 | 无状态 | 有状态(服务端保存快照) | 无状态 || 深度分页性能 | 差(O(n)排序开销) | 好(一次排序分批取) | 好(基于排序值定位) || 实时性 | 实时 | 快照,不反映后续变更 | 实时 || 随机跳页 | 支持 | 不支持 | 不支持 || 资源消耗 | 深分页时高 | 占用堆内存直到超时 | 低 || 典型场景 | Top N 查询 | 批量导出/重建索引 | 实时深度分页 |选择建议:数据量小、页码浅:from + size,简单直接批量离线处理:scroll实时深度分页:search_after需要一致性视图 + 实时分页:search_after + PIT(Point in Time)Sliced Scroll 如何提升并行处理效率?单条 scroll 串行拉取大量数据时效率有限。ES 提供 Sliced Scroll,将一个 scroll 查询切分为多个切片,并行拉取:GET /products/_search?scroll=5m{ "size": 1000, "slice": { "id": 0, "max": 4 }, "query": { "match_all": {} }}max 为切片总数,id 为当前切片编号(0 到 max-1)。每个切片独立返回一部分数据,多个线程/进程可并行拉取不同切片,显著缩短总耗时。注意: 切片数不宜超过分片数,否则部分切片无数据可返回。面试高频追问Q1: scroll 的 scrollid 会变吗?会。每次滚动请求返回新的 scrollid,客户端应始终使用最新返回的值。Q2: 忘记清除 scroll 上下文会怎样?上下文会持续占用堆内存直到超时。大量未清除的上下文可能导致 OOM,生产环境务必在处理完成后调用 DELETE /_search/scroll 清理。Q3: PIT + searchafter 和 scroll 有什么区别?PIT(Point in Time)也创建快照,但更轻量,与 searchafter 配合可实现一致性视图的实时分页。scroll 适合一次性全量遍历,PIT + search_after 适合交互式逐页浏览。ES 7.10+ 推荐用 PIT 替代 scroll 做深度分页。Q4: scroll 查询期间索引发生变更怎么办?scroll 基于快照,索引变更不影响已发起的 scroll 结果。但新文档不会出现在结果中,已删除文档可能仍存在——这取决于快照创建时机。