<?xml version="1.0" encoding="utf-8"?>
<ul>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本附录汇总常用 API、查询、聚合、Mapping、配置、指标和排查命令。建议把它当作工作台参考，但不要未经压测直接把参数复制到生产环境。1. 集群与节点 API1.1 集群健康GET /_cluster/healthGET /_cluster/health?wait_for_status=yellow&amp;timeout=10sGET /_cluster/health?level=indices1.2 节点GET /_cat/nodes?vGET /_cat/nodes?v&amp;h=name,id,node.role,master,heap.percent,ram.percent,cpu,load_average,disk.used_percentGET /_nodes/stats/jvm,os,fs,indices?humanGET /_nodes/hot_threads1.3 分片与分配GET /_cat/shards?vGET /_cat/shards?v&amp;h=index,shard,prirep,state,docs,store,node,unassigned.reasonGET /_cat/allocation?vGET /_cluster/allocation/explainGET /_cat/recovery?v&amp;active_only=true1.4 索引GET /_cat/indices?vGET /_cat/indices?v&amp;health=yellowGET /_cat/indices/order*?v&amp;h=index,health,status,docs.count,store.size,pri.store.sizeGET /orders-v3/_stats?humanGET /orders-v3/_segments?humanGET /orders-v3/_settingsGET /orders-v3/_mapping1.5 任务与线程池GET /_cat/tasks?vGET /_tasks?actions=*search*&amp;detailedPOST /_tasks/TASK_ID/_cancelGET /_cat/thread_pool/write,search?v&amp;h=node_name,name,active,queue,rejected,completedGET /_cat/pending_tasks?v2. 文档操作2.1 写入PUT /products-v3/_doc/sku_10001{  "sku_id": "sku_10001",  "title": "轻薄笔记本",  "price": 5999.00}只创建，不允许覆盖：PUT /products-v3/_create/sku_10001{  "sku_id": "sku_10001"}2.2 查询GET /products-v3/_doc/sku_10001GET /products-v3/_source/sku_10001HEAD /products-v3/_doc/sku_100012.3 更新POST /products-v3/_update/sku_10001{  "doc": {    "price": 5499.00  }}脚本更新：POST /products-v3/_update/sku_10001?retry_on_conflict=3{  "script": {    "lang": "painless",    "source": "ctx._source.stock += params.delta",    "params": { "delta": -1 }  }}2.4 删除DELETE /products-v3/_doc/sku_10001POST /products-v3/_delete_by_query?conflicts=proceed{  "query": {    "term": { "status": "deleted" }  }}2.5 BulkPOST /_bulk{"index": {"_index": "products-v3", "_id": "sku_1"}}{"sku_id": "sku_1", "title": "无线鼠标", "price": 99}{"update": {"_index": "products-v3", "_id": "sku_2"}}{"doc": {"price": 199}}{"delete": {"_index": "products-v3", "_id": "sku_3"}}Bulk 必须逐项检查："items": [  {    "index": {      "status": 429,      "error": { "type": "es_rejected_execution_exception" }    }  }]3. 查询 DSL 速查3.1 termGET /products-v3/_search{  "query": {    "term": { "status": "on_sale" }  }}3.2 termsGET /products-v3/_search{  "query": {    "terms": {      "category_id": ["cat_1", "cat_2"]    }  }}3.3 matchGET /products-v3/_search{  "query": {    "match": {      "title": {        "query": "轻薄笔记本",        "operator": "and"      }    }  }}3.4 match_phraseGET /products-v3/_search{  "query": {    "match_phrase": {      "title": {        "query": "无线鼠标",        "slop": 1      }    }  }}3.5 multi_matchGET /products-v3/_search{  "query": {    "multi_match": {      "query": "轻薄笔记本",      "type": "best_fields",      "fields": ["title^4", "keywords^2", "combined_text"],      "minimum_should_match": "2&lt;75%"    }  }}3.6 rangeGET /orders-v3/_search{  "query": {    "range": {      "paid_at": {        "gte": "now-1d/d",        "lte": "now"      }    }  }}3.7 boolGET /products-v3/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "should": [        { "term": { "tags": "hot" } }      ],      "filter": [        { "term": { "status": "on_sale" } },        { "range": { "price": { "gte": 3000, "lte": 8000 } } }      ],      "must_not": [        { "term": { "tags": "blocked" } }      ]    }  }}3.8 nestedGET /products-v3/_search{  "query": {    "nested": {      "path": "attrs",      "query": {        "bool": {          "filter": [            { "term": { "attrs.name": "颜色" } },            { "term": { "attrs.value": "红色" } }          ]        }      }    }  }}3.9 分页GET /orders-v3/_search{  "from": 0,  "size": 20,  "sort": [    { "paid_at": "desc" },    { "_id": "asc" }  ]}Search After：GET /orders-v3/_search{  "size": 20,  "sort": [    { "paid_at": "desc" },    { "_id": "asc" }  ],  "search_after": ["2026-08-25T10:00:00Z", "order_10001"]}3.10 高亮GET /products-v3/_search{  "query": {    "match": { "title": "笔记本" }  },  "highlight": {    "fields": {      "title": {}    },    "pre_tags": ["&lt;em&gt;"],    "post_tags": ["&lt;/em&gt;"]  }}4. 聚合速查4.1 termsGET /orders-v3/_search{  "size": 0,  "aggs": {    "top_categories": {      "terms": {        "field": "category_id",        "size": 20,        "order": { "_count": "desc" }      }    }  }}4.2 指标GET /orders-v3/_search{  "size": 0,  "aggs": {    "gmv": { "sum": { "field": "pay_amount" } },    "avg_amount": { "avg": { "field": "pay_amount" } },    "max_amount": { "max": { "field": "pay_amount" } },    "min_amount": { "min": { "field": "pay_amount" } },    "buyers": { "cardinality": { "field": "user_id" } }  }}4.3 时间分桶GET /orders-v3/_search{  "size": 0,  "query": {    "range": { "paid_at": { "gte": "now-7d/d", "lte": "now" } }  },  "aggs": {    "per_hour": {      "date_histogram": {        "field": "paid_at",        "fixed_interval": "1h",        "time_zone": "Asia/Shanghai",        "min_doc_count": 0      }    }  }}4.4 rangeGET /products-v3/_search{  "size": 0,  "aggs": {    "price_ranges": {      "range": {        "field": "price",        "ranges": [          { "to": 100 },          { "from": 100, "to": 500 },          { "from": 500 }        ]      }    }  }}4.5 filter 嵌套GET /orders-v3/_search{  "size": 0,  "aggs": {    "paid": {      "filter": { "term": { "pay_status": "success" } },      "aggs": {        "gmv": { "sum": { "field": "pay_amount" } }      }    }  }}4.6 compositeGET /orders-v3/_search{  "size": 0,  "aggs": {    "groups": {      "composite": {        "size": 1000,        "sources": [          { "channel": { "terms": { "field": "channel" } } },          { "city": { "terms": { "field": "city_id" } } }        ]      }    }  }}5. Mapping 速查5.1 常用类型            类型      用途                  text      全文检索              keyword      精确匹配、聚合、排序              long / integer / short / byte      整数              double / float      浮点数              scaled_float      固定精度金额              date      时间              boolean      布尔              ip      IP 查询              object      JSON 对象              nested      数组内对象边界              join      父子关系              dense_vector      向量字段              geo_point / geo_shape      地理位置和形状              alias      字段别名      5.2 keyword + textPUT /articles-v1{  "mappings": {    "properties": {      "title": {        "type": "text",        "fields": {          "keyword": { "type": "keyword", "ignore_above": 256 }        }      }    }  }}5.3 只查不聚合"trace_id": {  "type": "keyword",  "doc_values": false}5.4 只聚合不查"city_id": {  "type": "keyword",  "index": false}5.5 dense_vector"embedding": {  "type": "dense_vector",  "dims": 768,  "index": true,  "similarity": "cosine"}6. 索引模板与 ILM6.1 Component TemplatePUT /_component_template/app-logs-settings{  "template": {    "settings": {      "number_of_shards": 3,      "number_of_replicas": 1,      "refresh_interval": "5s"    }  }}6.2 Index TemplatePUT /_index_template/app-logs{  "index_patterns": ["app-logs-*"],  "data_stream": {},  "composed_of": ["app-logs-settings"],  "priority": 200}6.3 ILMPUT /_ilm/policy/app-logs-policy{  "policy": {    "phases": {      "hot": {        "actions": {          "rollover": {            "max_primary_shard_size": "50gb",            "max_age": "1d"          }        }      },      "delete": {        "min_age": "30d",        "actions": { "delete": {} }      }    }  }}6.4 ILM 状态GET /app-logs*/_ilm/explain7. 别名与 Reindex7.1 别名POST /_aliases{  "actions": [    { "add": { "index": "products-v3", "alias": "products-read" } },    { "add": { "index": "products-v3", "alias": "products-write" } }  ]}7.2 原子切换POST /_aliases{  "actions": [    { "remove": { "index": "products-v3", "alias": "products-read" } },    { "add": { "index": "products-v4", "alias": "products-read" } }  ]}7.3 ReindexPOST /_reindex?wait_for_completion=false&amp;slices=auto&amp;refresh=true{  "source": {    "index": "products-v3",    "size": 1000  },  "dest": {    "index": "products-v4",    "op_type": "create"  }}查看任务：GET /_tasks?actions=*reindex&amp;detailed8. 分析与分词8.1 AnalyzePOST /_analyze{  "analyzer": "standard",  "text": "Elasticsearch 搜索引擎"}8.2 自定义 AnalyzerPUT /articles-v1{  "settings": {    "analysis": {      "filter": {        "my_stop": {          "type": "stop",          "stopwords": ["的", "了"]        }      },      "analyzer": {        "my_analyzer": {          "tokenizer": "standard",          "filter": ["lowercase", "my_stop"]        }      }    }  }}8.3 同义词"synonym_filter": {  "type": "synonym_graph",  "synonyms": [    "手机,智能手机,移动电话",    "电脑,笔记本"  ]}9. 快照与恢复9.1 注册仓库PUT /_snapshot/backup-repo{  "type": "fs",  "settings": {    "location": "/mnt/es-backup"  }}9.2 快照PUT /_snapshot/backup-repo/snapshot-2026.08.25?wait_for_completion=false{  "indices": "orders-v3,products-v3",  "include_global_state": false}9.3 查看GET /_snapshot/backup-repo/_currentGET /_snapshot/backup-repo/snapshot-2026.08.259.4 恢复POST /_snapshot/backup-repo/snapshot-2026.08.25/_restore?wait_for_completion=false{  "indices": "orders-v3",  "rename_pattern": "(.+)",  "rename_replacement": "restored-$1"}10. 常用设置10.1 索引设置PUT /orders-v3/_settings{  "number_of_replicas": 1,  "refresh_interval": "5s"}10.2 只读限制PUT /orders-v3/_settings{  "blocks.read_only_allow_delete": true}解除：PUT /orders-v3/_settings{  "blocks.read_only_allow_delete": null}10.3 分页限制PUT /orders-v3/_settings{  "index.max_result_window": 10000}10.4 慢查询PUT /orders-v3/_settings{  "index.search.slowlog.threshold.query.warn": "5s",  "index.search.slowlog.threshold.query.info": "2s",  "index.search.slowlog.threshold.fetch.warn": "1s"}11. 关键指标            指标      说明                  cluster status      green/yellow/red              number_of_nodes      节点数              unassigned_shards      未分配分片              pending_tasks      master 任务队列              heap used percent      JVM 堆使用率              old GC count/time      老年代回收              disk used percent      磁盘使用率              write rejected      写入拒绝              search rejected      查询拒绝              query_time / query_total      查询平均耗时              indexing_time / indexing_total      写入平均耗时              merge total/throttled time      合并压力              refresh/flush time      刷新与提交耗时              breaker tripped      熔断次数              recovery bytes      恢复流量              snapshot success      快照成功率      12. 故障排查命令12.1 集群异常GET /_cluster/health?level=indicesGET /_cat/indices?v&amp;health=redGET /_cat/shards?v&amp;h=index,shard,prirep,state,store,node,unassigned.reasonGET /_cluster/allocation/explain12.2 写入异常GET /_cat/thread_pool/write?vGET /_nodes/stats/indices/indexing,merge,refresh,flush?humanGET /_cat/allocation?v12.3 查询异常GET /_cat/thread_pool/search?vGET /_nodes/stats/indices/search?humanGET /_tasks?actions=*search*&amp;detailedGET /_nodes/hot_threads12.4 磁盘异常GET /_cat/allocation?vGET /_nodes/stats/fs?humanGET /_all/_settings?filter_path=*.settings.index.blocks.read_only_allow_delete13. 黄金清单13.1 Mapping  明确事实来源和文档粒度；  ID、枚举、标签用 keyword；  标题描述用 text；  排序聚合字段保留 doc_values；  高基数字段禁止自由聚合；  嵌套结构有数量上限；  生产索引使用 dynamic strict 或 false。13.2 查询  必须限制索引和时间范围；  filter 优先；  控制 size；  深分页使用 search after；  大导出使用 PIT；  避免前置通配符；  聚合控制桶数；  慢查询接入日志平台。13.3 写入  使用 Bulk；  明确文档 ID 和幂等；  429 指数退避；  按场景设置 refresh interval；  避免热点文档频繁 update；  监控 write rejected 和 merge；  导入后恢复副本。13.4 集群  master quorum 至少 3；  分片大小和数量有容量依据；  副本分布跨故障域；  磁盘提前告警；  每日快照并异地保存；  定期恢复演练；  滚动升级控制 allocation；  保持 cluster state 可控。13.5 安全  TLS 全链路启用；  不使用超级用户给应用；  API key 有过期和轮换；  索引权限最小化；  敏感字段使用 DLS/FLS；  高危操作有审批和审计；  集群和 Kibana 不暴露公网。14. 推荐版本  学习环境：Elasticsearch 8.x 或 9.x；  Java 客户端：Elasticsearch Java API Client；  Spring：Spring Boot 3.x；  Kibana：与服务端主版本匹配；  采集：Filebeat / OpenTelemetry Collector；  压测：Rally；  监控：Prometheus / Grafana。15. 一页排障图集群 red  -&gt; cat indices health=red  -&gt; cat shards + allocation explain  -&gt; 恢复节点 / 磁盘 / 分配 / 快照集群 yellow  -&gt; allocation explain  -&gt; 节点数 / 磁盘水位 / 分配规则 / 副本数写入 429  -&gt; write rejected  -&gt; bulk 策略 / merge / 磁盘 / 副本恢复  -&gt; 客户端限流退避查询慢  -&gt; search latency + rejected  -&gt; 索引范围 / 时间范围 / from+size / profile  -&gt; 缩小范围，取消大任务JVM 高  -&gt; GC + breaker + hot threads  -&gt; 大聚合 / 深分页 / scroll  -&gt; 取消任务并治理请求磁盘满  -&gt; cat allocation  -&gt; 清理 / 扩容 / 降低副本  -&gt; 解除 read-only block</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。读完前面章节，你已经能使用 Elasticsearch 构建搜索、日志和分析系统。大师之路的下一步，是从“会用配置”走向“理解系统、能读源码、能做设计、能带团队治理”。本章给出学习路线、源码阅读方法、调试手段和成长地图。35.1 能力模型            级别      能力特征                  L1 入门      会 CRUD、Mapping、基本查询和 Kibana              L2 熟练      能设计 Mapping、批量写入、组合查询和聚合              L3 工程师      能落地项目，处理别名、reindex、分页、性能和监控              L4 专家      能做容量规划、故障定位、安全治理、架构演进              L5 大师      能读源码、参与社区、设计大型平台和培养团队      自检问题：  能否解释一次查询和一次写入的完整链路？  能否根据数据规模设计分片和副本？  能否用 profile、stats、allocation explain 定位问题？  能否设计零停机变更和回滚？  能否评估一个方案的成本和风险？  能否带新人做一次完整故障复盘？35.2 学习路线第一阶段：基础扎实目标：独立完成 CRUD 和查询。  文档模型；  Mapping；  Analyzer；  Query DSL；  Bulk；  Alias；  Kibana Discover。验收项目：给 MySQL 商品表做一个搜索接口，支持关键词、类目、价格过滤和分页。第二阶段：搜索工程目标：做出可运营的搜索产品。  多字段召回；  filter 与评分；  同义词和词库；  function score；  搜索日志；  无结果治理；  评测集和 A/B 实验。验收项目：实现商品搜索 v2，支持筛选、聚合、业务排序、点击日志和 bad case 治理。第三阶段：数据平台目标：稳定承接日志和分析。  Data Stream；  ILM；  rollover；  冷热分层；  采集链路；  权限和脱敏；  预聚合。验收项目：设计日志平台，支持 7 天热数据、30 天保留、按团队权限查询和错误率告警。第四阶段：生产治理目标：独立负责集群可靠性。  分片规划；  快照恢复；  监控告警；  容量预测；  滚动升级；  故障演练；  安全加固。验收项目：输出集群 Runbook、容量报告、备份恢复演练报告和事故复盘。第五阶段：深入内核目标：从源码和论文层面理解系统。  Lucene 数据结构；  FST、posting、Doc Values；  段合并策略；  查询执行；  分布式一致性；  集群状态发布；  熔断器和资源隔离。验收项目：阅读一个功能的源码，输出设计文档或提交 issue/PR。35.3 源码结构Elasticsearch 主要仓库常见模块：            模块      职责                  server      核心服务、集群、索引、传输、设置              client      客户端相关代码              modules      ingest、analysis、snapshot 等模块              plugins      插件实现              test      测试框架和集成测试              distribution      打包和发行      重要包通常包括：org.elasticsearch.index       索引读写org.elasticsearch.search     查询执行org.elasticsearch.cluster    集群状态与分配org.elasticsearch.action     REST action 组织org.elasticsearch.transport  节点通信org.elasticsearch.indices    索引生命周期org.elasticsearch.node       节点启动Lucene 仓库关注：            包      职责                  org.apache.lucene.index      索引写入、Segment、merge              org.apache.lucene.search      查询树、评分、Collector              org.apache.lucene.store      数据目录和 IO              org.apache.lucene.codecs      编码格式              org.apache.lucene.document      字段和文档类型              org.apache.lucene.util      FST、位图、数组等基础结构      35.4 源码阅读路线建议按请求链路阅读，而不是按目录顺序阅读。4.1 写入链路RestIndexAction  -&gt; TransportBulkAction  -&gt; TransportShardBulkAction  -&gt; IndexShard  -&gt; InternalIndexingEngine  -&gt; Lucene IndexWriter关注：  文档如何解析；  Mapping 如何更新；  routing 如何计算；  主副本如何同步；  translog 何时写入；  refresh 如何触发；  版本冲突如何判断。4.2 查询链路RestSearchAction  -&gt; TransportSearchAction  -&gt; SearchPhaseController  -&gt; SearchService  -&gt; SearchContext  -&gt; Lucene Searcher关注：  query phase 如何分发；  fetch phase 何时执行；  每个分片返回什么；  排序如何归并；  超时和取消在哪里检查；  聚合如何收集；  缓存何时生效。4.3 集群分配链路ClusterService  -&gt; ClusterStatePublisher  -&gt; AllocationService  -&gt; ShardAllocationDecision  -&gt; BalancedShardsAllocator关注：  节点加入离开如何处理；  master 如何发布状态；  allocation decider 有哪些；  watermark 如何参与决策；  rebalance 什么时候触发；  recovery 如何限流。35.5 本地调试准备环境：git clone https://github.com/elastic/elasticsearch.gitcd elasticsearch./gradlew compileJava./gradlew :server:check常见调试方式：  用 IDE 启动 Elasticsearch 单节点；  在 action、transport、engine 关键类打断点；  用 Kibana Dev Tools 发请求；  打开 trace 日志；  阅读对应 REST YAML 测试；  从失败测试反向定位代码。调试建议：  一次只追一个请求；  先画时序图；  记录类和方法职责；  对照官方文档；  用单节点和小索引降低噪声；  不要一开始就读分布式协调代码。35.6 版本演进值得关注的版本主题：            版本线      主题                         5.x      类型弱化、索引性能与聚合改进                     6.x      移除多类型，默认单类型                     7.x      移除 mapping type，默认安全逐渐收紧                     8.x      安全默认、向量检索、Data Stream 成熟                     9.x      ES      QL、搜索与可观测性持续演进      跟进版本时要看：  Breaking changes；  REST API 兼容；  Java client 变化；  Mapping 变化；  分片和集群行为变化；  新查询能力；  许可和生态边界。生产升级不是只看新特性，还要验证客户端、插件、快照兼容和回滚路径。35.7 论文与资料推荐阅读：  Lucene 官方文档和 FORMATS.md；  Lucene FST 相关文档；  BM25 论文；  HyperLogLog 论文；  Elasticsearch 官方 Reference 和 Breaking Changes；  Elasticsearch GitHub issue 和 PR；  Elastic engineering blog；  OpenSearch 文档与源码；  分布式共识与 leader election 资料；  搜索排序和 Learning to Rank 资料。读资料的方法：  先官方文档，再源码；  先小实验，再论文细节；  每周固定时间沉淀笔记；  把结论和实验数据绑定；  输出内部分享，倒逼理解。35.8 实验环境建议长期维护三个环境：            环境      用途                  local      API 实验、Mapping 验证              staging      压测、升级演练、权限测试              prod      生产流量，只允许受控操作      必备工具：  Kibana Dev Tools；  Rally 压测；  Prometheus / Grafana；  curl / HTTP client；  索引模板版本库；  代码化运维脚本；  快照仓库。每个实验记录：假设：版本：数据规模：Mapping：查询：基线：变更：结果：结论：副作用：35.9 成长习惯成为专家的常见习惯：  每个 API 都知道失败场景；  每个参数都知道默认值和副作用；  每次事故写复盘；  每个集群有容量报告；  每个索引有负责人和保留策略；  每次变更有回滚方案；  每周阅读一个 issue 或源码片段；  每月做一次恢复演练；  每季度清理无用索引和权限；  每年重画一次系统架构图。技术深度来自重复验证，而不是收藏资料。35.10 大师级问题能回答这些问题，说明已经具备平台级视角：  集群当前最大可支撑写入和查询是多少？  下一次瓶颈会在哪个资源出现？  某个索引删除后业务如何恢复？  节点替换需要多久？  跨机房故障如何切换？  权限和审计是否满足合规？  搜索质量如何量化？  新团队接入需要哪些流程？  成本如何按业务拆分？  哪些能力应该下沉到平台，哪些留给业务？平台工程的目标是让业务安全自助，而不是所有问题都靠一个人手工处理。35.11 个人作品集建议沉淀四个作品：            作品      证明能力                  商品搜索项目      搜索工程和相关性              日志平台      数据管道和可观测性              集群治理手册      运维、安全和容量              源码解析系列      内核理解      作品集应包含：  需求和数据规模；  架构图；  Mapping 和索引模板；  核心代码；  压测报告；  监控截图；  故障案例；  优化前后对比。本章小结大师之路是持续迭代的能力体系：先做到工程可靠，再建立机制化治理，最后进入源码和生态贡献。无论选择搜索、可观测性还是数据平台方向，都要坚持“原理 + 实验 + 生产验证”的闭环。思考题  你现在处于 L1-L5 的哪一级？  未来三个月最重要的能力缺口是什么？  你能拿出哪个项目作为自己的代表作品？  哪个生产事故最值得整理成案例？  下一个源码模块打算读什么？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章整理 50 道 Elasticsearch 高频面试题。回答建议采用“结论 + 原理 + 边界 + 生产建议”的结构，不要只背名词。34.1 基础概念1. Elasticsearch 是什么？答：Elasticsearch 是基于 Lucene 的分布式搜索与分析引擎，提供文档模型、倒排索引、全文搜索、聚合分析、分片副本和 REST API。它适合搜索、日志检索和近实时分析，不适合作为强事务主存储。2. Index、Document、Field 是什么关系？答：Index 是文档集合和配置单元；Document 是一条 JSON 数据；Field 是文档中的字段。Mapping 定义字段类型和分析方式，决定字段如何被索引和聚合。3. ES 和 MySQL 有什么区别？答：MySQL 是事务型数据库，擅长强一致、关联和更新；ES 是搜索分析引擎，擅长全文检索、倒排索引、分布式聚合和近实时查询。生产中常见 MySQL 作为事实来源，ES 作为搜索投影。4. 为什么 ES 是近实时？答：写入先进入内存缓冲和 translog，refresh 后生成 Segment 才可搜索。默认刷新间隔约 1 秒，所以写入成功和立即可查不是同一个时点。5. 集群、节点、分片、副本是什么关系？答：集群由节点组成；索引拆成主分片；每个主分片可以有副本。文档通过 hash(routing) 定位主分片，主分片同步副本。6. green、yellow、red 分别代表什么？答：green 表示主副本都正常；yellow 表示主分片正常但至少一个副本未分配；red 表示至少一个主分片不可用。7. Lucene 和 ES 的分工是什么？答：Lucene 负责单机索引结构、搜索、评分、段和事务日志等底层能力；ES 在其上实现分布式分片、副本、路由、集群管理、REST API、安全和查询归并。8. 什么是倒排索引？答：倒排索引保存 term 到文档列表的映射，并可能带词频、位置和 norm。它让 term、match、phrase 等查询不需要顺序扫描所有文档。9. 什么是 Doc Values？答：Doc Values 是按文档到字段值的列式正排结构，主要用于排序、聚合和脚本访问。大多数聚合和排序字段需要启用 doc_values。10. _source 的作用是什么？答：_source 保存原始 JSON，用于返回结果、update、reindex、高亮和数据修复。关闭后可以省磁盘，但会失去更新和重建能力。34.2 Mapping 与分词11. keyword 和 text 怎么选？答：text 会分词，适合标题和描述；keyword 不分词，适合 ID、状态、标签、枚举和聚合字段。同一字段可同时配置 text 和 keyword multi-field。12. dynamic mapping 有什么风险？答：自动映射可能把 ID 映射成 text、日期映射错格式，或让高基数字段不断扩展 Mapping，增加 cluster state 和内存成本。生产建议 strict 或 false，并评审新字段。13. 什么是 analyzer？答：Analyzer 由 Character Filter、Tokenizer 和 Token Filter 组成，负责把文本变成词项。索引和搜索可以使用不同 analyzer。14. 中文搜索为什么不能只用 standard？答：standard 对中文偏单字切分，召回噪声大且短语语义弱。常用 IK 等分词器，并结合业务词库、同义词、停用词和自定义词典。15. 同义词放在索引阶段还是搜索阶段？答：索引阶段成本低但更新词表要重建索引；搜索阶段可即时更新但查询会扩展更多词项。业务常用搜索阶段，词表特别稳定时可用索引阶段。16. nested 和 object 有什么区别？答：object 会拍平字段，无法区分数组内对象边界；nested 为每个对象建立独立索引结构，能正确查询“颜色红色且尺码 XL”。nested 更精确但成本更高。17. index、doc_values、enabled、store 分别控制什么？答：index 控制是否能被搜索；doc_values 控制排序聚合正排；enabled 关闭整个对象解析；store 控制字段是否单独存储。默认通常依赖 _source，不必单独 store。18. 如何避免字段爆炸？答：使用 dynamic: strict/false、限制 labels 这类对象索引、字段上线评审、拆分业务数据集、用 metadata 管理字段、清理无用字段并重建索引。19. Mapping 能修改吗？答：已有字段类型通常不能直接修改，只能新建索引和 reindex。可以新增字段、修改 analyzer 相关设置有限，或使用别名切换。20. 如何设计搜索商品 Mapping？答：标题用 text 加 keyword；品牌/类目/状态用 keyword；价格用 scaled_float 或 double；属性用 nested；销量用于排序聚合保留 doc_values；向量字段按模型维度定义 dense_vector。34.3 查询 DSL21. query 和 filter 有什么区别？答：query 计算相关性评分；filter 只判断是否匹配，可缓存。状态、时间、价格、权限等条件应放 filter。22. match 和 term 的区别？答：match 会分析查询文本；term 不分析，适合 keyword 精确匹配。对 text 字段用 term 常常搜不到或结果异常。23. bool 查询各子句的作用？答：must 参与评分且必须匹配；filter 必须匹配但不评分；should 提供可选加分；must_not 排除且不评分。24. match_phrase 为什么比 match 贵？答：match_phrase 需要检查词项位置和顺序，依赖 positions 信息。适合精确短语或加权，不适合无限扩大召回。25. multi_match 的 best_fields 和 most_fields 区别？答：best_fields 取匹配最好的字段得分，适合标题优先；most_fields 把多个字段得分合并，适合多信号补充，但可能重复加权。26. 为什么深分页慢？答：每个分片必须取 from+size 条候选并维护排序队列，协调节点再归并。from 越大，内存和 CPU 越大。应使用 search after 或 PIT。27. search after 为什么能优化翻页？答：它使用上一页排序值作为游标，只取下一页数据，不需要保存跳过的前 N 条。要求排序稳定且不能直接跳页。28. PIT 是什么？答：Point In Time 保存一个索引数据视图，让多次 search after 使用一致快照，减少分页期间数据变化导致的重复或遗漏。29. _score 是怎么来的？答：默认主要是 BM25，根据词项 IDF、TF、文档长度、字段 boost 和查询组合方式计算。可用 explain 查看具体贡献。30. 如何排查某个文档排序不理想？答：先确认 query 和分词，再用 _explain 查看匹配字段、词频、IDF、boost 和 filter 影响，最后区分召回问题、权重问题或业务因子问题。34.4 写入与性能31. 写入链路是什么？答：协调节点路由，主分片校验并写入 Lucene 缓冲和 translog，再同步副本；refresh 使数据可搜，flush 提交 Lucene，merge 清理删除和合并 Segment。32. refresh、flush、translog 的区别？答：refresh 生成可搜索 Segment；flush 执行 Lucene commit 并处理 translog；translog 用于崩溃后重放已确认但未提交的变更。33. 为什么 update 成本高？答：Lucene Segment 不可变，更新需要读取旧 _source、标记旧文档删除、写入新文档，merge 时再物理清理，容易造成写入放大。34. Bulk 返回 200 是否代表全部成功？答：不代表。Bulk 响应里每个 item 可能有 400、429、版本冲突等错误，应用必须逐项检查并只重试失败项。35. 如何提高写入吞吐？答：批量写入、合理并发、放宽 refresh、评估 translog durability、减少热点 update、控制分片和 Mapping 复杂度、预留磁盘和页缓存，并让客户端支持退避。36. 写入 429 怎么处理？答：先看 write thread pool rejected、磁盘、merge、节点负载和恢复任务；客户端降低并发并指数退避；服务端治理根因，必要时扩容，而不是盲目加大队列。37. 为什么主分片数不能直接修改？答：文档路由依赖 hash(routing) % number_of_primary_shards，修改会导致旧文档无法按公式找到。需要新建索引并 reindex。38. 分片是不是越多越好？答：不是。分片多能并行但会增加查询扇出、内存句柄、merge 任务、cluster state 和恢复成本。应按单分片大小、查询模式和写入量设计。39. 什么是查询扇出？答：一次查询会被发送到涉及索引的每个分片。索引模式越宽、分片越多，协调节点归并和分片执行成本越高。40. 聚合为什么慢？答：常见原因是候选文档多、分片多、桶数量大、字段基数高、嵌套聚合深、cardinality/percentile 内存高、text 字段误聚合或缺少预聚合。34.5 架构与运维41. terms 聚合为什么可能不精确？答：每个分片只返回本地 top shard_size，再由协调节点归并，某些全局高频但本地不突出的词项可能被截断。提高 shard_size 只是提高准确性，不是完全保证。42. cardinality 是精确去重吗？答：不是。它基于 HyperLogLog++ 近似算法，precision_threshold 越高越准确但内存越大。财务和对账场景应使用精确计算或数仓。43. 什么是 global ordinals？答：分片内为唯一词项建立全局编号，加速 keyword terms 等聚合，但首次构建和 Segment 变化后重建有成本，高基数字段可能造成长尾。44. 磁盘水印是什么？答：low 阻止新分片分配，high 触发分片迁移，flood_stage 会设置索引 read-only 阻止写入，用于保护节点磁盘。处理顺序是释放空间、恢复分配、再解除 block。45. 副本能替代备份吗？答：不能。副本解决节点故障，但误删除、坏数据和逻辑损坏会同步到副本。备份需要 snapshot 并定期恢复演练。46. 如何安全修改 Mapping？答：创建新索引、验证 Mapping 和 Analyzer、双写或通过 reindex 迁移、建别名、灰度读、校验数据、切换别名，并保留回滚索引。47. 零停机索引变更怎么做？答：使用别名隔离读写，新索引建好后通过 _reindex 或双写追平数据，检查版本和文档数，原子切换别名，观察后保留旧索引用于回滚。48. 多租户如何隔离？答：可按索引、索引前缀或集群隔离，并配合租户 ID、DLS、权限、别名、监控和备份策略。大租户应有独立迁移和容量治理方案。49. ES 集群如何做高可用？答：至少 3 个 master-eligible 和多个 data 节点，合理副本和 awareness，快照异地备份，Kafka 缓冲写入，滚动升级控制 allocation，并定期故障演练。50. 如果让你设计商品搜索，怎么开始？答：先明确 SKU/SPU、搜索指标和排序目标；设计 Mapping 与分词器；实现多路召回和 filter；提供筛选聚合和业务排序；接入点击日志与评测集；最后做别名、监控、降线和容量治理。34.6 面试表达技巧回答架构题时建议主动补充：  场景和数据规模；  写入和查询峰值；  分片、副本和保留策略；  一致性和幂等方案；  性能瓶颈和监控指标；  故障时如何降级；  方案的取舍和成本。不要一开口就给参数。参数应来自业务目标和压测数据。本章小结面试中最重要的不是背出所有 API，而是能讲清机制、边界和生产取舍。每道题都可以从“是什么、为什么、什么时候不适合、生产怎么做”四个角度扩展。思考题  你最容易讲错的三道题是哪些？  哪些问题需要结合自己项目数据规模回答？  如何把一次线上故障整理成 3 分钟面试案例？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 很少单独存在。它通常与 MySQL、Kafka、Redis、对象存储、数据仓库、监控系统和向量模型服务共同工作。架构设计的关键不是把所有数据都放进 ES，而是明确每个系统的职责边界和数据流向。33.1 Elasticsearch 的定位Elasticsearch 适合：  全文搜索：商品、内容、工单、知识库；  日志与可观测性：日志、指标、链路检索；  近实时分析：看板、漏斗、多维聚合；  地理检索：附近门店、轨迹查询；  向量检索：语义搜索、推荐召回、RAG 检索。不适合：  强事务主存储；  复杂多表关联和财务对账；  高频点查缓存；  数据仓库级别复杂 SQL；  原始大对象存储；  强一致读写。常见分工：            系统      职责                  MySQL / PostgreSQL      事务事实来源              Kafka      变更事件流和缓冲              Redis      热点缓存和计数              Elasticsearch      搜索、检索、分析              对象存储      原始文件和快照              数仓      离线建模和精确报表              OLAP 数据库      复杂 join 与超宽表分析      33.2 搜索投影架构搜索系统常用 CQRS 思想：命令侧写入数据库，查询侧维护搜索投影。flowchart LR    A[业务服务] --&gt; B[(MySQL)]    A --&gt; C[Kafka]    C --&gt; D[投影服务]    D --&gt; E[Elasticsearch]    F[搜索 API] --&gt; E    F --&gt; B投影服务职责：  消费领域事件；  补齐搜索需要的冗余字段；  转换为搜索文档；  幂等写入 ES；  记录检查点；  失败进入重试或死信。好处：  数据库模型不被搜索需求污染；  ES 可随时重建；  写入峰值由 Kafka 缓冲；  搜索 Mapping 可以独立演进；  业务服务和搜索系统解耦。代价：  数据最终一致；  需要处理乱序和重复；  需要全量重建能力；  增加链路运维复杂度。33.3 与 MySQL 同步常见方案：            方案      延迟      复杂度      特点                  双写      低      中      事务一致难，失败补偿复杂              定时扫描      秒到分钟      低      压力可控，实时性弱              Binlog / CDC      毫秒到秒      中      推荐主流方案              领域事件      毫秒到秒      中      语义清晰              手动重建      小时级      低      用于全量初始化      推荐组合：初始化：全量扫描 + 版本号增量：Binlog CDC 或领域事件校验：定时抽样比对修复：按主键重放同步服务必须处理：  插入、更新、删除；  乱序事件；  重复事件；  Mapping 校验失败；  ES 写入 429；  检查点与数据一致性；  全量重建期间切换别名。33.4 与 Kafka 集成Kafka 在 ES 架构中承担：  日志削峰；  业务事件缓冲；  写入重放；  多消费者分发；  上游和下游解耦。flowchart LR    A[应用] --&gt; K[Kafka]    K --&gt; B[搜索投影]    K --&gt; C[日志写入]    K --&gt; D[流计算]    B --&gt; E[ES]    C --&gt; E    D --&gt; F[(数仓)]消费设计：            问题      方案                  峰值      消费限流 + 背压              失败      重试 topic + 死信              重复      文档 ID 幂等              乱序      事件版本号              断点      offset + checkpoint              监控      lag、处理耗时、死信数      如果 ES 故障 30 分钟，Kafka 可以继续接住上游数据；如果应用直写 ES，则通常只能失败、重试或本地堆积。33.5 与 Redis 集成Redis 与 ES 常见配合：            场景      Redis      ES                  热门搜索词      缓存计数      搜索词日志分析              库存或价格      高频热点计数      搜索展示与过滤              搜索结果      短期缓存      召回与排序              用户画像      高频读取      多维检索与分群              限流      API 计数      审计查询      不要把高频扣减、下单锁、交易一致性交给 ES。搜索服务可以读 Redis 展示实时价格，但交易仍以数据库为准。33.6 与数仓和 OLAPElasticsearch 和数据仓库的边界：            维度      Elasticsearch      数仓 / OLAP                  延迟      秒级      分钟到小时              Join      能力有限      强              全文搜索      强      弱              灵活聚合      强      强              财务精确性      需谨慎      通常更适合              历史建模      弱      强              明细保留      成本高      可控      典型链路：业务日志/事件  -&gt; Kafka  -&gt; ES: 近实时排障和看板  -&gt; 数仓: T+1 报表和建模如果业务同时要求近实时和财务精确，应双写两条链路，而不是强行用一条链路满足两种口径。33.7 多租户架构常见隔离方式：            方式      隔离      成本      适合                  单索引共享      弱      低      小客户              每租户索引      中      中      中等规模              索引前缀分组      中      中      团队/产品线              独立集群      强      高      大客户/合规              DLS 权限      安全边界      低      配合索引隔离      设计要点：  租户 ID 必须进入文档和别名；  权限使用 DLS 或索引权限；  监控按租户维度统计；  禁止通配所有租户索引；  大租户可单独迁移；  单租户数据倾斜要有隔离方案；  备份和保留策略符合合同要求。33.8 冷热分层日志和指标类架构：Hot：最近 0-2 天，高频写入查询Warm：2-15 天，低频排障Cold/Frozen：15-90 天，偶尔查询Delete：到期删除节点配置：node.roles: [data_hot]node.attr.rack: rack-a索引设置：PUT logs-app{  "settings": {    "index.routing.allocation.include._tier_preference": "data_hot"  }}分层可以降低成本，但会增加迁移和恢复负载。需要监控：  ILM 状态；  rollover 时间；  shrink 时间；  segment merge；  迁移带宽；  冷数据查询延迟。33.9 在线与离线分离如果同一个集群同时承担：  在线搜索 P99 &lt; 200ms；  日志看板任意 30 天聚合；  数据团队全量导出；  机器学习训练；  历史索引重建。稳定性很难保证。推荐拆分：在线集群：业务搜索，低延迟日志集群：观测数据，可接受延迟离线集群：全量扫描、建模、导出灾备集群：只读或低流量分离方式包括：  独立集群；  Kibana 空间与权限限制；  时间分区默认查询；  API 网关限流；  大任务进入任务队列低峰执行。33.10 RAG 检索架构Elasticsearch 8.x/9.x 常用于 RAG 中的检索层：flowchart LR    A[用户问题] --&gt; B[Embedding]    B --&gt; C[ES kNN]    A --&gt; D[关键词检索]    D --&gt; C    C --&gt; E[混合召回]    E --&gt; F[Rerank]    F --&gt; G[LLM]设计要点：  文档切块策略比模型更重要；  保存 chunk_id、doc_id、version；  混合 BM25 和向量召回；  使用 RRF 或 rerank 模型融合；  过滤权限和过期文档；  保存 trace 以便追溯答案来源；  评估召回率、答案质量和引用准确性。向量不是替代关键词，而是补充语义。系统报错码、专有名词、SKU 编号往往关键词检索更可靠。33.11 自建与云托管            维度      自建      云托管                  控制力      高      中              运维成本      高      低              弹性      需自行建设      较好              网络      自主规划      云内网              版本升级      自控      平台控制              成本模型      机器和人力      实例和存储              排障能力      需要团队      平台支持      适合自建：  有专职 SRE 团队；  有复杂网络和合规要求；  数据不能出特定环境；  规模大且成本敏感；  需要深度定制。适合托管：  团队小；  业务迭代优先；  缺少 7x24 值班；  云内生态已经成熟；  快速验证新场景。无论自建还是托管，快照、权限、容量和监控都不能完全外包给默认值。33.12 OpenSearch 与 ElasticsearchOpenSearch 从 Elasticsearch 7.10 分支演化，两者共享大量基础概念，但生态和版本能力逐渐分化。共同点：  文档模型；  Query DSL 基础；  Mapping 基础；  分片和副本；  Aggregation 基础；  Lucene 底层。差异点：            维度      Elasticsearch      OpenSearch                         商业归属      Elastic      OpenSearch 项目                     高级能力      ES      QL、托管生态等      项目能力持续演进              客户端      Java API Client 等      OpenSearch clients                     插件生态      Elastic 生态      OpenSearch 生态                     版本路线      独立演进      独立演进             迁移前要评估：  SQL/DSL 兼容性；  安全模块差异；  索引兼容和 reindex；  客户端 SDK；  快照格式；  监控告警；  团队技能和生态依赖。33.13 架构评审清单  数据事实来源在哪里？  ES 是否可重建？  RPO/RTO 是否明确？  同步链路是否幂等？  Mapping 和分片是否评审？  查询是否有时间范围和索引边界？  写入峰值是否有队列缓冲？  权限是否按租户或团队隔离？  快照和恢复是否演练？  监控告警是否覆盖上下游？  成本模型是否清楚？  是否存在把 ES 当主数据库的隐性依赖？本章小结好的 Elasticsearch 架构是“职责清晰的生态系统”：数据库负责事务，Kafka 负责事件流，Redis 负责热点，ES 负责检索和分析，数仓负责复杂建模。设计时应先明确事实来源、数据一致性和重建能力，再决定同步方式、分片、隔离和容量。思考题  为什么搜索系统常用 CQRS 或投影架构？  双写 MySQL 和 ES 有什么风险？  Kafka 在 ES 数据链路中解决哪些问题？  多租户应如何组合索引隔离和 DLS？  哪些分析应该迁移到数仓而不是继续放大 ES 集群？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。高可用不是“副本数设置为 2”这么简单。它包含节点冗余、分片分布、机架感知、快照备份、跨机房容灾、容量规划、滚动升级和故障演练。容量治理则回答：现在够不够、多久会不够、扩容后是否仍然安全。32.1 可用性目标先定义业务目标：            场景      RPO      RTO      建议架构                  商品搜索      分钟级同步      5-15 分钟      多节点、可重建索引              日志检索      可容忍队列缓冲      15-30 分钟      Kafka 削峰、多节点              订单分析      分钟级      30 分钟      主数据源可重建              安全审计      近零丢失      依合规要求      独立集群、快照、远程备份      关键概念：  RPO：最多丢多少数据；  RTO：最多多久恢复；  MTTR：平均恢复时间；  SLO：可用性目标；  error budget：允许的不可用预算。如果搜索索引可以从主数据源重建，架构会比把 ES 当最终数据库更从容。32.2 节点与分片冗余基本要求：  3 个 master-eligible 节点；  data 节点至少 3 个；  副本至少 1 个；  副本与主分片不在同一节点；  同一物理机、机架或可用区不承载同一分片多个副本；  磁盘保留足够余量。机架感知：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.awareness.attributes": "rack_id"  }}强制感知：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.awareness.attributes": "zone",    "cluster.routing.allocation.awareness.force.zone.values": "zone-a,zone-b"  }}节点配置属性：node.attr.zone: zone-a如果配置强制感知但节点只在单 zone，分片可能保持未分配。必须先确认拓扑，再调整 awareness。32.3 延迟分配节点短暂离开时，可以延迟副本分配，避免不必要恢复：PUT /_all/_settings{  "index.unassigned.node_left.delayed_timeout": "10m"}查看延迟分片：GET /_cluster/health关键字段：  delayed_unassigned_shards；  number_of_nodes；  unassigned_shards。适合网络抖动或计划内短暂重启。如果节点确定不会恢复，应手动触发分配或扩容，而不是无限等待。32.4 快照备份快照是应对误删除、逻辑损坏和机房级故障的基础。注册仓库：PUT /_snapshot/backup-repo{  "type": "s3",  "settings": {    "bucket": "es-backup",    "region": "cn-north-1",    "base_path": "prod",    "chunk_size": "1gb",    "max_restore_bytes_per_sec": "200mb",    "readonly": false  }}验证仓库：POST /_snapshot/backup-repo/_verify创建快照：PUT /_snapshot/backup-repo/snapshot-2026.08.25?wait_for_completion=false{  "indices": "products-v3,orders-v3,logs-*",  "include_global_state": false,  "partial": false}查看：GET /_snapshot/backup-repo/snapshot-2026.08.25GET /_snapshot/backup-repo/_current删除快照：DELETE /_snapshot/backup-repo/snapshot-2026.08.25治理要求：  每日快照，关键索引可更频繁；  快照仓库开启版本控制和只读副本；  备份异地保存；  定期恢复演练；  监控成功率和耗时；  明确保留策略；  快照桶权限独立。32.5 恢复查看可恢复快照：GET /_snapshot/backup-repo/snapshot-2026.08.25恢复索引：POST /_snapshot/backup-repo/snapshot-2026.08.25/_restore?wait_for_completion=false{  "indices": "orders-v3",  "rename_pattern": "(.+)",  "rename_replacement": "restored-$1",  "index_settings": {    "index.number_of_replicas": 1  }}查看恢复进度：GET /_recovery?humanGET /_cat/recovery/restored-orders-v3?v取消恢复：POST /orders-v3/_closePOST /_snapshot/backup-repo/snapshot-2026.08.25/_cancel实际取消方式与当前恢复任务相关，生产应在 Runbook 中写清版本适用的命令。恢复演练要验证：  恢复后的索引 Mapping 正确；  文档数和关键指标一致；  别名切换流程可执行；  应用能正常读写；  恢复时间满足 RTO。32.6 Searchable SnapshotSearchable Snapshot 允许把快照中的数据直接作为可搜索分片挂载，适合 cold/frozen 层。POST /_snapshot/backup-repo/snapshot-2026.08.25/_mount?storage=full_copy{  "index": "logs-app-2026.08.01",  "renamed_index": "mounted-logs-2026.08.01",  "index_settings": {    "index.number_of_replicas": 0  }}价值：  降低本地存储成本；  快照数据可查询；  适合冷日志和审计数据。限制：  查询延迟通常高于本地 hot 数据；  依赖对象存储性能；  不适合高频交互查询；  配置与版本能力相关；  权限和仓库治理仍然必要。32.7 跨机房与异地容灾常见模式：            模式      做法      适合                  主备集群      主集群快照到远端，备集群恢复      RPO/RTO 中等              双活写入      应用双写或消息广播      一致性复杂              远程查询      CCS 查询远端集群      联合查询              数据重建      从 MySQL/Kafka 重建索引      搜索和分析场景              云多区      同城多可用区部署      网络延迟低      同城多可用区通常优先于跨城双活。跨城集群会受网络延迟影响，写入和选主都可能变差。搜索类系统更常用：MySQL / Kafka 作为事实来源  -&gt; 同步服务  -&gt; ES 主集群  -&gt; 快照远端  -&gt; 灾备集群可重建灾备不只是数据恢复，还包括：  配置版本管理；  索引模板；  ILM；  用户和角色；  别名；  监控和告警；  DNS 或网关切换；  应用降级逻辑。32.8 容量规划容量估算至少考虑：原始数据量  x 副本数  x 索引结构放大系数  x 预留水位  + 峰值 merge 临时空间  + 恢复临时空间示例：原始数据 10TB副本数 1结构放大估算 1.2可用水位 70%所需磁盘 = 10 * 2 * 1.2 / 0.7 = 34.3TB这不是精确值，必须通过真实数据压测校准。还要评估：  写入 docs/s；  查询 QPS；  P95/P99 延迟；  CPU 使用率；  JVM heap；  页缓存命中率；  磁盘 IOPS 和吞吐；  网络流量；  分片数量和大小。32.9 扩缩容扩容步骤：  明确瓶颈：磁盘、CPU、页缓存、查询扇出；  新节点使用相同或兼容配置；  低峰加入集群；  控制恢复和 rebalance 速度；  观察分片均衡；  验证查询和写入延迟；  更新容量报告。限制迁移速度：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.cluster_concurrent_rebalance": "2",    "indices.recovery.max_bytes_per_sec": "100mb"  }}缩容必须先确认：  节点上没有唯一主分片；  数据可以迁移；-磁盘余量足够；  查询峰值不会马上到来；  有回滚路径。使用 allocation filter 驱逐分片：PUT /_cluster/settings{  "transient": {    "cluster.routing.allocation.exclude._name": "es-data-old-01"  }}迁移完成后移除节点，再清理过滤规则。32.10 滚动升级升级前：  阅读官方升级路径和 breaking changes；  备份配置和快照；  在测试环境演练；  确认插件和客户端兼容；  停止索引自动分配；  通知写入方准备重试。滚动流程：禁用新分片分配  -&gt; 关闭非关键索引(可选)  -&gt; 停止一个节点  -&gt; 升级该节点  -&gt; 启动并重新加入  -&gt; 等待集群稳定  -&gt; 重复  -&gt; 恢复分片分配  -&gt; 升级 Kibana / 客户端核心原则：  一次只处理一个节点；  不让 master quorum 同时下线；  跨大版本先做快照和兼容测试；  客户端与应用同步升级；  每一步都有健康检查和回退点。32.11 故障演练至少定期演练：            演练      验证                  单 data 节点宕机      副本可用、写入可恢复              master 节点宕机      选主时间、集群可用              磁盘水位      告警和写入保护              快照恢复      RTO、RPO、数据完整性              Kafka 积压      写入背压和恢复              慢查询      限流、取消、降级              网络分区      quorum 和可用性              误删索引      恢复流程和审批      演练要有观测指标：  集群变为 yellow/red 的时间；  告警触发时间；  写入失败量；  查询错误率；  恢复耗时；  业务侧降级是否生效。32.12 高可用检查清单  master quorum 至少 3 节点；  副本分布跨节点和故障域；  磁盘余量充足；  快照每日成功并异地保存；  快照恢复定期演练；  ILM 自动管理保留；  滚动升级方案可回退；  缩容前有分片迁移验证；  Kafka 或队列有缓冲能力；  应用有搜索降级；  监控告警和 Runbook 完整；  容量报告每周更新。本章小结高可用由冗余、隔离、备份、可恢复和可演练共同组成。副本解决节点级故障，awareness 解决故障域问题，快照解决逻辑损坏和机房级故障，容量治理避免系统在增长中被动触顶。能被演练验证过的方案，才是真正的高可用方案。思考题  副本和快照分别解决什么故障？  RPO 和 RTO 如何影响架构选择？  为什么同城多可用区常优于跨城双活？  扩容前如何确认应该加节点还是重建索引分片数？  滚动升级时为什么要控制 quorum 和 allocation？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 常常保存搜索数据、日志、用户行为和业务分析结果，其中日志字段可能包含手机号、IP、请求头、授权 token，甚至数据库连接串。安全不是上线后补一项开关，而是从网络、传输、认证、授权、审计和数据分级六个层面同时治理。31.1 安全边界一个生产集群至少要明确：谁能访问集群？  -&gt; TLS / VPN / 网络边界谁能调用 API？  -&gt; 认证能访问哪些索引？  -&gt; index privileges能看到哪些文档？  -&gt; document level security能看到哪些字段？  -&gt; field level security做过什么操作？  -&gt; audit logging数据保留多久？  -&gt; ILM / snapshot / compliance常见风险：            风险      后果                  集群裸奔公网      全量读写、删除索引              共享超级用户      无法定位误操作人              日志字段全量开放      泄露 token、手机号、IP              客户端明文传输      凭据和数据被窃听              任意 Dashboard 查询      一个大聚合拖垮集群              快照桶权限过宽      绕过 ES 权限读数据              API key 不轮换      长期凭据泄露      31.2 传输加密 TLS生产环境应启用：  HTTP layer TLS：客户端到节点；  Transport layer TLS：节点之间；  Kibana 到 Elasticsearch TLS；  采集器到 Elasticsearch TLS；  证书有效期和轮换机制。Elasticsearch 8.x 默认更强调安全，生产应使用受信任 CA 签发证书，而不是长期使用自动生成证书。elasticsearch.yml 示例：xpack.security.enabled: truexpack.security.transport.ssl.enabled: truexpack.security.transport.ssl.verification_mode: fullxpack.security.transport.ssl.keystore.path: certs/es01.p12xpack.security.transport.ssl.truststore.path: certs/truststore.p12xpack.security.http.ssl.enabled: truexpack.security.http.ssl.keystore.path: certs/http.p12xpack.security.http.ssl.truststore.path: certs/truststore.p12把密码放入 keystore：bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_passwordbin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_passwordbin/elasticsearch-keystore add xpack.security.http.ssl.keystore.secure_password验证：curl -vk https://localhost:9200不要关闭主机名或节点证书校验。verification_mode: none 只适合临时排障，且必须在工单中记录恢复时间。31.3 认证常见认证方式：            方式      适合      注意                  内置用户      运维和初始化      不用于应用共享              LDAP / Active Directory      企业统一账号      组映射要简洁              SAML / OIDC      Kibana 登录      区分用户和机器              API key      服务集成      scope、过期、轮换              服务账号      应用写入      最小索引权限      创建本地用户：POST /_security/user/search_app{  "password": "change-me",  "roles": ["products_writer"],  "full_name": "Search Application"}修改密码：PUT /_security/user/search_app/_password{  "password": "new-password"}生产应用更推荐使用配置中心托管的密码或 API key，并定期轮换。31.4 API KeyAPI Key 适合服务与服务之间的认证。POST /_security/api_key{  "name": "search-app-prod",  "expiration": "30d",  "role_descriptors": {    "products_writer": {      "cluster": ["monitor"],      "indices": [        {          "names": ["products-v3", "products-write"],          "privileges": ["create_index", "write", "read"]        }      ]    }  }}返回：{  "id": "api_key_id",  "encoded": "base64_api_key"}查询：GET /_security/api_key?name=search-app-prodPOST /_security/_query_api_key{  "query": {    "term": { "name": "search-app-prod" }  }}失效：DELETE /_security/api_key{  "id": "api_key_id"}治理要求：  设置过期时间；  限制 index 和 cluster 权限；  不使用超级用户给应用；  key 泄露立即 invalidate；  定期盘点；  环境之间不复用。31.5 角色与索引权限创建角色：POST /_security/role/products_writer{  "cluster": ["monitor"],  "indices": [    {      "names": ["products-v3"],      "privileges": ["write", "create_index", "view_index_metadata"]    }  ]}只读角色：POST /_security/role/products_reader{  "cluster": ["monitor"],  "indices": [    {      "names": ["products-read"],      "privileges": ["read"]    }  ]}常用权限：            权限      说明                  read      搜索和读取文档              write      写入文档              create_index      创建索引              delete_index      删除索引              manage      管理索引              view_index_metadata      查看 Mapping、settings              monitor      查看监控              manage_index_templates      管理模板      应用账号不应拥有：  all；  manage；  delete_index；  manage_index_templates；-集群级变更权限。31.6 文档级安全Document Level Security 允许角色只看到满足查询条件的文档。POST /_security/role/order_team_reader{  "indices": [    {      "names": ["orders-v3"],      "privileges": ["read"],      "query": {        "term": { "team_id": "team_order" }      }    }  ]}多租户示例："query": {  "term": { "tenant_id": "tenant_1001" }}注意：  DLS 是安全边界，不要在业务代码里用 tenant_id filter 替代；  条件要简单、可索引；  权限变更要审计；  不要在高 QPS 角色上拼复杂脚本；  多租户更适合索引隔离加 DLS 双层防护。31.7 字段级安全Field Level Security 控制角色可见字段。POST /_security/role/logs_reader{  "indices": [    {      "names": ["logs-app-*"],      "privileges": ["read"],      "field_security": {        "grant": [          "@timestamp",          "service.name",          "log.level",          "message",          "trace.id"        ],        "except": []      }    }  ]}也可以默认允许，排除敏感字段："field_security": {  "grant": ["*"],  "except": ["authorization", "token", "request.headers"]}建议：  用户 ID、手机号、IP 默认不可见；  异常堆栈按团队授权；  authorization、cookie、token 不落 ES；  生产日志查询有审计；  安全团队和研发使用不同角色。31.8 Kibana 权限Kibana 权限应按空间和功能收敛：            角色      权限                  普通开发      指定空间的 read，能看到本团队索引              值班工程师      Discover、Dashboard、告警查看              SRE      集群监控、索引管理、有限写权限              数据分析      只读聚合空间              平台管理员      权限管理，但操作有审计      危险操作要分离：  删除索引；  修改 ILM；  修改 index template；  执行 reindex；  修改用户和角色；  管理快照仓库。Kibana Dashboard 也可能因为无时间范围查询导致集群压力，应通过空间、索引权限和默认查询约束共同治理。31.9 审计日志审计日志可以记录：  请求类型；  来源用户；  来源 IP；  索引；  成功或拒绝；  响应状态；  操作时间。启用审计需要配置安全订阅和静态设置，具体配置随版本变化。原则：  删除索引必须可追溯；  权限变更必须可追溯；  敏感数据查询必须可追溯；  审计日志独立保存；  普通业务角色不能删除审计日志；  审计日志也要设置保留周期。即使不启用完整审计，也应至少保留：  管理操作工单；  索引删除审批；  角色变更记录；  快照仓库修改记录；  生产 API key 发放记录。31.10 网络与访问控制建议网络分层：Internet  -&gt; WAF / VPN / Bastion     -&gt; Application        -&gt; Elasticsearch private subnet           -&gt; Data nodes要求：  ES 9200 不暴露公网；  Kibana 走统一登录和访问控制；  采集器只访问写入端点和指定索引；  跨集群访问使用 remote cluster 安全配置；  云安全组按来源收敛；  快照存储桶权限单独控制；  监控 exporter 不能暴露敏感数据。31.11 数据分级与脱敏            级别      示例      策略                  公开      商品标题      常规权限              内部      服务日志      团队权限              敏感      用户 ID、IP      字段授权、脱敏              高敏      手机号、身份证、token      默认不落 ES      在 Ingest Pipeline 中兜底脱敏：PUT _ingest/pipeline/sensitive-redact{  "processors": [    {      "script": {        "lang": "painless",        "source": """          if (ctx.message != null) {            ctx.message = ctx.message              .replaceAll(/\\b\\d{17}[0-9Xx]\\b/, '&lt;id_card&gt;')              .replaceAll(/\\b1[3-9]\\d{9}\\b/, '&lt;mobile&gt;');          }        """      }    }  ]}脱敏应在应用输出和采集端优先完成，ES Pipeline 只作为最后防线。31.12 安全基线检查  xpack.security.enabled: true；  HTTP 和 Transport 均启用 TLS；  证书校验不是 none；  无匿名访问；  应用不使用 elastic 超级用户；  API key 有过期和轮换；  角色 minimal privileges；  删除索引有审批；  敏感日志有 DLS / FLS；  审计日志独立保存；  快照仓库权限收敛；  集群和 Kibana 不暴露公网；  定期盘点用户、角色和 key；  定期做权限演练和证书过期监控。本章小结安全体系的核心是最小权限和可追溯。网络边界决定能不能进来，TLS 决定传输是否安全，认证和角色决定能做什么，DLS/FLS 决定能看哪些数据，审计决定事后能不能追责。上线前先做安全基线检查，比事故后补救便宜得多。思考题  应用账号为什么不能使用超级用户？  DLS 和业务代码里的租户过滤有什么本质区别？  API Key 应如何设计过期和轮换？  哪些日志字段不应进入 Elasticsearch？  删除索引这类高危操作应如何治理？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章按故障现象组织，给出确认命令、常见原因、处理步骤和预防动作。事故处理时先止血，再定位，最后复盘；不要在未确认影响面的情况下做大规模重建或重启。30.1 通用排查流程确认影响面  -&gt; 保存现场  -&gt; 判断集群/节点/索引/请求/链路  -&gt; 执行止血动作  -&gt; 根因定位  -&gt; 验证恢复  -&gt; 复盘改进第一时间执行的命令：GET /_cluster/health?timeout=5sGET /_cat/nodes?v&amp;h=name,node.role,master,heap.percent,cpu,disk.used_percentGET /_cat/indices?v&amp;health=redGET /_cat/shards?v&amp;h=index,shard,prirep,state,store,node,unassigned.reasonGET /_cluster/allocation/explainGET /_cat/thread_pool/write,search?v&amp;h=node_name,name,active,queue,rejected保存现场：GET /_cluster/state?filter_path=metadata,routing_tableGET /_nodes/stats?humanGET /_nodes/hot_threadsGET /_cat/pending_tasks?v30.2 集群 Yellow现象：主分片正常，部分副本未分配。常见原因：  新增索引后节点资源不足；  节点离线；  磁盘 low/high watermark；  副本数 &gt;= 数据节点数；  allocation 被禁用；  索引分配过滤规则冲突；  节点角色不匹配。排查：GET /_cluster/health?level=indicesGET /_cluster/allocation/explainGET /_cat/allocation?vGET /_cluster/settings?flat_settings=true处理：  找到具体索引和分片；  阅读 allocation explain；  如果节点数不足，恢复节点或降低副本数；  如果磁盘水位高，清理或扩容；  如果 allocation 禁用，确认维护窗口后恢复；  如果过滤规则错误，修正 tier 或 attribute。临时降低副本必须经过明确审批：PUT logs-app/_settings{  "number_of_replicas": 0}30.3 集群 Red现象：至少一个主分片不可用。常见原因：  持有主分片的节点离线；  分片分配失败；  磁盘 flood stage；  损坏的分片数据；  创建索引时节点资源不足；  滚动重启时过早移除节点；  网络分区导致节点离开。排查：GET /_cat/indices?v&amp;health=redGET /_cat/shards?v&amp;h=index,shard,prirep,state,store,node,unassigned.reasonGET /_cluster/allocation/explain处理顺序：  确认是否所有节点在线；  恢复离线节点；  查看是否能从副本提升主分片；  查看磁盘水位；  查看 allocation enable；  必要时从快照恢复索引；  确认数据缺失影响面。红线：  不要直接删除索引；  不要随便丢弃 stale 主分片；  不要在没有快照验证时重建；  不要同时重启多个数据节点；  不要忽视 red 只处理 yellow。30.4 Unassigned Shard定位原因：GET /_cluster/allocation/explain输出中的 allocate_explanation、node_allocation_deciders 通常会说明被哪个决策器阻止。常见 decision：            原因      处理                  disk watermark      清理或扩容              same shard conflict      增加数据节点              allocation disabled      恢复 allocation              tier mismatch      调整 tier preference 或节点属性              no node      恢复节点              shard failure      查看节点日志和损坏原因      重试分配：POST /_cluster/reroute?retry_failed=true这个命令只能重试已失败决策，不能解决磁盘满或节点不足等根因。30.5 磁盘满确认：GET /_cat/allocation?vGET /_nodes/stats/fs?humanGET /_cat/indices?v&amp;h=index,store.size,pri.store.size处理顺序：  暂停非必要导入和离线查询；  删除确认过期的索引；  缩短 ILM 保留期；  降低日志副本；  扩容磁盘或节点；  触发 reroute；  解除 read-only block。查看 blocked 索引：GET /_all/_settings?filter_path=*.settings.index.blocks.read_only_allow_delete解除：PUT /_all/_settings{  "index.blocks.read_only_allow_delete": null}只有先释放空间再解除，否则很快再次触发。30.6 写入 429现象：EsRejectedExecutionException: rejected execution on write thread pool排查：GET /_cat/thread_pool/write?v&amp;h=node_name,name,active,queue,rejected,completedGET /_nodes/stats/indices/indexing,merge,refresh?humanGET /_cat/allocation?v常见原因：  客户端并发过高；  bulk 过小；  merge 太重；  磁盘慢或接近水位；  副本恢复占用 IO；  Mapping 过于复杂；  写入与查询争抢资源；  分片分布热点。止血：  写入端限流；  指数退避；  暂停低优先级任务；  延长 refresh interval；  排查磁盘和节点热点。客户端重试示例：long delay = 500L;for (int attempt = 0; attempt &lt; 5; attempt++) {    BulkResponse response = client.bulk(request);    if (!response.hasFailures()) {        return;    }    // 记录并只重试可重试 item，同时遵守退避    Thread.sleep(delay);    delay *= 2;}30.7 写入延迟高排查路径：            检查项      命令                  写入统计      GET index/_stats/indexing              合并压力      GET index/_stats/merges              刷新耗时      GET index/_stats/refresh              flush 耗时      GET index/_stats/flush              线程池      _cat/thread_pool/write              磁盘      _nodes/stats/fs              分片大小      _cat/shards      常见原因：  单条写入；  bulk 太小或太大；  refresh 频繁；  update 热点；  副本慢；  translog durability 太强但磁盘慢；  segment 太多；  Mapping 嵌套复杂。优先调整：  批量；  refresh interval；  写入并发；  热点更新；  分片和磁盘容量。30.8 查询慢排查：GET /_nodes/stats/indices/search,thread_pool?humanGET /_cat/thread_pool/search?vGET products-v3/_stats/search?human常见原因：            原因      特征                  查询扇出大      涉及分片多，CPU 全节点升高              时间范围宽      日志类最常见              深分页      from+size 大              高基数聚合      heap 和 CPU 高              wildcard      build_scorer 高              script      collect/score 高              fetch 大      fetch phase 高              页缓存不足      磁盘 read 高              分片热点      个别节点负载高      止血：  取消当前搜索任务；  限制 Kibana 或分析端访问；  降低查询 QPS；  关闭明显错误的大查询；  必要时让索引 read-only 或隔离流量。查看任务：GET /_tasks?actions=*search*&amp;detailedPOST /_tasks/TASK_ID/_cancel30.9 JVM 高与熔断确认：GET /_nodes/stats/jvm,breaker?humanGET /_nodes/hot_threads常见原因：  聚合桶过多；  深分页；  大 size；  大 scroll/PIT；  fielddata；  大 bulk 请求；  segment 内存高；  查询响应 JSON 过大。止血：  取消大查询；  限制聚合和分页；  关闭相关 Dashboard；  必要时滚动重启前先切流；  确认恢复后再放流量。不要只根据 heap 使用率重启。先看 GC 是否能回收、 breaker 类型、当前任务和 hot threads。30.10 Master 不稳定现象：  master 频繁切换；  cluster state publish timeout；  pending tasks 堆积；  创建索引长时间无响应；  节点反复 join/leave。排查：GET /_cluster/health?explain=trueGET /_cat/pending_tasks?vGET /_nodes/hot_threadsGET /_cat/nodes?v&amp;h=name,node.role,master,cpu,load_average,heap.percent常见原因：  master 节点资源不足；  master 与 data 混部被重查询拖慢；  cluster state 过大；  索引和分片数量过多；  网络抖动；  GC 长暂停；  3 节点集群同时维护两个节点。处理：  恢复 master-eligible 节点数量；  降低 cluster state 规模；  清理无用索引和模板；  分离 master 节点；  排查网络和 GC；  控制索引创建频率。30.11 副本恢复慢查看：GET /_cat/recovery?v&amp;active_only=trueGET /_cat/shards?v&amp;h=index,shard,prirep,state,store,node常见限制：  recovery 带宽；  磁盘 IO；  节点 CPU；  分片大小；  translog 重放；  并发恢复数。调整恢复速度要谨慎：PUT /_cluster/settings{  "persistent": {    "indices.recovery.max_bytes_per_sec": "100mb"  }}恢复期间业务影响：  写入延迟上升；  查询可能路由到正在恢复的节点；  磁盘和网络被占用；  merge 变慢。30.12 数据不一致或丢失先确认问题类型：            现象      可能原因                  查不到最新数据      refresh 未发生              主库有但 ES 没有      同步链路断              ES 有重复数据      幂等缺失              字段缺失      Mapping/dynamic 策略              数值统计不对      指标口径或近似聚合              索引被删      权限或误操作              节点数据损坏      磁盘故障      排查顺序：  按业务 ID 点查；  检查 _source 与主库差异；  查看同步任务 offset；  查看 Kafka lag 和死信；  检查 bulk item 错误；  检查 Mapping 和 pipeline；  检查最近变更和审计日志；  必要时按 ID 重放。不要把 ES 当主数据源。恢复顺序应以数据库、消息系统或快照为准。30.13 滚动重启滚动重启前：  确认集群 green 或明确当前风险；  暂停索引分配；  确认快照可恢复；  记录分片分布；  通知写入方准备重试；  一次只重启一个节点。暂停分配：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.enable": "primaries"  }}节点恢复并加入后，再恢复：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.enable": null  }}如果使用 none，必须确保维护过程没有索引创建或节点二次故障。30.14 事故复盘模板事故时间：发现时间：恢复时间：影响范围：业务指标：时间线：根因： contributing factors：止血动作：长期改进：责任人：截止时间：验证方式：复盘重点不是处罚，而是补齐四件事：监控、自动化、权限和演练。本章小结故障排查要先判断影响面和资源层级，再使用 health、allocation explain、stats、thread pool、tasks 和 hot threads 定位。集群问题时看分片与水位，写入问题时看 bulk、merge、translog 和磁盘，查询问题时看扇出、分页、聚合、缓存和 fetch。思考题  yellow 和 red 的第一步排查有什么不同？  磁盘 flood stage 解锁前必须先做什么？  为什么 429 出现时不建议先加大线程池队列？  如何区分“数据没写入”和“数据写入但不可见”？  滚动重启时为什么要控制 allocation？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。监控的目标不是画很多图，而是在问题影响业务前发现它，并在事故中快速回答四个问题：影响面多大、瓶颈在哪、先做什么、什么时候恢复。本章建立 Elasticsearch 的监控体系：集群、节点、索引、查询、写入、JVM、磁盘和告警 Runbook。29.1 监控分层            层级      关注问题      常用 API                  集群      健康、节点、分片、master      _cluster/health              节点      CPU、内存、磁盘、线程池      _nodes/stats              索引      写入、查询、段、合并      _stats              分片      大小、文档数、未分配      _cat/shards              请求      延迟、失败、慢查询      slowlog、APM              采集链路      lag、失败、重试      Kafka、Filebeat、OTel              业务      无结果率、搜索成功率      应用日志      常用快照：GET /_cluster/health?prettyGET /_cat/nodes?vGET /_cat/indices?v&amp;health=yellowGET /_cat/allocation?vGET /_cat/thread_pool/write,search?v&amp;h=node_name,name,active,queue,rejected生产监控建议通过 Prometheus 采集，而不是人肉执行 Cat API。29.2 集群健康GET /_cluster/health?timeout=5s关键字段：            字段      含义                  status      green/yellow/red              number_of_nodes      当前节点数              number_of_data_nodes      数据节点数              active_primary_shards      主分片数              active_shards      主副本总数              unassigned_shards      未分配分片              initializing_shards      恢复中分片              relocating_shards      迁移中分片              delayed_unassigned_shards      延迟分配分片              pending_tasks      master 队列              max_task_wait_time      最长任务等待      告警示例：            条件      级别                  status=red 持续 1 分钟      P0/P1              status=yellow 持续 10 分钟      P2              number_of_nodes 低于期望      P1              pending_tasks &gt; 50 持续 5 分钟      P2              unassigned_shards 增加      P2              master 变更      P1      29.3 节点指标查看节点统计：GET /_nodes/stats/jvm,os,fs,indices,thread_pool?human29.3.1 CPU 与负载关注：  CPU percent；  load average；  IO wait；  进程 CPU 与系统 CPU；  hot threads。CPU 高的常见原因：  查询扇出过大；  高基数聚合；  分词和脚本；  segment merge；  recovery；  节点分片热点。29.3.2 JVM关注：  heap used percent；  old GC 频率和耗时；  young GC；  breakers 触发；  pool 使用趋势。GET /_nodes/stats/jvm?human告警：            条件      级别                  heap used &gt; 85% 持续 5 分钟      warning              old GC 每分钟超过 1 次持续 5 分钟      warning              old GC 单次超过 5 秒      critical              breaker tripped 增加      critical      29.3.3 磁盘GET /_cat/allocation?vGET /_nodes/stats/fs?human关注：  disk used percent；  可用空间绝对值；  IO read/write；  IO latency；  inode 使用率；  磁盘增长速率。告警：            条件      级别                  使用率 &gt; 75%      warning              使用率 &gt; 82%      critical              按当前增速 7 天内触发 low watermark      capacity              IO util &gt; 90% 持续 5 分钟      warning      提前告警比等到 flood stage 更重要。29.4 索引指标GET /_stats/indexing,search,merge,refresh,flush,segments?human29.4.1 写入            指标      含义                  indexing.index_total      写入文档数              indexing.index_time      写入耗时              indexing.index_current      当前写入              indexing.index_failed      写入失败              indexing.is_throttled      是否限流              indexing.throttle_time      限流时间      延迟计算：avg index latency = index_time_in_millis / index_total29.4.2 查询            指标      含义                  search.query_total      query phase 次数              search.query_time      query phase 总耗时              search.query_current      当前查询数              search.fetch_total      fetch phase 次数              search.fetch_time      fetch 总耗时              search.scroll_current      当前 scroll              search.suggest_current      当前 suggestion      29.4.3 合并与刷新关注：  merge.current；  merge.total；  merge.total_time；  merge.total_throttled_time；  refresh.total_time；  flush.total_time。如果 merge throttled time 很高，说明磁盘压力或写入放大已经明显。29.4.4 SegmentGET /_stats/segments?human关注：  count；  memory_in_bytes；  terms_memory_in_bytes；  doc_values_memory_in_bytes；  stored_fields_memory_in_bytes；  index_writer_memory_in_bytes。Segment 数量异常增长通常与 refresh 过频、写入毛刺或 merge 跟不上有关。29.5 慢查询日志设置慢查询阈值：PUT products-v3/_settings{  "index.search.slowlog.threshold.query.warn": "3s",  "index.search.slowlog.threshold.query.info": "1s",  "index.search.slowlog.threshold.query.debug": "500ms",  "index.search.slowlog.threshold.fetch.warn": "1s",  "index.search.slowlog.threshold.fetch.info": "500ms",  "index.indexing.slowlog.threshold.index.warn": "1s",  "index.indexing.slowlog.threshold.index.info": "500ms"}慢日志应接入日志平台，并至少提取：  索引；  来源 IP 或用户；  查询语句；  总命中；  耗时；  分片数；  客户端 trace id。注意 info/debug 级别会产生大量日志，生产环境要控制保留时间和磁盘。29.6 采集链路监控日志系统还要监控 ES 之前的链路：            组件      指标                  应用      输出速率、丢弃数、阻塞队列              Filebeat/OTel      registry backlog、发送失败              Kafka      lag、分区倾斜、重平衡              消费器      批次耗时、死信数、offset              Pipeline      处理耗时、失败数      如果 Kafka lag 增长但 ES 写入指标下降，问题可能在消费器；如果 ES write rejected 上升，问题可能在集群侧；如果应用无日志，问题可能在应用输出或采集器。29.7 Prometheus 指标常用 exporter 会把 Cat API 和 stats API 转为 Prometheus 指标。核心 PromQL 示例：# cluster statuselasticsearch_cluster_health_status{cluster="prod"}# node heapelasticsearch_jvm_memory_used_bytes{area="heap"} / elasticsearch_jvm_memory_max_bytes{area="heap"}# write rejectedrate(elasticsearch_thread_pool_rejected_count{name="write"}[5m])# search latencyrate(elasticsearch_indices_search_query_time_seconds[5m]) / rate(elasticsearch_indices_search_query_total[5m])# disk usageelasticsearch_filesystem_data_available_bytes / elasticsearch_filesystem_data_size_bytesGrafana 建议看板：  Cluster Overview：健康、节点数、分片、pending tasks；  Node Health：CPU、heap、GC、磁盘、网络；  Index Throughput：写入 QPS、失败、429；  Search Performance：P95/P99、超时、取消；  Merge &amp; Recovery：merge、refresh、flush、recovery；  Pipeline：Kafka lag、采集失败、死信；  Capacity：磁盘增长、分片大小、节点均衡。29.8 告警设计告警要满足：  有明确负责人；  有可执行动作；  有严重级别；  有抑制和静默机制；  能关联到业务影响；  不重复轰炸。            指标      阈值建议      动作                  cluster red      立即      P1，定位分片              node offline      立即      P1，确认机器              disk &gt; 82%      5 分钟      清理或扩容              write rejected 增长      3 分钟      限流、排查              search rejected 增长      3 分钟      查询治理              heap &gt; 85%      5 分钟      排查大查询              breaker 触发      立即      P1              Kafka lag 持续增长      10 分钟      排查消费              快照失败      1 次      检查仓库              master 频繁切换      1 次      P1      29.9 Runbook 模板每类告警应有标准 Runbook：告警名称：严重级别：业务影响：确认命令：常见原因：止血动作：升级路径：恢复标准：事后动作：示例：write rejected。确认：GET /_cat/thread_pool/write?vGET /_nodes/stats/indices/indexing,merge?human止血：1. 写入端降低并发 50%2. 开启指数退避3. 暂停低优先级导入排查：1. 查看 rejected 所在节点2. 查看磁盘水位3. 查看 merge throttled4. 查看 bulk item 错误5. 查看是否有节点离线恢复标准：rejected 不再增长，写入 P99 回到基线29.10 容量预测容量监控不只看当前使用率，还要看趋势：剩余天数 = 可用容量 / 最近 7 天日均增长需要记录：  每日新增文档数；  每日新增磁盘量；  副本系数；  索引保留期；  快照增长率；  查询峰值 QPS；  写入峰值 docs/s；  分片平均大小。建议每周发布容量报告：            项目      当前      7 天增长      预计触达水位                  磁盘      62%      +1.2%      19 天              JVM P95      48%      平稳      无              写入峰值      8k/s      +5%      30 天              查询 P99      800ms      +10%      需优化      29.11 监控清单  集群健康和 master 稳定性；  节点在线数和角色；  JVM heap 和 GC；  磁盘使用率和增长趋势；  write/search rejected；  query/fetch 延迟；  慢查询日志；  merge、refresh、flush；  unassigned shard 原因；  recovery 进度；  快照成功率；  采集链路 lag 和死信；  业务侧无结果率和错误率；  每类 P0/P1 告警都有 Runbook。本章小结Elasticsearch 监控要同时覆盖集群资源、请求执行、分片状态和上游链路。真正有效的告警不是“CPU 高”，而是能说明影响面、根因方向和处理动作。容量趋势、快照、恢复和慢查询是生产系统最容易被忽视的部分。思考题  yellow 持续 10 分钟和 red 持续 1 分钟，告警级别如何设计？  为什么 JVM 告警不能只看瞬时 heap used？  write rejected 上升时，应先检查哪些指标？  如何计算磁盘预计触达水位时间？  慢查询日志应该采集哪些上下文字段？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。性能调优不是收集一堆“最佳参数”，而是先定位瓶颈，再在容量、延迟、成本和可靠性之间做取舍。Elasticsearch 的性能通常由五个资源共同决定：CPU、JVM 堆、操作系统页缓存、磁盘 IO 和网络。本章按写入、查询、聚合、Mapping、JVM 和硬件几个层面梳理调优方法。28.1 性能调优流程一次完整的调优应遵循：定义目标  -&gt; 建立基线  -&gt; 观察资源指标  -&gt; 定位瓶颈层  -&gt; 修改一个变量  -&gt; 压测对比  -&gt; 评估副作用  -&gt; 记录参数和结论目标必须具体：            模糊目标      可验证目标                  写入快一点      10000 docs/s，P99 写入 &lt; 200ms              查询别慢      P95 &lt; 300ms，P99 &lt; 1s              聚合别卡      30 天报表 &lt; 3s              集群稳定      7 天无 429、无熔断、无节点离线      没有基线的调优，很容易只是把噪声当收益。28.2 写入调优28.2.1 批量写入单文档请求固定成本高。应使用 Bulk：POST _bulk{"index": {"_index": "logs-app", "_id": "1"}}{"message": "request ok", "log.level": "info"}{"index": {"_index": "logs-app", "_id": "2"}}{"message": "request failed", "log.level": "error"}建议：  批次大小从 1MB 到 10MB 压测；  控制并发，而不是无限开线程；  部分失败时只重试失败项；  429 时指数退避；  保持请求顺序，方便定位 item 错误。28.2.2 Refresh默认 1 秒刷新一次。高吞吐日志可放宽：PUT logs-app/_settings{  "index.refresh_interval": "30s"}收益：  减少 Segment 数量；  降低 refresh CPU；  降低 merge 压力；  提高写入吞吐。代价：  数据可搜索延迟增加；  监控告警数据变旧；  用户能看到的数据滞后。28.2.3 副本与导入大批量初始化导入时可以临时：PUT import-v1/_settings{  "number_of_replicas": 0,  "refresh_interval": "-1"}导入完成后恢复：PUT import-v1/_settings{  "number_of_replicas": 1,  "refresh_interval": "1s"}必须确保恢复动作自动化。否则一旦节点故障，索引可能只有一个主分片副本，风险极高。28.2.4 Translog可靠性优先：PUT orders-v3/_settings{  "index.translog.durability": "request"}吞吐优先且可接受少量丢失窗口：PUT logs-app/_settings{  "index.translog.durability": "async",  "index.translog.sync_interval": "5s"}订单、支付、审计不要为了性能直接改为 async，除非业务明确接受一致性代价。28.2.5 写入放大高频 update、频繁 refresh、大量分片和过度 force merge 都会放大写入。治理：  能用全量替换就不用 update；  热点计数先在 Redis/Kafka 聚合，再低频写 ES；  控制嵌套对象数量；  降低写入端并发毛刺；  给 merge 留出 CPU 和 IO；  使用 rollover 控制单分片大小；  低峰执行 force merge。28.3 查询调优28.3.1 缩小数据范围第一优先级永远是缩小范围：GET logs-app/_search{  "size": 20,  "query": {    "bool": {      "filter": [        { "term": { "service.name": "order-service" } },        { "range": { "@timestamp": { "gte": "now-15m", "lte": "now" } } }      ]    }  },  "sort": [{ "@timestamp": "desc" }]}避免：GET logs-*/_search{  "size": 10000}索引名通配符越宽，查询扇出越大。28.3.2 Filter 优先不需要评分的条件放 filter："filter": [  { "term": { "status": "on_sale" } },  { "range": { "price": { "gte": 100, "lte": 500 } } }]filter 可以利用 node query cache，语义也更清晰。28.3.3 控制 size 和深分页  默认 size 用 10 或 20；  列表页不超过 100；  深翻页用 search after；  导出用 PIT + search after；  不提高 max_result_window 解决分页问题。28.3.4 Source Filtering只返回必要字段：GET products-v3/_search{  "_source": ["sku_id", "title", "price", "image"],  "query": { "match_all": {} }}大字段返回会放大 fetch、网络和 JSON 序列化成本。28.3.5 避免昂贵查询常见昂贵模式：            查询      问题      替代                  wildcard: "*手机"      无法高效用词典      ngram、search_as_you_type、前缀查询              大范围 script      每文档执行      预计算字段              text 上高基数聚合      fielddata / 错误建模      keyword              match_all + 大 size      全量扫描      按时间/状态过滤              深分页      内存放大      search after              多层嵌套聚合      桶爆炸      预聚合              前缀通配符任意组合      查询树巨大      受控搜索词表      28.3.6 Profile用 Profile 定位：GET products-v3/_search{  "profile": true,  "query": {    "bool": {      "must": [{ "match": { "title": "笔记本" } }],      "filter": [{ "term": { "status": "on_sale" } }]    }  }}重点看 build_scorer、next_doc、score、aggregation 和 fetch。如果时间主要在 build_scorer，可能是查询树或 high frequency term 问题；如果在 next_doc，可能是候选集过大。28.4 聚合调优核心是控制三个规模：候选文档数 x 桶数量 x 每桶工作建议：  size: 0；  限制时间范围；  控制嵌套聚合；  terms 使用低中基数字段；  控制 cardinality precision；  大报表预聚合；  composite 用于导出；  慢查询日志和熔断指标监控。查看聚合缓存：GET orders-v3/_stats/request_cache,query_cache,fielddata?human时间条件尽量取整："range": {  "@timestamp": {    "gte": "now-1d/d",    "lte": "now/d"  }}这样更容易命中 request cache。28.5 Mapping 调优28.5.1 字段只保留需要的索引结构PUT events-v1{  "mappings": {    "dynamic": false,    "properties": {      "event_type": {        "type": "keyword",        "doc_values": true,        "index": true      },      "trace_id": {        "type": "keyword",        "index": true,        "doc_values": false      },      "message": {        "type": "text",        "index": true      },      "raw_body": {        "type": "keyword",        "index": false,        "doc_values": false      }    }  }}说明：  trace_id 只查不聚合，可关闭 doc_values；  raw_body 只返回不查不聚合，可关闭索引和 doc_values；  message 全文检索需要 text；  不要默认让所有字段都拥有倒排、doc_values 和 multi-field。28.5.2 控制 dynamic mapping生产模板建议：  日志类：dynamic: false 或 dynamic: strict；  业务索引：dynamic: strict；  调试数据单独放测试索引；  新字段走评审；  使用 meta 记录字段负责人和口径。PUT orders-v4{  "mappings": {    "dynamic": "strict",    "_meta": {      "owner": "order-team",      "version": "v4"    },    "properties": {}  }}28.5.3 嵌套与父子nested 精确但昂贵；join 灵活但查询和内存成本更高。优先顺序通常是：反范式冗余 &gt; object(若语义允许) &gt; nested &gt; join如果 nested 对象数量可能无限增长，必须设置上限并监控。28.6 JVM 调优Elasticsearch 对 JVM 堆和页缓存的依赖都很强。经验规则：  JVM 堆不超过 50% 机器内存；  堆大小不超过 31GB；  Xms 和 Xmx 相等；  保留足够页缓存给 Lucene；  优先使用 G1GC；  不要随意切换实验 GC；  观察老年代回收频率，而不是只看堆使用率。查看 JVM：GET /_nodes/stats/jvm?humanGET /_nodes/hot_threads常见问题：            现象      可能原因                  Old GC 频繁      堆不足、查询结果过大、fielddata、segment 内存              Heap 100% 且熔断      聚合桶过多、深分页、大请求              GC 后堆不下降      缓存或长期对象占用              JVM 正常但查询慢      页缓存不足、磁盘 IO、查询扇出      不要把堆调到机器内存的 80%。Lucene 依赖操作系统页缓存，堆过大反而降低整体性能。28.7 磁盘与文件系统生产建议：  数据盘使用 SSD，尤其是 hot 层；  避免网络存储或高抖动云盘；  磁盘吞吐与写入、merge、恢复匹配；  预留磁盘余量，避免 85% 水位；  使用 noatime；  文件描述符上限充足；  关注 IO util、await、queue；  merge 和 recovery 都会大量使用磁盘。查看磁盘：GET /_cat/allocation?vGET /_cat/nodes?v&amp;h=name,disk.used_percent,disk.total,disk.availGET /_nodes/stats/fs?human28.8 线程池查看线程池：GET /_cat/thread_pool/search,write?v&amp;h=node_name,name,active,queue,rejected,completed拒绝处理：            池      常见原因      处理                  write      写入并发过高、merge、磁盘慢      降并发、批量、扩容              search      查询扇出、深分页、聚合大      限流、优化 DSL、预聚合              get      高频点查过多      检查是否误用 ES 做主存储              flush      磁盘慢、分片多      IO 评估、flush 频率              refresh      刷新过频      放宽 refresh_interval      不建议首先加大队列。队列越长，故障时的内存压力和等待延迟越大。28.9 冷热与隔离常见隔离方式：            隔离      解决问题                  hot/warm/cold      日志生命周期和成本              读写分离      重查询不影响写入              在线/离线集群      大分析不拖垮在线服务              多租户索引隔离      权限、流量和容量边界              Kibana 与 API 集群分离      可视化流量隔离      如果团队经常在 30 天日志上做临时聚合，应把这些分析迁移到离线集群或数据仓库，而不是让在线日志集群承担分析负载。28.10 压测方法推荐使用 Rally 或自建压测：esrally race --track=geonames --challenge=append-no-conflicts --car=4gheap压测必须覆盖：  混合读写；  目标数据规模；  目标字段 Mapping；  查询语句集合；  峰值持续时长；  merge 和 refresh 周期；  节点故障场景；  磁盘接近水位后的行为。只做短时间纯写入压测，无法代表生产负载。28.11 调优清单  有明确 SLO 和基线数据；  写入使用 Bulk 和合理并发；  refresh interval 按场景设置；  导入后自动恢复副本；  权衡 translog 可靠性；  查询限制索引和时间范围；  filter 优先；  size 和分页受控；  source filtering；  聚合限制桶数和基数；  Mapping 关闭不用的 doc_values 和 index；  JVM 堆和页缓存比例合理；  磁盘余量充足；  慢查询、rejected、GC、merge 有监控；  所有参数变更可回滚并记录原因。本章小结性能调优的第一步是定位瓶颈：写入端看 bulk、refresh、merge、translog 和磁盘；查询端看扇出、分页、候选集、聚合桶和 fetch；资源端看 JVM、页缓存、IO 和线程池。大多数优化来自更小的数据范围、更合理的 Mapping 和更受控的请求模式，而不是神秘参数。思考题  为什么 JVM 堆不是越大越好？  write rejected 和 search rejected 的处理思路有什么不同？  放宽 refresh interval 会带来什么代价？  哪些字段可以关闭 doc_values，哪些不能？  如果压测吞吐很高但线上延迟很差，可能遗漏了什么测试维度？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。相关性优化是搜索系统从“能搜到”走向“搜得好”的关键。它不只是调大某个字段权重，而是包含查询理解、召回、特征、排序、评估和持续迭代的完整过程。27.1 什么是相关性从用户视角看，相关结果应满足：  语义匹配：商品确实符合关键词；  意图匹配：理解品牌、型号、规格、用途；  可购买：有货、可售、价格合理；  可信：销量、评价、售后、商家服务；  可解释：排序符合用户直觉，异常结果能解释。从系统视角看，排序由三部分组成：相关性得分：BM25 / 向量相似度 / phrase / boost业务得分：销量、转化、库存、信誉、时效策略得分：置顶、活动、多样性、个性化只优化 BM25 往往解决不了业务排序问题；只按销量排序又会牺牲文本相关性。27.2 BM25 原理BM25 是 Elasticsearch 默认文本评分算法。直观形式：score =  IDF(term)  x TF(term in doc) / (TF + k1 * (1 - b + b * dl / avgdl))  x boost三个核心因素：            因素      含义      影响                  IDF      词在集合中越罕见，区分度越高      罕见词贡献更高              TF      词在文档中出现次数越多，越相关      受饱和效应限制              文档长度 norm      文档越短，同样匹配越集中      长文档被适当惩罚      参数：  k1：控制词频饱和；  b：控制字段长度归一化强度。查看字段 similarity：GET products-v3/_mapping/field/title自定义 similarity：PUT articles-v1{  "settings": {    "index": {      "similarity": {        "default": {          "type": "BM25",          "k1": 1.2,          "b": 0.75        }      }    }  }}除非有评测集证明有效，不建议凭感觉改 BM25 参数。27.3 字段权重不同字段的重要性不同。标题、品牌、类目、运营词、详情应有不同权重：GET products-v3/_search{  "query": {    "multi_match": {      "query": "苹果手机",      "type": "best_fields",      "fields": [        "title^4",        "brand_name^3",        "keywords^2",        "description^1"      ]    }  }}best_fields：取匹配效果最好的字段得分，适合“多个字段表达同一含义”。most_fields：多个字段得分相加，适合字段互补，但容易让多字段弱匹配超过单字段强匹配。cross_fields：把多个字段视作一个大字段，适合名、姓、地址拼接，但对中文复杂分词要谨慎。            类型      适合      风险                  best_fields      标题优先的搜索      忽略多字段弱匹配              most_fields      多信号补充      重复加权              cross_fields      结构化姓名地址      依赖 analyzer 一致              phrase      精确顺序要求      召回变少，成本更高      27.4 Match 与 Phrasematch 不保证词序：{ "match": { "title": "红色无线鼠标" } }match_phrase 要求词项顺序和临近：{ "match_phrase": { "title": "无线鼠标" } }组合使用：GET products-v3/_search{  "query": {    "bool": {      "should": [        { "match": { "title": { "query": "无线鼠标", "boost": 1 } } },        { "match_phrase": { "title": { "query": "无线鼠标", "boost": 3, "slop": 1 } } }      ],      "minimum_should_match": 1    }  }}这样能先保证召回，再让更精确的短语排序更靠前。27.5 Function ScoreFunction Score 用于把业务因子加入相关性排序。GET products-v3/_search{  "query": {    "function_score": {      "query": {        "multi_match": {          "query": "笔记本",          "fields": ["title^4", "keywords^2"]        }      },      "functions": [        {          "filter": { "term": { "tags": "hot" } },          "weight": 1.2        },        {          "field_value_factor": {            "field": "sales_30d",            "modifier": "log1p",            "factor": 0.5,            "missing": 0          }        },        {          "gauss": {            "on_shelf_at": {              "origin": "now",              "scale": "30d",              "decay": 0.5            }          }        },        {          "filter": { "term": { "shop_grade": "a" } },          "weight": 1.1        }      ],      "score_mode": "sum",      "boost_mode": "multiply",      "max_boost": 10    }  }}常用函数：            函数      用途                  weight      固定加权              field_value_factor      根据数值字段加分              decay      时间、距离、价格接近度衰减              random_score      实验分流或打散              script_score      自定义公式或模型打分      score_mode 决定多个函数之间怎么合并；boost_mode 决定函数结果和原查询得分怎么合并。上线前必须固定排序版本并记录特征值。27.6 Script Score当公式复杂时，可以使用 script score：GET products-v3/_search{  "query": {    "script_score": {      "query": {        "multi_match": {          "query": "笔记本",          "fields": ["title^4", "keywords^2"]        }      },      "script": {        "source": """          double score = _score;          double sales = doc['sales_30d'].size() == 0 ? 0 : doc['sales_30d'].value;          double stock = doc['stock'].size() == 0 ? 0 : doc['stock'].value;          score += Math.log1p(sales) * params.salesWeight;          if (stock &gt; 0) {            score *= params.stockBoost;          }          return score;        """,        "params": {          "salesWeight": 0.8,          "stockBoost": 1.05        }      }    }  }}注意：  脚本必须参数化；  优先使用 Painless；  限制脚本执行时间；  避免在脚本中访问超大嵌套结构；  脚本复杂时考虑外部重排；  需要记录 score 可解释性。27.7 多路召回与 Rerank复杂搜索常用多路召回：query  -&gt; 文本召回 BM25  -&gt; 短语召回 phrase boost  -&gt; 向量召回 semantic kNN  -&gt; 运营/个性化召回  -&gt; merge by id  -&gt; first-stage rank  -&gt; rerank model混合检索示例：GET products-v3/_search{  "size": 50,  "query": {    "bool": {      "must": [        { "match": { "title": "轻薄高续航笔记本" } }      ],      "should": [        { "match_phrase": { "title": { "query": "轻薄高续航笔记本", "boost": 2 } } }      ],      "filter": [        { "term": { "status": "on_sale" } }      ]    }  },  "knn": {    "field": "embedding",    "query_vector": [0.01, 0.02, 0.03],    "k": 50,    "num_candidates": 200  }}不同版本对 query 与 kNN 的组合方式有差异，需要按当前版本验证。工程上更常见的做法是：  分别取 BM25 top N 和向量 top N；  应用层合并去重；  使用 RRF（Reciprocal Rank Fusion）或模型 rerank；  再叠加业务规则；  记录每个来源的分数和排名。RRF 示例：score(doc) = sum(1 / (k + rank_i(doc)))其中 k 常取 60。RRF 不要求不同来源的分数可比，适合合并 BM25 和向量结果。27.8 搜索评估没有评估体系，相关性优化只能靠感觉。至少建立三层数据。27.8.1 离线评测集            query      文档      相关等级      标注人                  无线鼠标      doc_1      3      搜索组              无线鼠标      doc_2      2      业务组              无线鼠标      doc_3      0      搜索组      常用指标：  Precision@K；  Recall@K；  NDCG@K；  MRR；  MAP；  无结果率；  首屏点击率。NDCG 关注高质量文档是否排在前面，适合搜索排序评估。27.8.2 在线指标  点击率 CTR；  首屏点击率；  加购率；  转化率；  换词率；  无结果率；  搜索时长；  退货或投诉率。27.8.3 分层指标不同 query 应分开看：            类型      示例      重点                  品牌词      iPhone      品牌、型号、正品              类目词      手机      类目、销量、多样性              属性词      轻薄 16G      属性召回              长尾词      送女生的礼物      意图、推荐              错别字      显视器      纠错、同义词      全站平均指标可能掩盖局部劣化，必须按 query 分层评估。27.9 点击日志与反馈闭环搜索日志建议记录：PUT search_logs-v1/_doc/search_log_001{  "search_id": "search_log_001",  "user_id": "u_1001",  "query": "无线鼠标",  "rewritten_query": "无线 鼠标",  "result_ids": ["sku_1", "sku_2", "sku_3"],  "result_scores": [12.3, 11.8, 10.1],  "clicked_positions": [1],  "search_version": "rank-v3.4",  "experiment_bucket": "control",  "latency_ms": 48,  "result_count": 320,  "created_at": "2026-08-25T10:00:00Z"}这些数据可以用于：  统计点击率和换词率；  发现坏 query；  生成候选同义词；  构建训练样本；  验证排序版本；  排查线上排序问题。注意不要把点击当成绝对真理：首屏位置本身会带来点击偏差，需要用位置偏差修正或 A/B 实验验证。27.10 排名稳定性与多样性搜索结果不稳定会损害用户信任。常见原因：  数据持续写入；  refresh 后分片统计变化；  排序键重复；  随机分数未固定 seed；  个性化上下文变化；  实验分流变化。建议：  sort 加稳定 tie-breaker；  实验分桶在会话内稳定；  同一 query 短时间缓存；  新品或随机流量控制在固定比例；  避免无意义的频繁重排。多样性策略：  同店铺、同品牌限量；  类目分桶；  MMR 或规则打散；  置顶数量限制；  广告与自然结果区分标注。27.11 排序治理流程一次相关性优化应经过：发现 bad case  -&gt; 归因：召回 / 过滤 / 分词 / 排序  -&gt; 构造离线评测集  -&gt; 修改配置或模型  -&gt; 离线回归  -&gt; 小流量实验  -&gt; 在线指标验证  -&gt; 全量上线  -&gt; 记录版本与回滚方案禁止只看一个 query 的截图就上线权重改动。搜索是全局系统，一个 case 的提升可能带来一批 query 劣化。27.12 相关性优化清单  标题、品牌、类目、属性字段的职责清晰；  同义词、停用词、拼音、纠错策略有版本管理；  权重改动必须通过离线评测；  每次请求记录排序版本和主要特征；  用 _explain 和 profile 排查 bad case；  业务因子可解释、可配置；  新排序支持灰度和回滚；  点击日志完整接入；  建立分层指标和固定评测集；  向量与文本混合检索有融合策略；  定期治理无结果和高换词 query。本章小结相关性优化是“查询理解 + 召回 + 排序 + 评估”的闭环。BM25 和字段权重只是起点，业务因子、点击反馈、评测集和版本治理才决定搜索质量能否持续提升。所有排序改动都要可解释、可回放、可回滚。思考题  BM25 中 IDF、TF 和文档长度分别如何影响得分？  best_fields 和 most_fields 分别适合什么场景？  为什么排序字段需要唯一 tie-breaker？  BM25 分数和向量相似度为什么不能直接相加？  如果一个 query 效果变差，你会如何定位是分词、召回还是排序问题？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。聚合是 Elasticsearch 成为分析引擎的关键能力。它可以在搜索结果上做分组、统计、嵌套和管道计算，但聚合也最容易把集群打慢。理解聚合执行模型、Doc Values、global ordinals 和分片归并机制，才能安全使用聚合。26.1 聚合执行模型一次聚合请求会分发到所有相关分片：coordinating node  -&gt; shard 0: local aggregation  -&gt; shard 1: local aggregation  -&gt; shard 2: local aggregation  &lt;- merge shard-level results  -&gt; final responseTerms 聚合的典型流程：  每个分片统计本分片 top shard_size 词项；  返回词项和本地计数；  协调节点归并；  按全局近似计数重排；  返回 top size。这就是 terms 聚合可能近似的原因。如果某个值在很多分片上都排第 20 名以外，但它的全局总数很高，本地截断可能让协调节点看不到它。GET orders-v3/_search{  "size": 0,  "aggs": {    "top_categories": {      "terms": {        "field": "category_id",        "size": 10,        "shard_size": 100      }    }  }}提高 shard_size 可以提高准确率，但会增加网络、内存和归并成本。26.2 Doc Values 与聚合大多数聚合依赖 Doc Values 正排读取：doc id -&gt; field value如果字段没有 Doc Values，聚合会失败或退化为 fielddata。text 字段默认不能直接聚合，因为分词后一个字段会变成多个词项，业务含义不清。错误示例：GET products-v3/_search{  "size": 0,  "aggs": {    "brands": {      "terms": { "field": "brand_name" }    }  }}如果 brand_name 是 text，应聚合 brand_name.keyword，或单独设计 brand_id / brand_name_raw keyword 字段。合理 Mapping：PUT sales-v1{  "mappings": {    "properties": {      "city_name": { "type": "keyword" },      "city_text": {        "type": "text",        "fields": {          "keyword": { "type": "keyword" }        }      },      "amount": { "type": "double" }    }  }}“text 字段直接聚合”是高频 Mapping 错误之一。生产请求应使用 city_text.keyword。26.3 Global Ordinals对于 keyword 等分类型字段，Lucene 可以构建 global ordinals：为每个分片内的唯一值建立全局编号，posting list 和 Doc Values 可以用编号表示，减少字符串比较和存储。优点：  terms 聚合更快；  内存占用可控；  多字段比较更高效。代价：  首次使用前要构建；  新 Segment 加入后可能重构建；  高基数字段构建成本高；  eager global ordinals 会把构建提前到 refresh 后。查看全局序数指标：GET /_stats/indices/fielddata?humanGET products-v3/_stats/fielddata?human对于查询延迟敏感、字段基数可控的索引，可以预热：PUT products-v3/_settings{  "index": {    "eager_global_ordinals": true  }}这不是万能优化。如果字段有一千万唯一值，预热本身可能造成长尾延迟。26.4 聚合类型与成本            聚合      成本特点      建议                  terms      与唯一值数量、shard_size 相关      控制 size，优先低基数字段              date_histogram      桶数量由时间范围和 interval 决定      固定 interval，限制范围              range      桶数量固定      比动态 terms 稳定              histogram      桶数可能爆炸      控制 interval 和范围              cardinality      近似去重，占内存      控制 precision_threshold              percentile      近似，内存高      明确精度需求              nested      需要跨嵌套关系映射      控制嵌套对象数量              composite      游标分页拉取全量组合      用于导出，不适合首屏              top_hits      每个 bucket 保留文档      控制 size 和 source              script      每文档执行脚本      避免在大数据集运行      原则：  先限制查询范围，再聚合；  先减少桶数，再嵌套聚合；  先预计算，再实时算；  先看监控，再调参数；  高基数字段必须白名单管控。26.5 嵌套聚合嵌套聚合的成本是乘法关系：外层 100 个 bucket x 内层 20 个 bucket x 内层 metric 3 个即使最终返回不多，中间也需要维护大量桶状态。GET orders-v3/_search{  "size": 0,  "query": {    "range": { "paid_at": { "gte": "now-1d/d" } }  },  "aggs": {    "categories": {      "terms": { "field": "category_id", "size": 20 },      "aggs": {        "cities": {          "terms": { "field": "city_id", "size": 20 },          "aggs": {            "gmv": { "sum": { "field": "pay_amount" } },            "buyers": { "cardinality": { "field": "user_id" } }          }        }      }    }  }}优化方式：  缩小时间范围；  降低外层 size；  只在需要时增加嵌套层；  把明细聚合拆成多个并行查询；  使用预聚合表；  大规模分析转到数仓。26.6 Cardinalitycardinality 使用 HyperLogLog++ 近似算法。GET orders-v3/_search{  "size": 0,  "aggs": {    "buyers": {      "cardinality": {        "field": "user_id",        "precision_threshold": 40000      }    }  }}precision_threshold 越高越准确，内存越大。阈值内通常接近精确，超过后误差增大。适合：  看板趋势；  影响面评估；  粗略独立用户数。不适合：  财务结算；  精确对账；  低容忍度报表；  超大基数下强制高精度。如果必须精确去重，可以在写入侧预聚合，或使用离线计算。26.7 Date Histogram时间分桶要控制桶数：30 天 x 1 分钟间隔 = 43200 个桶如果还嵌套 100 个类目，中间桶规模会迅速膨胀。GET orders-v3/_search{  "size": 0,  "query": {    "range": {      "paid_at": {        "gte": "now-7d/d",        "lte": "now"      }    }  },  "aggs": {    "per_hour": {      "date_histogram": {        "field": "paid_at",        "fixed_interval": "1h",        "time_zone": "Asia/Shanghai",        "min_doc_count": 0      }    }  }}优化建议：  使用 fixed_interval 避免动态日历桶不可控；  查询范围和 interval 匹配，例如 30 天用小时或天；  使用 extended_bounds 时必须限制范围；  看板默认时间不超过必要范围；  预聚合分钟级或小时级数据。26.8 Composite 聚合Composite 用于游标式拉取维度组合：GET orders-v3/_search{  "size": 0,  "aggs": {    "combinations": {      "composite": {        "size": 1000,        "sources": [          { "channel": { "terms": { "field": "channel" } } },          { "category": { "terms": { "field": "category_id" } } }        ]      },      "aggs": {        "gmv": { "sum": { "field": "pay_amount" } }      }    }  }}响应中的 after_key 用于下一页："aggregations": {  "combinations": {    "after_key": { "channel": "app", "category": "cat_1" }  }}下一页："composite": {  "size": 1000,  "after": { "channel": "app", "category": "cat_1" },  "sources": []}Composite 适合离线导出、报表生成和维表构建，不适合用户每次打开页面都拉全量组合。26.9 预聚合当实时聚合成本过高时，可以把固定维度提前算好。PUT order_reports_5m-v1{  "mappings": {    "properties": {      "stat_time": { "type": "date" },      "channel": { "type": "keyword" },      "category_id": { "type": "keyword" },      "gmv": { "type": "double" },      "order_count": { "type": "long" },      "buyer_count_approx": { "type": "long" }    }  }}预聚合适合：  固定看板；  高 QPS 报表；  长时间范围对比；  多维组合数量可控；  数据延迟允许分钟级。不适合：  任意自由探索；  新维度频繁变化；  精确去重要求高；  明细排障场景。常见架构是“明细短期 + 预聚合长期”：最近 1-3 天：查明细索引更长时间：查分钟/小时/天级预聚合对账与复杂建模：数仓26.10 聚合监控查看聚合耗时：GET /_nodes/stats/indices/search?humanGET orders-v3/_stats/search?human查看慢查询：PUT orders-v3/_settings{  "index.search.slowlog.threshold.query.warn": "5s",  "index.search.slowlog.threshold.query.info": "2s",  "index.search.slowlog.threshold.fetch.warn": "1s"}重点指标：            指标      含义                  query_time_in_millis      查询阶段总耗时              query_current      当前查询数              fetch_current      当前 fetch 数              search thread pool rejected      搜索线程池拒绝              circuit breaker tripped      熔断次数              fielddata memory      fielddata 内存              request cache hit / miss      请求缓存              indicessegments memory      Segment 内存      聚合慢的典型信号：  CPU 高但磁盘 IO 不高；  query time 高；  heap 使用增长；  circuit breaker 触发；  search rejected；  响应 JSON 很大；  Dashboard 同时发出多个大范围聚合。26.11 聚合优化清单  查询必须有明确范围，尤其时间范围；  size: 0 获取纯统计结果；  terms 字段使用 keyword 或 .keyword；  控制 terms 的 size 和 shard_size；  高基数字段禁止自由聚合；  时间桶数量设置上限；  避免多层嵌套聚合；  top_hits 只返回必要字段；  脚本聚合必须评审；  cardinality 明确精度阈值和用途；  长期报表预聚合；  慢查询日志和熔断指标接入告警；  大范围分析迁移到离线集群或数仓。本章小结聚合性能由候选文档数、桶数量、唯一值数量、嵌套深度、Doc Values、global ordinals 和分片归并共同决定。安全的做法是先限制范围和基数，再优化聚合结构，最后用预聚合或离线计算承接重分析。思考题  terms 聚合为什么可能是近似结果？  为什么 text 字段不能直接用于 terms 聚合？  global ordinals 的收益和代价是什么？  什么情况下应该把实时聚合改为预聚合？  一个多维看板查询很慢，你会如何逐层定位？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。搜索请求看起来只是一条 JSON，但背后是协调节点、多个分片、Lucene 查询树、缓存、排序队列和 fetch 阶段的协作。理解这个过程，才能解释深分页、查询超时、缓存命中、评分差异和查询取消。25.1 查询的两个阶段Elasticsearch 默认搜索分为 query phase 和 fetch phase。sequenceDiagram    participant C as Client    participant N as Coordinating Node    participant S1 as Shard 0    participant S2 as Shard 1    participant S3 as Shard 2    C-&gt;&gt;N: search request    N-&gt;&gt;S1: query phase    N-&gt;&gt;S2: query phase    N-&gt;&gt;S3: query phase    S1--&gt;&gt;N: top N doc id + score + sort values    S2--&gt;&gt;N: top N doc id + score + sort values    S3--&gt;&gt;N: top N doc id + score + sort values    N-&gt;&gt;N: merge and choose final top size    N-&gt;&gt;S1: fetch selected docs    N-&gt;&gt;S2: fetch selected docs    N-&gt;&gt;S3: fetch selected docs    S1--&gt;&gt;N: _source / highlight    S2--&gt;&gt;N: _source / highlight    N--&gt;&gt;C: final response25.1.1 Query Phase每个分片执行：  解析查询 DSL；  构造 Lucene 查询树；  找到候选文档；  计算评分或应用 filter；  按 _score 或 sort values 排序；  返回前 from + size 条文档标识。这一阶段不返回 _source，只返回足够的排序信息和 doc id。25.1.2 Fetch Phase协调节点归并出最终结果后，只去相关分片取被选中的文档内容：  _source；  selected fields；  highlight；  nested 或 parent/child 需要的字段；  script fields；  explain 信息。因此，返回 20 条结果通常比返回 1000 条便宜得多。深分页的问题不在最后一页展示多少条，而在于每个分片必须为前 from + size 条维护排序队列。25.2 查询树改写DSL 会被解析成 Lucene 查询对象，并在执行前改写。例如：GET products-v3/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "轻薄笔记本" } }      ],      "filter": [        { "term": { "status": "on_sale" } },        { "range": { "price": { "lte": 8000 } } }      ]    }  }}可能被改写为类似结构：BooleanQuery  MUST: BM25 scored query for title  FILTER: TermQuery(status=on_sale)  FILTER: IndexOrDocValuesQuery(price &lt;= 8000)查询改写会考虑：  字段类型；  是否有 norms；  是否需要评分；  是否能使用缓存；  range 查询适合倒排索引还是 Doc Values；  match phrase 是否需要 positions；  nested 查询是否能精确关联子对象。这也是为什么 Mapping 会影响查询性能，而不仅是影响磁盘占用。25.3 Query 与 Filterquery 上下文回答“这个文档是否匹配，匹配得怎么样”，会计算相关性评分；filter 上下文回答“是或不是”，不评分，结果可缓存。GET products-v3/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "filter": [        { "term": { "status": "on_sale" } },        { "range": { "price": { "gte": 3000, "lte": 8000 } } },        { "term": { "category_id": "cat_101" } }      ]    }  }}判断标准：            条件      建议                  关键词、标题、描述      must / should，参与评分              状态、类目、标签、权限      filter              时间范围、价格范围      filter              需要影响相关性的运营加权      should 或 function_score              只做统计范围限制      post_filter 或聚合外部条件      经验法则：能放 filter 的条件不要放 must。权限过滤尤其应该放 filter，并可被缓存。25.4 DFS Query Then FetchBM25 评分需要词项在分片内的统计信息。默认 query then fetch 使用分片本地统计，不同分片对同一词的 IDF 可能略有差异。GET products-v3/_search?dfs_query_then_fetch{  "query": {    "match": { "title": "elasticsearch" }  }}DFS Query Then Fetch 会先收集全局词频，再执行评分，分数更接近单索引语义，但多一轮通信，通常更慢。是否使用 DFS：            场景      建议                  数据量大、分片统计均匀      默认即可              小索引、低频词、评分奇怪      可测试 DFS              高 QPS 在线搜索      谨慎使用              排序主要由业务因子决定      默认即可      生产搜索通常不会盲目开启 DFS，而是通过评测集判断收益。25.5 归并与排序协调节点拿到每个分片的前 from + size 条结果后，按排序键归并。排序键可能是：  _score；  字段值；  _id；  geo distance；  script score；  nested 或 parent/child 内部排序。如果排序键存在相同值，结果顺序可能不稳定。常见处理：GET products-v3/_search{  "sort": [    { "sales_30d": "desc" },    { "_id": "asc" }  ]}用 _id 做 tie-breaker 能得到稳定顺序，但 _id 是字符串，排序成本较高。业务系统也可以引入单调递增的 sequence_no 或 updated_seq。25.6 深分页25.6.1 From / Sizefrom = 10000, size = 20每个分片需要取前 10020 条协调节点最多归并 分片数 * 10020 条排序候选默认限制：index.max_result_window = 10000查看和修改：GET products-v3/_settings/index.max_result_windowPUT products-v3/_settings{  "index.max_result_window": 20000}不要为了业务随手调大。深分页会放大内存和 CPU 压力，还可能被恶意请求利用。25.6.2 Search AfterSearch After 适合无跳页的连续翻页：GET products-v3/_search{  "size": 20,  "sort": [    { "sales_30d": "desc" },    { "_id": "asc" }  ],  "search_after": [10000, "sku_10001"]}每个结果会返回 sort 值，下一页把它作为游标："sort": [12300, "sku_10020"]限制：  不能直接跳到第 N 页；  排序值必须稳定唯一；  页面刷新期间数据变化可能导致重复或遗漏；  需要配合 PIT 获得更一致的快照。25.6.3 Point In TimePIT 保存一个轻量的索引视图：POST products-v3/_pit?keep_alive=2m返回：{  "id": "pit_id_base64"}带 PIT 查询：GET /_search{  "size": 20,  "pit": {    "id": "pit_id_base64",    "keep_alive": "2m"  },  "sort": [    { "sales_30d": "desc" },    { "_id": "asc" }  ],  "search_after": [10000, "sku_10001"]}删除 PIT：DELETE /_pit{  "id": "pit_id_base64"}PIT 会持有 searcher 相关资源，必须设置合理 keep_alive，用完及时删除。25.6.4 ScrollScroll 适合离线导出：POST products-v3/_search?scroll=2m{  "size": 1000,  "sort": ["_doc"]}Scroll 快照成本高，不适合普通用户分页。新代码应优先考虑 PIT + search after，导出场景也应设置清楚超时和清理机制。25.7 缓存Elasticsearch 有多类缓存：            缓存      作用对象      失效时机                  Node Query Cache      filter 查询结果位图      段结构变化、容量淘汰              Request Cache      size=0 请求结果      索引 refresh、分段变化              Field Data Cache      text 聚合等正排需求      建议避免使用              Page Cache      操作系统磁盘页      内存压力              Shard Request Cache      聚合与 suggestion      refresh 后失效      查看缓存：GET products-v3/_stats/query_cache,request_cache,fielddataRequest Cache 只对 size=0 的请求生效：GET products-v3/_search{  "size": 0,  "query": {    "bool": {      "filter": [        { "range": { "created_at": { "gte": "now-1d/d", "lte": "now/d" } } }      ]    }  },  "aggs": {    "brands": {      "terms": { "field": "brand_id" }    }  },  "request_cache": true}如果查询里使用 now 精确到毫秒，会导致缓存几乎无法命中。把时间取整为当天、当小时，能显著提高复用率。25.8 查询取消与超时搜索可以设置超时：GET products-v3/_search{  "timeout": "2s",  "query": {    "match_all": {}  }}timeout 是尽力而为的检查点，不保证请求一定被立即取消。响应中的 timed_out 表示部分结果可能不完整："timed_out": true查询取消来源包括：  客户端断开连接；  搜索超时；  搜索任务被取消；  分片级超时或失败；  搜索线程池拒绝。查看当前搜索任务：GET /_tasks?actions=*search*&amp;detailedGET /_cat/tasks?v取消任务：POST /_tasks/task_id:1/_cancel应用侧必须设置请求超时和最大返回值，不能把用户输入直接变成无约束查询。25.9 查询线程池搜索请求使用 search thread pool：GET /_cat/thread_pool/search?v&amp;h=node_name,name,active,queue,rejected,completedGET /_nodes/stats/indices/search常见压力来源：  查询扇出过大；  深分页；  大 size；  高基数字段聚合；  通配符前缀查询；  script 遍历大量文档；  查询时间范围过宽；  查询与写入争抢资源。治理顺序通常是：  限制 from/size 和索引范围；  改写查询，缩小 filter；  优化 Mapping 和字段类型；  使用预聚合；  拆分冷热集群；  增加节点或分片；  调整线程池，但这通常是最后一步。25.10 Explain 与 Profile25.10.1 Explainexplain 查看文档为什么匹配：GET products-v3/_search{  "explain": true,  "query": {    "match": { "title": "轻薄笔记本" }  }}也可以解释指定文档：GET products-v3/_explain/sku_10001{  "query": {    "match": { "title": "轻薄笔记本" }  }}explain 本身昂贵，只适合排查单条请求，不应在高 QPS 常规查询中开启。25.10.2 ProfileProfile API 查看查询耗时分布：GET products-v3/_search{  "profile": true,  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "filter": [        { "term": { "status": "on_sale" } }      ]    }  }}关注：  rewrite time；  collect time；  next doc time；  score time；  build_scorer time；  aggregation time；  fetch phase。Profile 会增加开销，输出也很大。适合测试环境或低峰单请求诊断。25.11 查询优化清单  请求只指向必要的索引或别名；  强制时间范围；  size 尽量小；  深分页改 search after + PIT；  filter 优先；  避免通配符前缀和宽泛正则；  高基数字段聚合白名单化；  排序字段使用 Doc Values；  _source 只返回必要字段；  脚本参数化并避免全量遍历；  聚合结果可缓存时使用固定时间桶；  用 profile 定位，不靠猜；  查询 QPS 和扇出有上限；  慢查询日志接入监控。本章小结一次搜索由 query phase 找到每个分片的候选结果，再由 fetch phase 取回最终文档内容。查询扇出、深分页、缓存、评分和排序队列共同决定了性能。优化搜索时，先缩小数据范围和返回规模，再优化 DSL 和 Mapping，最后才考虑扩容。思考题  Query phase 和 fetch phase 分别做什么？  为什么深分页会放大内存和 CPU 成本？  Search After 为什么必须配合稳定排序？  Request Cache 为什么对 size=0 请求更有价值？  遇到查询延迟毛刺，你会用哪些工具定位？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。写入链路是性能和可靠性的交汇点。理解一次写入请求在协调节点、主分片、副本分片、Lucene 内存、translog、refresh 和 flush 之间的流转后，才能解释写入延迟、可见性、数据恢复和写入放大。本章拆解完整的写入过程。24.1 一次 Index 请求的生命周期假设写入 orders-v3/_doc/order_10001，索引有 3 个主分片和 1 个副本：sequenceDiagram    participant C as Client    participant N as Coordinating Node    participant P as Primary Shard    participant R as Replica Shard    C-&gt;&gt;N: PUT /orders-v3/_doc/order_10001    N-&gt;&gt;N: 根据路由计算目标主分片    N-&gt;&gt;P: 转发写请求    P-&gt;&gt;P: 校验 Mapping / 执行 pipeline    P-&gt;&gt;P: 写 Indexing Buffer + translog    P-&gt;&gt;R: 同步副本    R-&gt;&gt;R: 写 Indexing Buffer + translog    R--&gt;&gt;P: 副本成功    P--&gt;&gt;N: 主副本均成功    N--&gt;&gt;C: 返回 2xx默认 wait_for_active_shards=1 只要求写入前主分片可用，但请求成功还取决于 consistency 和版本实现。8.x 默认副本写入失败会导致主流程失败，具体行为要结合版本确认。核心点：  协调节点负责路由，不决定文档内容；  主分片负责校验和写入顺序；  副本接收主分片转发的内容；  写成功不代表立刻可搜索；  translog 保证故障恢复，refresh 决定可见性。24.2 协调节点做什么协调节点执行：  解析请求；  根据 _id 或自定义 routing 计算分片；  选择目标主分片所在节点；  转发请求；  汇总响应或错误。Bulk 请求会更复杂：POST _bulk{"index": {"_index": "orders-v3", "_id": "order_1"}}{"order_id": "order_1", "pay_amount": 100}{"index": {"_index": "orders-v3", "_id": "order_2"}}{"order_id": "order_2", "pay_amount": 200}协调节点需要把批量项按目标分片拆分，分别发给对应主分片，再把逐项结果按原始顺序组装返回。因此：  Bulk 中部分成功、部分失败是正常的；  应用必须检查每个 item 的状态；  请求过大可能导致协调节点内存压力；  Bulk size 通常从 1MB 到 10MB 之间测试；  发生 429 时应指数退避，而不是立即重试。24.3 主分片写入主分片写入前会处理：  Mapping 校验与动态映射更新；  自动生成 _id；  版本检查；  routing 校验；  ingest pipeline；  脚本更新或 doc update。更新请求会经历：读取 _source  -&gt; 应用 update doc 或 script  -&gt; 标记旧文档 deleted  -&gt; 写入新文档因此 update 比 index 更重。它需要读旧值、计算新值，并产生删除标记。高频更新同一个文档会显著放大写入。24.4 版本控制与乐观并发Elasticsearch 通过 seq_no 和 primary_term 实现乐观并发控制。PUT products-v3/_update/sku_10001?if_seq_no=10&amp;if_primary_term=1{  "doc": {    "price": 4999  }}如果读取后有其他人已经修改，写入会返回 409 冲突。应用可以选择：  重新读取再修改；  使用 retry_on_conflict；  改成以事件时间为准的脚本判断；  将热点文档更新合并到外部缓冲，再低频写入。POST products-v3/_update/sku_10001?retry_on_conflict=3{  "script": {    "source": "ctx._source.stock += params.delta",    "lang": "painless",    "params": { "delta": -1 }  }}retry_on_conflict 适合幂等性较弱但冲突可控的更新。如果每次库存扣减都直接打 ES，热点 SKU 会成为瓶颈，交易扣减应以数据库或缓存为准，再异步同步到搜索。24.5 RefreshRefresh 把 Indexing Buffer 中的数据生成一个新 Segment，并打开可搜索。write -&gt; memory + translog -&gt; refresh -&gt; searchable segment查看和调整刷新间隔：GET orders-v3/_settings/index.refresh_intervalPUT orders-v3/_settings{  "index.refresh_interval": "5s"}常见策略：            场景      建议值                  常规业务索引      1s              日志高频写入      5s 到 30s              批量导入      -1，导入完成后恢复              写后必须可查      refresh=wait_for 或接口层等待              测试或演示      refresh=true      批量导入示例：PUT import-v1/_settings{  "refresh_interval": "-1",  "number_of_replicas": 0}导入完成后：PUT import-v1/_settings{  "refresh_interval": "1s",  "number_of_replicas": 1}不要忘记恢复副本。很多人批量导入时把副本设为 0，导入完成后忘记恢复，节点故障时数据风险会很高。24.6 FlushFlush 是 Lucene commit，将变更真正提交到磁盘，并生成新的 commit point。Indexing Buffer / Segment  -&gt; fsync  -&gt; Lucene commit  -&gt; 清理 translogFlush 比 Refresh 成本高得多。它受 translog 大小和时间阈值控制：GET orders-v3/_stats/flush,merge,refresh,indexing如果 flush 频繁：  translog 阈值过小；  磁盘写入能力不足；  分片数太多导致每个分片都频繁触发；  写入请求过小，系统调用和元数据开销占比高。24.7 Translog 与恢复节点重启或崩溃时，Lucene 最近 commit 之后但已对客户端确认的变更需要通过 translog 重放。恢复流程：打开最近 commit point  -&gt; 重放 translog  -&gt; 恢复到确认状态  -&gt; 分片重新可用因此 translog 不是可有可无的日志，而是写确认语义的一部分。可靠性取舍：PUT critical-index-v1/_settings{  "index.translog.durability": "request"}            场景      建议                  订单、支付、审计      request              商品搜索、画像      可评估 async              日志与指标      常可评估 async              测试环境      默认或更宽松      即使使用 async，也要确认应用侧有幂等重试，因为网络失败后可能重复写入。24.8 Segment Merge 与写入放大一次业务写入可能引发多层数据搬运：1. 写 Indexing Buffer2. refresh 生成 Segment3. merge 把小 Segment 合成大 Segment4. 副本节点重复 refresh 和 merge5. 更新场景标记 deleted，merge 时清理这就是写入放大。表现通常是：  写入吞吐不稳定；  CPU IO wait 高；  merge 任务排队；  查询延迟上升；  磁盘使用临时上涨。查看 merge 统计：GET _nodes/stats/indices/mergesGET orders-v3/_stats/merges治理方式：  增大刷新间隔；  合理批量写入；  避免高频小 bulk；  避免高频更新同一文档；  控制分片数；  使用 rollover 控制单分片大小；  在低峰执行 force merge；  确认磁盘和页缓存充足。24.9 写入性能模型写入吞吐由以下资源共同决定：            资源      影响                  CPU      分词、脚本、merge、压缩              磁盘 IO      translog fsync、flush、merge              页缓存      Segment 读取和合并效率              网络      副本同步和客户端传输              分片数      并行度与扇出              请求大小      每项固定成本占比              Mapping 复杂度      解析与索引成本      一个实用判断表：            现象      优先检查                  单条写入很慢      客户端同步调用、refresh、副本              Bulk 吞吐低      bulk size、并发、mapping、分片              429 rejected      write thread pool 队列满              延迟毛刺      refresh、flush、merge、磁盘              CPU 高      分词、脚本、merge、查询混部              IO 高      translog、merge、磁盘能力不足              副本延迟      网络、恢复限流、节点负载      24.10 写入背压Elasticsearch 的写入线程池是有限的。当队列满时，请求会被拒绝：GET /_nodes/stats/thread_poolGET /_cat/thread_pool/write?v&amp;h=node_name,name,active,queue,rejected,completed遇到 429 时，客户端应该：  记录失败批次；  指数退避；  降低并发；  检查是否有慢查询抢占资源；  确认磁盘水位和节点是否离线；  评估扩容或拆分集群。不要无脑把线程池队列调大。队列变长只会增加等待时间、占用内存，并让故障雪崩更晚暴露。24.11 幂等写入网络超时不代表写入失败。应用必须支持幂等：  明确指定 _id；  消息系统使用 offset + 批次检查点；  事件携带唯一 event_id；  重放不会产生重复数据；  对部分失败的 Bulk 只重试失败项。示意流程：poll batch  -&gt; 按 item 写入 bulk  -&gt; 成功项记录 offset  -&gt; 失败项进入重试  -&gt; 超过阈值进入死信  -&gt; 恢复后可人工重放如果使用自动生成 _id，重试会写入两个文档。对于日志可能可以容忍，对于订单和支付不可接受。24.12 写入调优清单  批量提交，避免单文档高频写入；  bulk size 和并发通过压测确定；  明确 _id 与幂等策略；  高吞吐索引增大 refresh_interval；  导入期间临时关闭副本并恢复；  避免热点文档高频 update；  脚本简单，参数化，避免每请求编译；  Mapping 控制字段数量和嵌套深度；  监控 write rejected、merge、flush、translog；  磁盘保留足够余量，避免触碰 flood stage；  写入与重查询在高峰期错峰或资源隔离；  429 使用退避和限流，不盲目加大客户端并发。本章小结写入请求经历协调路由、主分片校验、Lucene 写入、translog 持久化、副本同步、refresh 可见、flush 提交和 merge 清理。写成功和可搜索是两个不同时点，可靠性和吞吐之间也始终存在取舍。写入调优的关键是批量、幂等、控制刷新与合并、留足磁盘与页缓存，并让客户端具备背压能力。思考题  为什么写入成功后立刻查询可能看不到？  refresh、flush、translog 分别解决什么问题？  为什么 update 比全新 index 更昂贵？  Bulk 返回 200 时，是否代表所有文档都写入成功？  如果 write 线程池 rejected 快速上升，你会怎么处理客户端和服务端？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 是分布式系统，但这不意味着“加节点就一定快”。真正决定集群能力和稳定性的，是节点角色、分片数量、分片大小、副本、分配策略和查询扇出。本章从架构视角拆解 Elasticsearch 集群。23.1 集群组成一个最小生产集群通常包含：  3 个 master-eligible 节点；  若干 data 节点；  协调节点或负载均衡入口；  Kibana / 监控组件；  快照存储仓库。flowchart TB    C[Client] --&gt; L[Load Balancer]    L --&gt; N1[Coordinating Node]    N1 --&gt; D1[Data Node 1]    N1 --&gt; D2[Data Node 2]    N1 --&gt; D3[Data Node 3]    M1[Master-eligible 1] -. cluster state .-&gt; N1    M2[Master-eligible 2] -. vote .-&gt; M1    M3[Master-eligible 3] -. vote .-&gt; M1集群状态由 master 节点维护，包括索引、分片、节点、别名、模板和分配信息。cluster state 越大，发布和同步成本越高，这是“不要创建几十万小索引”的重要原因。23.2 节点角色查看节点：GET /_cat/nodes?v&amp;h=name,node.role,master,heap.percent,ram.percent,cpu,load_average,disk.used_percent常见角色：            角色      配置      职责                  master-eligible      node.master: true      参与选主，维护集群状态              data      node.data: true      存储分片，执行读写              data_hot      node.attr.data: hot      承接高频写入和查询              data_warm      node.attr.data: warm      承接较旧、低频查询              data_cold      node.attr.data: cold      承接冷数据              data_content      内容数据层      常规业务索引              ingest      node.ingest: true      执行 ingest pipeline              coordinating only      所有 master/data/ingest 为 false      接收请求、分发归并              machine learning      node.ml: true      机器学习作业      节点角色不是越多越好。小集群可以混合部署，大集群应分离 master、data 和 coordinating，避免 master 被重查询或重写入拖垮。23.3 Master 与选主master-eligible 节点通过投票选出 active master。为了避免脑裂，必须满足：master-eligible 数量 &gt;= 2 * 失败节点数 + 1常见选择：            节点数      最多容忍失败数      说明                  1      0      无高可用，仅开发环境              2      0      一台失败就可能无法选出 master              3      1      生产最小推荐              5      2      大集群常用      master 做的事：  维护 cluster state；  创建、删除索引；  分配和迁移分片；  处理节点加入和离开；  发布集群状态变更。master 不直接执行搜索，但集群状态过大会影响所有节点。应避免：  无限创建小索引；  每日索引分片数过大；  大量无用别名和模板；  频繁批量创建删除索引；  让 master 节点同时承担重 data 负载。查看当前 master：GET /_cluster/state/master_node查看未选举原因：GET /_cluster/health?explain=true23.4 分片与路由创建索引时必须认真设置 number_of_shards，主分片数一旦创建不能直接修改：PUT orders-v4{  "settings": {    "number_of_shards": 6,    "number_of_replicas": 1  }}文档路由公式：shard = hash(routing) % number_of_primary_shards默认 routing 是文档 _id。这意味着：  主分片数改变后，同一文档会路由到不同分片；  不能通过 update setting 直接修改主分片数；  需要变更主分片数时，只能 reindex 到新索引；  自定义 routing 可以让同一类文档落同一分片，但可能造成数据倾斜。23.4.1 分片不是越多越好分片多的好处：  数据分布更均匀；  查询可以并行；-单分片恢复更快。分片多的代价：  查询扇出更大；  每个分片都是一个 Lucene 实例，内存和句柄更多；  合并和刷新任务更多；  cluster state 更大；  小分片浪费磁盘和 CPU。经验边界：  单分片常见容量：10GB 到 50GB；  日志类可以放宽到 50GB 左右；  搜索类和频繁更新类建议更小；  面向低延迟查询的索引不宜使用几十上百个分片；  用 rollover 按主分片大小滚动，而不是只按时间滚动。23.4.2 副本副本解决高可用和读吞吐：PUT orders-v4/_settings{  "number_of_replicas": 2}含义：  每个主分片有 2 个副本分片；  总数据分片数 = 主分片数 * (1 + 副本数)；  写入需要同步主副本；  查询可以落在主或副本。副本不是备份。节点损坏后副本能参与恢复，但误删除索引、错误脚本和逻辑损坏仍会同步到副本。真正的备份依赖 snapshot。23.5 分片分配分片分配由 master 决定。常见影响因素：            因素      说明                  节点在线状态      节点离线后分片需要重新分配              磁盘水位      低磁盘水位可能阻止分配和写入              副本数      副本必须与主分片不同节点              allocation filtering      按节点属性迁移分片              awareness      机架或区域感知              shard balancing      节点间分片数量和磁盘均衡              recovery 限流      恢复速度受网络和磁盘限流控制      查看未分配原因：GET /_cluster/allocation/explain查看分片：GET /_cat/shards/orders-v4?v&amp;h=index,shard,prirep,state,docs,store,node,unassigned.reason23.5.1 磁盘水印常见默认值：            水位      默认      行为                  low      85%      不再向该节点分配新分片              high      90%      尝试迁移该节点分片              flood_stage      95%      索引标记 read-only，阻止写入      PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.disk.watermark.low": "85%",    "cluster.routing.allocation.disk.watermark.high": "90%",    "cluster.routing.allocation.disk.watermark.flood_stage": "95%"  }}如果触发 flood stage，需要先清理空间或扩容，再手动解除 read-only：PUT /_all/_settings{  "index.blocks.read_only_allow_delete": null}解除锁只是止血，不是治理。根因通常是分片过多、保留期过长、副本过多或容量规划不足。23.6 集群健康GET /_cluster/health?wait_for_status=yellow&amp;timeout=10s            状态      含义                  green      主分片和副本都正常              yellow      主分片正常，至少一个副本未分配              red      至少一个主分片不可用      查看具体问题：GET /_cat/indices?v&amp;health=redGET /_cat/shards?v&amp;h=index,shard,prirep,state,node,unassigned.reasonGET /_cluster/allocation/explainyellow 不一定立刻影响查询，但表示高可用受损。red 则表示部分数据不可读写，应按索引和分片定位。23.7 冷热架构日志和指标类数据常用冷热分层：node.attr.data: hotnode.roles: [data_hot]node.attr.data: warmnode.roles: [data_warm]索引模板可以指定层：PUT _component_template/logs-allocate{  "template": {    "settings": {      "index.routing.allocation.include._tier_preference": "data_hot"    }  }}Hot 节点：  SSD；  更高 CPU 和内存；  承接写入和最近查询；  更高副本数。Warm 节点：  大容量磁盘；  较低 CPU；  查询频率较低；  可减少副本或使用压缩。Cold 节点：  searchable snapshot 或归档快照；  查询延迟容忍度高；  成本优先。分层的关键是数据时间边界清晰，并且 ILM 能自动 rollover 和迁移。23.8 查询扇出一次搜索请求默认会路由到每个涉及索引的每个分片，每个分片再选择一个主或副本副本执行。6 个索引 x 10 主分片 x 1 副本 = 60 个目标分片查询扇出过大时：  协调节点内存和 CPU 上升；  每个分片结果队列占用内存；  尾部延迟变差；  集群整体吞吐下降；  低效查询被放大。治理方式：  缩小索引模式范围；  使用别名精确指向索引；  查询必须带时间过滤并路由到相关时间分区；  控制分片数量；  使用 pre-filter shard 能力；  大范围分析迁移预聚合或离线集群；  避免通配符查询所有数据。23.9 扩容方式Elasticsearch 扩容分两类：23.9.1 增加副本适合读压力大、数据量不变的场景。副本增加会提高磁盘占用和写入成本。23.9.2 增加数据节点适合磁盘或 CPU 压力大。已有主分片数不变，但集群会重新平衡分片。如果主分片数太少，例如只有 1 个，增加节点无法让写入并行拆分，只能通过 reindex 新建更多分片。扩容步骤：  确认瓶颈是磁盘、CPU、页缓存、IO 还是查询扇出；  预估新节点后的分片分布；  调整 allocation 限流，避免恢复打满网络；  低峰加入节点；  观察恢复速度、磁盘水位和查询延迟；  确认未出现新热点节点。临时禁用分配：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.enable": "none"  }}恢复分配：PUT /_cluster/settings{  "persistent": {    "cluster.routing.allocation.enable": null  }}23.10 架构设计检查清单  master-eligible 节点数量为 3 或 5，且不承担重数据负载；  每个索引的主分片数有容量依据；  单分片大小控制在合理范围；  副本数与可用性要求匹配；  日志和时序数据有 ILM 和 rollover；  查询无法随意扫全量索引；  冷热节点属性和 tier preference 正确；  磁盘水位告警早于 flood stage；  快照仓库可用且定期恢复演练；  cluster state、open indices、shard count 有上限治理。本章小结分片架构决定了 Elasticsearch 的容量上限和故障行为。主分片数一旦确定就难以修改，分片过多会放大查询扇出和集群状态成本，分片过少又会限制并行度和恢复能力。生产设计要从数据增长、查询模式、副本目标和运维成本四方面同时评估。思考题  为什么主分片数不能通过设置直接修改？  3 个 master-eligible 节点和 2 个节点有什么本质差别？  yellow 和 red 分别代表什么风险？  什么情况下增加副本有效，什么情况下应该 reindex？  查询 30 天日志很慢，分片架构上有哪些可能原因？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 的搜索能力来自 Apache Lucene。理解 Lucene 后，很多“看起来很魔法”的现象都会变得自然：为什么写入后要等刷新才能搜到，为什么更新等于删除加写入，为什么过滤不评分更快，为什么磁盘上会同时存在很多小文件，为什么段合并会占用大量 CPU 和 IO。本章聚焦 Lucene 的核心数据结构与存储模型。22.1 Lucene 是什么Lucene 是一个高性能、全功能的倒排索引引擎库。它提供：  文档索引与搜索；  分词和分析器；  倒排索引、列式存储和存储字段；  段合并、事务日志和近实时搜索；  相关性评分和查询执行框架。Lucene 不是分布式数据库。它本身只管理本地索引，不知道节点、分片、副本、集群状态和请求路由，这些由 Elasticsearch 补齐。Elasticsearch  |-- 集群、分片、副本、分配  |-- REST API、安全、SQL、ES|QL  |-- 请求解析、分布式查询归并  +-- Lucene Segment       |-- 倒排索引       |-- Doc Values       |-- Stored Fields       +-- Term Vectors / Points / Vectors 等可选结构一个 Elasticsearch 分片在逻辑上是一个 Lucene 索引。一个分片内部可以有多个 Segment；每个 Segment 是一个不可变的、可独立搜索的小索引。22.2 文档的内部编号Lucene 会给每个 Segment 内的文档分配一个从 0 开始的内部 doc id。这个 id 不是业务文档 _id，而是 Lucene 内部用于定位 posting、Doc Values 和 Stored Fields 的编号。Segment A:doc 0 -&gt; sku_10001doc 1 -&gt; sku_10002doc 2 -&gt; sku_10003Segment B:doc 0 -&gt; sku_10004doc 1 -&gt; sku_10005删除文档时，Lucene 不会立即从 posting list 中移除它，而是记录一个 deleted bitset。查询会先命中 doc id，再过滤已删除文档。这就是“更新文档”的真实流程：update doc 100  -&gt; 标记旧版本 deleted  -&gt; 写入一个新版本到新 segment  -&gt; 查询时旧版本被过滤  -&gt; merge 时删除文档真正物理清除因此高频更新同一个文档会带来额外写入放大：业务以为更新了 1 次，Lucene 可能写入 1 个新文档、标记 1 次删除，并在后台合并时再次搬运数据。22.3 倒排索引倒排索引解决“一个词出现在哪些文档里”。以三个标题为例：            doc      标题分词                  0      elasticsearch, search              1      lucene, search              2      elasticsearch, engine      倒排索引大致为：            Term      Posting List                  elasticsearch      0, 2              search      0, 1              lucene      1              engine      2      Posting List 不只是一串 doc id，还可能包含：  词频 TF；  文档长度或 norm；  词项位置 position；  起始/结束 offset；  payload；  跳表结构，用于快速交集和定位。这些结构支撑了 match、match_phrase、term、bool、span query 和 BM25 评分。22.3.1 Term Dictionary 与 Posting ListTerm Dictionary 保存所有词项及其指向的 posting list。Lucene 通常把词项字典拆成：  .tim：term dictionary；  .tip：term index，FST 结构，常驻内存；  .doc：doc id 与词频；  .pos：词项位置；-.pay`：offset 和 payload。不同 Lucene 版本文件名和布局有差异，不必死记文件名。更重要的是理解访问路径：term -&gt; 内存 FST 定位词典块 -&gt; 磁盘/页缓存读取 term dictionary     -&gt; 找到 posting list -&gt; 按位图或跳表遍历 doc id22.3.2 为什么 term 查询快term 查询不需要分词、不需要扫描所有文档。它只需要在词典中定位一个词项，然后读取其 posting list。GET products-v3/_search{  "query": {    "term": { "brand_id": "brand_1001" }  }}如果 brand_id 是 text，写入时会先分词，查询 brand_1001 也可能被分词，导致精确匹配不可靠。因此 ID、枚举、标签、状态通常应使用 keyword。22.4 Doc Values倒排索引适合“词找文档”，但排序和聚合需要“文档找值”。例如按价格排序：GET products-v3/_search{  "sort": [{ "price": "desc" }]}如果只靠倒排索引，就必须遍历所有价格词项，再收集文档，成本非常高。Doc Values 把字段值按文档编号列式存储，适合排序、聚合和脚本访问。            存储      访问方向      主要用途                  Inverted Index      term -&gt; docs      文本搜索、过滤              Doc Values      doc -&gt; value      排序、聚合、脚本              Stored Fields      doc -&gt; source      返回 _source              Page Cache      磁盘块缓存      近似全内存读性能      默认情况下，多数字段类型会启用 doc_values。如果字段只需要搜索，不需要排序聚合，可以关闭以省磁盘：PUT demo-v1{  "mappings": {    "properties": {      "search_only_keyword": {        "type": "keyword",        "doc_values": false      },      "aggregate_only_keyword": {        "type": "keyword",        "index": false      }    }  }}反过来，只要排序或聚合字段，可以 index: false，减少倒排索引开销。22.5 Stored Fields 与 _sourceElasticsearch 默认把原始 JSON 保存在 _source 中。_source 是 Stored Fields 的一部分，用于：  返回搜索结果；  update / update by query；  reindex；-高亮重建上下文；  数据修复和重建索引。如果不需要返回某个字段，可以设置 stored: false；如果完全不需要 _source，也可以关闭，但必须清楚代价：PUT telemetry-v1{  "mappings": {    "_source": { "enabled": false },    "properties": {      "@timestamp": { "type": "date" },      "host.name": { "type": "keyword" },      "message": { "type": "text" }    }  }}关闭 _source 后：  搜索结果无法返回原始字段；  不能 update；  不能 reindex；  高亮和部分脚本能力受限；  后续补字段需要重新采集数据。日志类数据如果确认永远只查询、不更新、不重建，才考虑关闭 _source。更常见的优化是查询时用 _source filtering，而不是建索引时全局关闭。22.6 Segment 不可变性Segment 一旦写入，就不会修改。不可变性带来几个重要优势：  文件可以被操作系统页缓存长期复用；  数据结构不需要支持原地更新；  并发读不需要加写锁；  压缩率高；  缓存和预读策略简单。代价是：  文档删除只能打标记；  更新等于删除加新写；  查询需要遍历多个 Segment；  小 Segment 会带来句柄、内存和查询开销；  后台合并会消耗 CPU、磁盘 IO 和网络。可以用 API 观察段信息：GET /products-v3/_segments输出中常见字段：            字段      含义                  num_docs      未删除文档数              deleted_docs      已标记删除文档数              size      Segment 磁盘大小              memory_in_bytes      Segment 内部结构占用内存              version      Lucene 版本      如果大量 Segment 只有几十 KB 到几 MB，通常是刷新间隔太短；如果删除文档比例很高，通常是频繁更新或删除后未合并。22.7 Segment MergeSegment Merge 把多个小 Segment 合并成大 Segment，同时物理清除已删除文档。Segment 0 (100 docs, 20 deleted)Segment 1 (200 docs, 0 deleted)Segment 2 (150 docs, 80 deleted)        |        v mergeSegment 3 (350 docs, 0 deleted)合并过程可以理解为：  选择若干小 Segment；  按文档编号归并读取数据；  跳过 deleted 文档；  重写倒排索引、Doc Values、Stored Fields；  构建新 Segment；  原子切换 Segment 列表；  删除旧 Segment 文件。合并策略会综合考虑 Segment 大小、数量、删除比例、磁盘阈值和当前 IO 压力。生产系统不应盲目调小 refresh_interval，否则会产生大量小 Segment，让 CPU 同时承担查询归并和后台合并。_force_merge 会触发强制合并：POST /products-v3/_forcemerge?max_num_segments=1注意：  只适合只读索引、日志保留结束前的低峰操作或重建后的索引；  在持续写入的索引上强制合并，会带来巨大 IO 浪费；  合并期间磁盘占用可能临时上升；  分片数很多时，一次 force merge 可能拖慢整个集群。22.8 TranslogLucene 的写入是近实时，搜索依赖 Segment，但 Segment 刷新前数据还不可见。如果每次写入都生成 Segment，成本太高。事务日志 translog 用于保证未 flush 到 Lucene 的变更在进程崩溃后可恢复。Index API  -&gt; 写入 IndexingBuffer / Lucene 内存  -&gt; 追加 translog  -&gt; 返回客户端refresh:  -&gt; IndexingBuffer 生成新 Segment  -&gt; 打开搜索flush:  -&gt; 将 Lucene 变更提交到磁盘  -&gt; 保留或清空 translogtranslog 的持久化策略：            index.translog.durability      行为                  request      每个请求完成后 fsync，可靠性最高，成本最高              async      每隔 sync_interval 刷盘，吞吐更高，可能丢最近请求      相关参数：PUT orders-v3/_settings{  "index.translog.durability": "request",  "index.translog.sync_interval": "5s",  "index.translog.retention.size": "512mb"}注意不同版本对 translog retention 的支持和默认值有变化。不要只为了写入吞吐把可靠性参数调低，除非业务能接受对应丢失窗口。22.9 近实时搜索Elasticsearch 通常说自己是近实时（NRT），不是强实时。默认 refresh_interval 为 1s。写入成功并不代表立刻可搜，而是最多等待一次 refresh 后可搜。PUT orders-v3/_doc/order_10001?refresh=wait_for{  "order_id": "order_10001",  "amount": 99.00}刷新选项：            参数      含义      适用                  空      不主动刷新，等待周期 refresh      大多数写入              refresh=false      显式不刷新      默认行为等价              refresh=true      请求后立即 refresh      测试、极低频写入              refresh=wait_for      等到下次 refresh 后返回      写完必须可查，又不想主动触发      不要在高吞吐写入中使用 refresh=true。它会把批量写入拆成频繁 refresh，产生大量小 Segment。22.10 查询如何使用这些结构以一个查询为例：GET products-v3/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "轻薄笔记本" } }      ],      "filter": [        { "term": { "status": "on_sale" } },        { "range": { "price": { "lte": 8000 } } }      ]    }  },  "sort": [    { "_score": "desc" },    { "sales_30d": "desc" }  ]}执行时可能用到：  Analyzer 分析查询字符串；  倒排索引找到候选文档和评分信息；  Filter 利用缓存和位图；  遍历多个 Segment 并合并结果；  排序字段读取 Doc Values；  返回 top N 的 _source；  高亮按需从 _source 或 Term Vectors 取文本。这也解释了几个性能原则：  先用 filter 缩小候选集，再做评分；  排序聚合字段保留 Doc Values；  不需要返回的字段做 source filtering；  避免频繁小批量 refresh；  控制分片和 Segment 数量；  深分页会让每个分片维护大量候选文档。22.11 Lucene 与 ES 的边界            能力      Lucene      Elasticsearch                  单机索引      是      通过分片管理              分布式副本      否      是              REST API      否      是              集群选主和分配      否      是              查询归并      单索引内      跨分片              安全与权限      基本无      内置安全体系              段合并      是      调度和限制      Elasticsearch 的性能问题通常分三类：  Lucene 层：索引结构、Segment、Doc Values、页缓存；  分布式层：分片路由、副本、查询扇出、集群状态；  应用层：查询写法、分页、聚合、更新频率和写入并发。排障时先判断问题属于哪一层，再决定看什么指标。本章小结Lucene 通过不可变 Segment、倒排索引、Doc Values、Stored Fields 和 translog，提供了一个可高效搜索和恢复的本地索引引擎。Elasticsearch 在其上构建分片、副本和分布式查询能力。理解这些结构后，刷新、合并、更新成本、过滤缓存和分页开销就不再神秘。思考题  为什么更新一个文档在 Lucene 中是“删除 + 新写入”？  倒排索引和 Doc Values 分别解决什么方向的数据访问？  关闭 _source 能节省磁盘，但会失去哪些能力？  为什么在持续写入索引上执行 _forcemerge 是危险的？  refresh=true 和 refresh=wait_for 的区别是什么？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。订单分析是典型的 OLAP 场景：数据量持续增长，查询以时间范围、维度分组和指标计算为主，写入以追加和状态更新混合。Elasticsearch 可以很好地支持近实时看板和灵活探索，但也必须明确精确性边界和查询成本。本章设计一套订单分析系统，覆盖指标建模、文档设计、聚合查询、漏斗留存、报表加速和上线治理。21.1 分析需求订单团队通常关注四类问题：            类型      问题      示例指标                  规模      今天卖了多少      GMV、订单数、付款人数              转化      用户是否完成下单付款      浏览-加购-下单-付款漏斗              结构      哪些品类贡献最大      类目 GMV、品牌 GMV、客单价              质量      用户是否留存      复购率、7 日留存、退款率      先定义核心指标：  GMV：支付成功订单的金额汇总；  订单数：去重订单 ID 数量；  客单价：GMV / 支付人数；  支付转化率：支付订单数 / 下单订单数；  复购率：周期内下单次数大于 1 的用户占比；  退款率：退款金额 / 支付金额。指标必须写清口径，尤其是时间字段用下单时间还是支付时间、金额是否含运费、是否剔除退款、是否只看有效门店。21.2 订单文档设计订单事件有几种建模方式：            方式      特点      适合场景                  订单快照      每次更新覆盖一个订单文档      查当前状态简单              事件流      每个状态变化一条事件      可回放历史，但查询复杂              快照 + 事件      当前订单一个索引，状态事件一个索引      兼顾看板与审计      推荐主看板使用订单快照，审计和漏斗使用事件流。PUT orders-v3{  "settings": {    "number_of_shards": 6,    "number_of_replicas": 1,    "refresh_interval": "5s"  },  "mappings": {    "dynamic": "strict",    "properties": {      "order_id": { "type": "keyword" },      "user_id": { "type": "keyword" },      "order_status": { "type": "keyword" },      "pay_status": { "type": "keyword" },      "channel": { "type": "keyword" },      "platform": { "type": "keyword" },      "shop_id": { "type": "keyword" },      "city_id": { "type": "keyword" },      "category_id": { "type": "keyword" },      "brand_id": { "type": "keyword" },      "product_id": { "type": "keyword" },      "sku_id": { "type": "keyword" },      "quantity": { "type": "integer" },      "original_amount": { "type": "scaled_float", "scaling_factor": 100 },      "pay_amount": { "type": "scaled_float", "scaling_factor": 100 },      "discount_amount": { "type": "scaled_float", "scaling_factor": 100 },      "refund_amount": { "type": "scaled_float", "scaling_factor": 100 },      "created_at": { "type": "date" },      "paid_at": { "type": "date" },      "completed_at": { "type": "date" },      "first_order_flag": { "type": "boolean" },      "is_test_order": { "type": "boolean" },      "updated_at": { "type": "date" }    }  }}其他设计要点：  订单文档 ID 使用 order_id，幂等重写不会产生重复订单；  明细如果需要按 SKU 分析，建议一条订单明细一个文档，而不是把 SKU 放进 nested；  时间字段分别保留，便于按业务口径切换；  测试订单、内部订单必须打标，默认聚合过滤；  金额使用 scaled_float 保留分。21.3 状态更新与幂等订单会经历创建、支付、发货、完成、退款等状态。推荐由订单消费服务统一处理 Kafka 消息，并按事件版本更新。POST orders-v3/_update/order_10001?retry_on_conflict=3{  "doc": {    "order_status": "paid",    "pay_status": "success",    "pay_amount": 5999.00,    "paid_at": "2026-08-25T10:12:33Z",    "updated_at": "2026-08-25T10:12:35Z"  }}为了防止乱序消息覆盖新状态，可以在文档里保存事件版本或状态时间：POST orders-v3/_update/order_10001?retry_on_conflict=3{  "script": {    "lang": "painless",    "source": """      if (ctx._source.event_version == null || ctx._source.event_version &lt; params.version) {        ctx._source.order_status = params.status;        ctx._source.updated_at = params.updated_at;        ctx._source.event_version = params.version;      }    """,    "params": {      "version": 12,      "status": "completed",      "updated_at": "2026-08-25T12:00:00Z"    }  }}如果更新非常频繁，可以降低同步频率或使用局部状态表异步合并，再定期刷新到 ES。21.4 GMV 与订单数按小时统计 GMV：GET orders-v3/_search{  "size": 0,  "query": {    "bool": {      "filter": [        { "term": { "pay_status": "success" } },        { "term": { "is_test_order": false } },        { "range": { "paid_at": { "gte": "now-1d/d", "lte": "now" } } }      ]    }  },  "aggs": {    "per_hour": {      "date_histogram": {        "field": "paid_at",        "fixed_interval": "1h",        "min_doc_count": 0,        "time_zone": "Asia/Shanghai"      },      "aggs": {        "gmv": { "sum": { "field": "pay_amount" } },        "orders": { "cardinality": { "field": "order_id" } },        "buyers": { "cardinality": { "field": "user_id" } }      }    }  }}如果一条文档就是一个订单且 order_id 是文档 ID，value_count 比 cardinality 更便宜。cardinality 是近似算法，用户量很大时需要在准确率和内存成本之间取舍。21.5 维度分析按类目统计 GMV、退款率和客单价：GET orders-v3/_search{  "size": 0,  "query": {    "bool": {      "filter": [        { "term": { "pay_status": "success" } },        { "range": { "paid_at": { "gte": "now-7d/d", "lte": "now" } } }      ]    }  },  "aggs": {    "categories": {      "terms": {        "field": "category_id",        "size": 50,        "order": { "gmv": "desc" }      },      "aggs": {        "gmv": { "sum": { "field": "pay_amount" } },        "refund": { "sum": { "field": "refund_amount" } },        "buyers": { "cardinality": { "field": "user_id" } },        "refund_rate": {          "bucket_script": {            "buckets_path": {              "refund": "refund",              "gmv": "gmv"            },            "script": "params.refund / params.gmv * 100"          }        },        "avg_order_value": {          "bucket_script": {            "buckets_path": {              "gmv": "gmv",              "buyers": "buyers"            },            "script": "params.buyers == 0 ? 0 : params.gmv / params.buyers"          }        }      }    }  }}注意 bucket_script 不能用于已排序的 terms 聚合作为排序依据时破坏近似语义，具体取决于版本。生产报表如果要严格排序，建议先取足够大的 size，再由应用层或预聚合表完成最终排序。21.6 转化漏斗漏斗需要事件数据，例如 order_events-v1：PUT order_events-v1/_doc/event_001{  "event_id": "event_001",  "user_id": "u_1001",  "event_type": "submit_order",  "order_id": "order_10001",  "event_time": "2026-08-25T10:00:00Z"}简单漏斗可以用 filter 聚合实现：GET order_events-v1/_search{  "size": 0,  "query": {    "range": {      "event_time": { "gte": "now-1d/d", "lte": "now" }    }  },  "aggs": {    "users": {      "filter": { "term": { "event_type": "view_product" } },      "aggs": {        "view_users": {          "cardinality": { "field": "user_id" }        },        "add_cart_users": {          "filter": { "term": { "event_type": "add_cart" } },          "aggs": {            "distinct_users": {              "cardinality": { "field": "user_id" }            }          }        },        "submit_users": {          "filter": { "term": { "event_type": "submit_order" } },          "aggs": {            "distinct_users": {              "cardinality": { "field": "user_id" }            }          }        },        "pay_users": {          "filter": { "term": { "event_type": "pay_success" } },          "aggs": {            "distinct_users": {              "cardinality": { "field": "user_id" }            }          }        }      }    }  }}            这种写法只适合简单口径。若要求“先浏览、后加购、再支付”的顺序，以及窗口期约束，通常更适合使用事件分析引擎、数据仓库 SQL，或 ES      QL 的 sequence 能力。      21.7 留存分析留存分析先确定 cohorts：  注册日期、首次购买日期、活动参与日期；  观察 N 天内是否再次购买；  按渠道、城市、品类或用户等级拆分。一种常见做法是在用户宽表中冗余首购日期和最近购买日期：PUT user_order_profiles-v1/_doc/u_1001{  "user_id": "u_1001",  "first_paid_at": "2026-08-01T10:00:00Z",  "last_paid_at": "2026-08-20T09:00:00Z",  "order_count_30d": 3,  "pay_amount_30d": 899.00,  "category_id": "category_1",  "channel": "app"}按首购月份和购买次数分层：GET user_order_profiles-v1/_search{  "size": 0,  "aggs": {    "first_paid_month": {      "date_histogram": {        "field": "first_paid_at",        "calendar_interval": "month",        "time_zone": "Asia/Shanghai"      },      "aggs": {        "repeat_users": {          "range": {            "field": "order_count_30d",            "ranges": [              { "to": 2 },              { "from": 2, "to": 5 },              { "from": 5 }            ]          }        }      }    }  }}严格留存需要完整事件历史，通常由离线数仓计算后写入 ES，供看板查询。21.8 报表加速当看板查询同时满足这些条件时，就该考虑预聚合：  查询范围经常覆盖 30 天以上；  维度组合固定；  查询 QPS 较高；  原始分片查询延迟或 CPU 明显偏高；  用户能接受分钟级数据延迟。预聚合表设计：PUT order_reports_1m-v1{  "mappings": {    "dynamic": "strict",    "properties": {      "stat_time": { "type": "date" },      "channel": { "type": "keyword" },      "category_id": { "type": "keyword" },      "city_id": { "type": "keyword" },      "gmv": { "type": "double" },      "order_count": { "type": "long" },      "buyer_count": { "type": "long" },      "refund_amount": { "type": "double" },      "updated_at": { "type": "date" }    }  }}文档 ID 可以设计为：stat_time + channel + category_id + city_id例如 202608251010|app|category_1|city_1。这样每分钟重复计算可以幂等覆盖，不会产生重复统计。21.9 精确性边界Elasticsearch 聚合默认为分布式近似设计，需要清楚边界：            能力      精确性      建议                  sum / min / max / avg      数值精确      适合订单金额              terms      可能截断      设置合理 size 和 shard_size              cardinality      近似      对账时使用精确去重或离线计算              percentile      近似      用于观察分布，不用于财务结算              top_hits      每个 bucket 内部结果      不等价于全局 Top N      财务结算、发票、佣金、对账等场景不要用 Elasticsearch 作为最终依据。它适合看板和探索，账实核对应以交易数据库或数仓为准。21.10 上线清单  所有指标都有明确定义、负责人和口径文档；  订单写入有幂等键，消息乱序可处理；  测试订单和退款订单默认排除规则明确；  高频看板已评估预聚合；  大范围查询有索引分区和 time filter；  高基数聚合有白名单；  cardinality 精度与内存已评估；  查询延迟和查询淘汰有监控；  财务级指标与离线数仓定期比对；  集群压力过大时可切换只读快照或降级查询。本章小结订单分析的重点是指标口径、文档粒度、时间维度和查询成本。当前状态看板适合订单快照，转化和留存适合事件数据，长期高频报表适合预聚合。把精确性边界讲清楚，比追求所有查询都实时更重要。思考题  订单快照、订单事件、订单明细三种文档分别适合什么问题？  为什么财务结算不能依赖 cardinality 和 percentile？  如果 GMV 看板查询 30 天数据很慢，你会如何优化？  订单状态乱序到达时，如何避免旧状态覆盖新状态？  预聚合表的文档 ID 应该怎么设计，才能支持幂等更新？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。日志检索是 Elasticsearch 另一个主流场景。它和商品搜索的关注点完全不同：商品搜索追求相关性，日志检索追求稳定接入、低成本存储、快速定位异常，以及清晰的生命周期治理。本章从日志模型、采集管道、索引模板、查询排障到权限治理，搭建一套可落地的日志系统。20.1 日志系统的需求一个生产日志平台通常要回答这些问题：            问题      示例      依赖能力                  服务是否报错      某服务近 15 分钟 ERROR 日志      时间过滤、级别过滤              请求在哪里失败      trace_id 串联多个服务      keyword 精确查找              影响多少用户      某错误影响的 UID 数      去重聚合              什么时候开始      错误按分钟分布      date_histogram              是否已恢复      错误率与成功率对比      指标日志、聚合              如何审计      谁查询了敏感日志      权限与审计      日志系统的常见挑战：  写入峰值高，故障时日志量可能放大 5 到 10 倍；  保存周期长，磁盘成本容易失控；  字段不规范，排障时会遇到同一个含义多个字段名；  日志包含敏感信息，需要脱敏和权限控制；  查询时间范围过大时，容易造成集群压力。20.2 日志数据模型推荐统一使用 ECS（Elastic Common Schema）风格字段。字段规范越早统一，后期排障成本越低。PUT _index_template/app-logs{  "index_patterns": ["app-logs-*"],  "data_stream": {},  "template": {    "settings": {      "index.default_pipeline": "app-logs-pipeline",      "index.refresh_interval": "5s",      "index.number_of_shards": 3,      "index.number_of_replicas": 1,      "index.codec": "best_compression",      "index.sort.field": ["@timestamp", "service.name"],      "index.sort.order": ["desc", "asc"]    },    "mappings": {      "dynamic": false,      "properties": {        "@timestamp": { "type": "date" },        "data_stream.type": { "type": "constant_keyword", "value": "logs" },        "data_stream.dataset": { "type": "constant_keyword", "value": "app" },        "data_stream.namespace": { "type": "keyword" },        "service.name": { "type": "keyword" },        "service.environment": { "type": "keyword" },        "service.version": { "type": "keyword" },        "host.name": { "type": "keyword" },        "container.id": { "type": "keyword" },        "log.level": { "type": "keyword" },        "log.logger": { "type": "keyword" },        "message": { "type": "text" },        "trace.id": { "type": "keyword" },        "span.id": { "type": "keyword" },        "request.id": { "type": "keyword" },        "user.id": { "type": "keyword" },        "http.method": { "type": "keyword" },        "http.status_code": { "type": "short" },        "http.route": { "type": "keyword" },        "event.duration": { "type": "long" },        "error.type": { "type": "keyword" },        "error.message": { "type": "text" },        "geo.ip": { "type": "ip" },        "labels": { "type": "object", "enabled": false }      }    }  }}设计要点：  使用 Data Stream 管理只追加的日志数据，避免手工维护每日索引；  service.name、log.level、trace.id 都是 keyword，适合精确过滤；  message 保留全文检索；  labels 关闭索引，避免业务标签无限膨胀破坏 Mapping；  时间字段统一为 @timestamp，查询、告警和 ILM 都依赖它。20.3 Ingest PipelineIngest Pipeline 可以完成字段规整、类型转换、地理解析、失败处理和脱敏。PUT _ingest/pipeline/app-logs-pipeline{  "processors": [    {      "set": {        "field": "data_stream.type",        "value": "logs"      }    },    {      "set": {        "field": "data_stream.dataset",        "value": "app"      }    },    {      "date": {        "field": "time",        "target_field": "@timestamp",        "formats": ["ISO8601", "UNIX_MS"],        "timezone": "Asia/Shanghai",        "on_failure": [          {            "set": {              "field": "event.parse_error",              "value": "invalid_time"            }          },          {            "set": {              "field": "@timestamp",              "value": "{{{_ingest.timestamp}}}"            }          }        ]      }    },    {      "lowercase": { "field": "log.level", "ignore_missing": true }    },    {      "script": {        "lang": "painless",        "source": """          if (ctx.message != null) {            ctx.message = ctx.message              .replaceAll(/\\b\\d{17}[0-9Xx]\\b/, '&lt;id_card&gt;')              .replaceAll(/\\b1[3-9]\\d{9}\\b/, '&lt;mobile&gt;')              .replaceAll(/(?i)authorization\\s*[:=]\\s*\\S+/, 'authorization=&lt;redacted&gt;');          }        """      }    },    {      "remove": {        "field": ["password", "token", "secret"],        "ignore_missing": true      }    }  ]}日志脱敏最好不要完全依赖 Elasticsearch。应用输出、采集器和 ES Pipeline 三层都可以做，越靠近源头做，风险越小；ES Pipeline 适合作为最后兜底。20.4 采集管道常见接入方式有三种。20.4.1 Filebeat 直写适合中小规模：filebeat.inputs:  - type: filestream    id: app-log    paths:      - /var/log/app/*.log    parsers:      - ndjson:          add_error_key: true          message_key: messageoutput.elasticsearch:  hosts: ["https://es01:9200"]  username: "filebeat-writer"  password: "${ES_PASSWORD}"  index: "app-logs"  pipeline: "app-logs-pipeline"setup.template.enabled: false20.4.2 Kafka 缓冲适合大规模和突发流量：flowchart LR    A[应用日志] --&gt; B[Filebeat/OTel Collector]    B --&gt; K[Kafka]    K --&gt; L[Logstash/消费者]    L --&gt; E[Elasticsearch]Kafka 的价值是削峰、解耦和可重放。ES 故障时日志先留在 Kafka，恢复后继续消费，避免日志直接丢失。20.4.3 OpenTelemetry如果团队同时需要日志、指标和链路，OpenTelemetry Collector 是更统一的选择。它能把 trace_id、span_id、service.name 自动带入日志，减少手工埋点不一致。20.5 ILM 生命周期日志最典型的生命周期：PUT _ilm/policy/app-logs-policy{  "policy": {    "phases": {      "hot": {        "min_age": "0ms",        "actions": {          "rollover": {            "max_primary_shard_size": "50gb",            "max_age": "1d"          },          "forcemerge": {            "max_num_segments": 1          }        }      },      "warm": {        "min_age": "2d",        "actions": {          "shrink": {            "number_of_shards": 1          },          "allocate": {            "require": { "data": "warm" }          }        }      },      "cold": {        "min_age": "15d",        "actions": {          "searchable_snapshot": {            "snapshot_repository": "backup-repo"          }        }      },      "delete": {        "min_age": "30d",        "actions": {          "delete": {}        }      }    }  }}使用建议：  Hot 节点保留最近高频查询数据，优先内存和 SSD；  Warm 节点承接较旧数据，可以减少副本、合并段；  Cold 数据可用 searchable snapshot 或快照归档；  Delete 阶段必须与合规要求确认，不要只按磁盘压力决定；  max_primary_shard_size 通常比 max_age 更能保持分片均匀。20.6 查询排障20.6.1 查询某条链路GET logs-app-default/_search{  "size": 100,  "query": {    "bool": {      "filter": [        { "term": { "trace.id": "4bf92f3577b34da6a3ce929d0e0e4736" } },        { "range": { "@timestamp": { "gte": "now-2h", "lte": "now" } } }      ]    }  },  "sort": [{ "@timestamp": "asc" }]}20.6.2 查询服务错误GET logs-app-default/_search{  "size": 50,  "query": {    "bool": {      "filter": [        { "term": { "service.name": "order-service" } },        { "terms": { "log.level": ["error", "fatal"] } },        { "range": { "@timestamp": { "gte": "now-15m" } } }      ]    }  },  "sort": [{ "@timestamp": "desc" }]}20.6.3 查看错误分布和影响用户GET logs-app-default/_search{  "size": 0,  "query": {    "bool": {      "filter": [        { "term": { "service.name": "order-service" } },        { "term": { "log.level": "error" } },        { "range": { "@timestamp": { "gte": "now-1h" } } }      ]    }  },  "aggs": {    "per_minute": {      "date_histogram": {        "field": "@timestamp",        "fixed_interval": "1m",        "min_doc_count": 0      },      "aggs": {        "users": {          "cardinality": { "field": "user.id" }        }      }    },    "top_errors": {      "terms": { "field": "error.type", "size": 10 }    }  }}排障时尽量使用 filter，不要无必要地把日志级别、服务名、时间范围放进 must。这些条件不需要评分，放进 filter 可以利用缓存，也让查询语义更清晰。20.7 控制查询成本日志查询最容易出问题的不是单条查询，而是“无时间范围的聚合”和“所有人都能查 30 天”。治理建议：  所有查询强制带 @timestamp 范围；  默认查询最近 15 分钟，扩大范围需要用户主动选择；  Dashboard 默认刷新间隔不要小于 1 分钟；  高基数字段不要做 terms 大 size 聚合；  避免对 message 做大规模聚合或排序；  保存常用 query template，减少临时复杂 DSL；  对 Kibana 用户设置并发、超时和索引权限；  对特别重的分析任务迁移到离线集群或数据仓库。20.8 权限与审计日志包含用户 ID、IP、请求参数甚至异常堆栈中的敏感数据。权限设计要同时控制“能查什么索引”和“能看到什么字段”。POST /_security/role/logs_order_reader{  "indices": [    {      "names": ["logs-app-order-*"],      "privileges": ["read"],      "field_security": {        "grant": [          "*",          "geo.ip",          "user.id"        ],        "except": ["authorization", "request.headers"]      }    }  ]}实际权限策略要遵循最小权限：  业务开发只能看本服务相关数据集；  生产日志查询需要审批或审计；  含用户手机号、身份证、token 的字段默认不可见；  客服、研发、安全团队使用不同角色；  审计日志单独保存，不能被普通用户删除；  ES 节点间、客户端到 ES 全部启用 TLS。20.9 上线清单  字段规范已统一，团队有新增字段评审流程；  时间解析失败时有兜底和错误标记；  索引模板使用 Data Stream 和 ILM；  写入端有限流、重试和背压；  Kafka 消费有 lag 监控和死信队列；  高峰写入和高峰查询已经压测；  查询默认带时间范围；  敏感字段已脱敏，角色已收敛；  磁盘水位、写入拒绝、查询延迟、消费延迟有告警；  定期演练节点故障、磁盘告警和快照恢复。本章小结日志系统不是把日志写进 Elasticsearch 就结束了。稳定的数据模型、统一的时间字段、受控的采集链路、合理的生命周期、强制的时间范围和最小权限，共同决定了排障效率和长期成本。思考题  为什么日志索引推荐用 Data Stream，而不是普通索引？  trace.id 为什么必须是 keyword？如果误建成 text 会发生什么？  Filebeat 直写和 Kafka 中转各适合什么规模？  ILM 的 Hot/Warm/Cold/Delete 阶段分别解决什么问题？  如果用户反馈“日志查询很慢”，你会先检查哪五项？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。商品搜索是 Elasticsearch 最经典的应用场景。它不只是“能搜到商品”，而是一个完整的搜索产品：要理解用户输入，召回合适商品，过滤不可售商品，提供筛选项，给出符合业务目标的排序，并能持续治理坏结果。本章以一个电商商品搜索为例，完成从需求分析、文档设计、查询构造到上线治理的完整流程。19.1 需求拆解一个典型的商品搜索页面包含五个区域：            区域      内容      Elasticsearch 能力                  搜索框      关键词、品牌、规格、错别字、同义词      Analyzer、multi_match、synonym              结果列表      商品卡片、价格、销量、标签      Query DSL、function_score、source filtering              筛选区      品牌、分类、价格、属性、库存      term、range、bool filter、aggregation              排序区      综合、销量、价格、上新      sort、function_score、业务评分              搜索运营      置顶、屏蔽、活动加权      运营文档、filter、衰减函数      在设计前先明确几个问题：  搜索的是 SKU 还是 SPU？两者更新频率和展示粒度不同。  是否要聚合库存、价格区间、销量分布？  是否要支持拼音、错别字、同义词和多语言？  不可售商品是物理删除，还是搜索时过滤？  排序目标是什么：成交、点击、毛利，还是用户体验？没有这些答案，搜索系统很容易变成“接口能用，业务不满意”。19.2 商品文档设计本章选择以 SKU 为搜索主文档，并冗余 SPU 级信息。这样可以直接返回商品卡片，也能按品牌、类目、销量过滤。PUT products-v3{  "settings": {    "number_of_shards": 3,    "number_of_replicas": 1,    "refresh_interval": "1s",    "analysis": {      "filter": {        "product_synonym": {          "type": "synonym_graph",          "synonyms": [            "手机,智能手机,移动电话",            "电脑,笔记本,laptop",            "显视器,显示器"          ]        }      },      "analyzer": {        "product_search_analyzer": {          "type": "custom",          "tokenizer": "ik_max_word",          "filter": ["lowercase", "cjk_width", "product_synonym"]        },        "product_index_analyzer": {          "type": "custom",          "tokenizer": "ik_max_word",          "filter": ["lowercase", "cjk_width"]        }      }    }  },  "mappings": {    "dynamic": "strict",    "properties": {      "product_id": { "type": "keyword" },      "sku_id": { "type": "keyword" },      "title": {        "type": "text",        "analyzer": "product_index_analyzer",        "search_analyzer": "product_search_analyzer",        "fields": {          "keyword": { "type": "keyword", "ignore_above": 256 }        }      },      "brand_id": { "type": "keyword" },      "brand_name": {        "type": "text",        "analyzer": "product_index_analyzer",        "fields": { "keyword": { "type": "keyword" } }      },      "category_id": { "type": "keyword" },      "category_path": { "type": "keyword" },      "price": { "type": "scaled_float", "scaling_factor": 100 },      "original_price": { "type": "scaled_float", "scaling_factor": 100 },      "sales_30d": { "type": "long" },      "sales_7d": { "type": "long" },      "comment_count": { "type": "long" },      "good_comment_rate": { "type": "float" },      "stock": { "type": "integer" },      "status": { "type": "keyword" },      "shop_id": { "type": "keyword" },      "attrs": {        "type": "nested",        "properties": {          "name": { "type": "keyword" },          "value": { "type": "keyword" },          "value_text": { "type": "text", "copy_to": "combined_text" }        }      },      "tags": { "type": "keyword" },      "keywords": { "type": "text", "analyzer": "product_index_analyzer" },      "combined_text": { "type": "text", "analyzer": "product_index_analyzer" },      "on_shelf_at": { "type": "date" },      "updated_at": { "type": "date" },      "weight": { "type": "double" },      "embedding": {        "type": "dense_vector",        "dims": 768,        "index": true,        "similarity": "cosine"      }    }  }}几个关键设计：  title、keywords、combined_text 分工不同：标题强相关，运营词负责补召回，组合字段兜底。  attrs 使用 nested，避免“颜色=红色 AND 尺寸=XL”这类条件串到同一商品的其他 SKU 上。  价格使用 scaled_float，避免浮点展示和过滤边界不稳定。  status 保留商品生命周期，不可售商品通常过滤而不是立即删除。  embedding 是可选能力，适合在关键词搜索稳定后再引入。19.3 召回策略搜索请求通常由三层召回组成：用户输入  |  |-- 文本召回：title / brand / keywords / combined_text  |-- 结构化召回：类目、品牌、属性、价格、库存  |-- 向量召回：标题、类目、商品图片或详情的语义向量  |  +-- 合并去重 -&gt; 业务过滤 -&gt; 排序 -&gt; 返回基础查询可以先用 bool 表达：GET products-v3/_search{  "query": {    "bool": {      "must": [        {          "multi_match": {            "query": "轻薄笔记本",            "fields": [              "title^4",              "keywords^2",              "brand_name^1.5",              "combined_text^1"            ],            "type": "best_fields",            "operator": "and",            "fuzziness": "AUTO"          }        }      ],      "filter": [        { "term": { "status": "on_sale" } },        { "range": { "stock": { "gt": 0 } } }      ]    }  }}如果 operator: and 导致召回过少，可以降级为 or，并用 minimum_should_match 控制匹配强度：{  "multi_match": {    "query": "轻薄笔记本 16G 独显",    "fields": ["title^4", "keywords^2", "combined_text"],    "type": "best_fields",    "minimum_should_match": "2&lt;75%"  }}中文搜索常见问题是同义词放在索引阶段还是搜索阶段。推荐默认放在搜索阶段：  索引阶段同义词：召回稳定，查询成本低，但词表更新必须重建索引；  搜索阶段同义词：词表可即时生效，但查询会扩展更多词项，成本更高。19.4 筛选与聚合搜索页的筛选条件要同时支持“过滤当前结果”和“展示可选聚合值”。这两者并不总是同一个请求。下面的查询展示：搜索“笔记本”，过滤品牌和价格，同时返回品牌、价格区间和属性聚合。GET products-v3/_search{  "size": 20,  "query": {    "bool": {      "must": [        {          "multi_match": {            "query": "笔记本",            "fields": ["title^4", "keywords^2", "combined_text"]          }        }      ],      "filter": [        { "term": { "status": "on_sale" } },        { "term": { "brand_id": "brand_1001" } },        { "range": { "price": { "gte": 3000, "lte": 8000 } } }      ]    }  },  "aggs": {    "category": {      "terms": { "field": "category_id", "size": 20 }    },    "price_ranges": {      "range": {        "field": "price",        "ranges": [          { "to": 3000 },          { "from": 3000, "to": 5000 },          { "from": 5000, "to": 8000 },          { "from": 8000 }        ]      }    },    "attrs": {      "nested": { "path": "attrs" },      "aggs": {        "names": {          "terms": { "field": "attrs.name", "size": 20 },          "aggs": {            "values": {              "terms": { "field": "attrs.value", "size": 20 }            }          }        }      }    }  }}注意：如果希望筛选项统计“应用当前筛选前的搜索结果”，需要把聚合放到 post_filter 外侧，或者单独发送聚合请求。post_filter 只影响命中结果，不影响聚合。GET products-v3/_search{  "size": 20,  "query": {    "multi_match": {      "query": "笔记本",      "fields": ["title^4", "keywords^2", "combined_text"]    }  },  "aggs": {    "brands": {      "terms": { "field": "brand_id", "size": 20 }    }  },  "post_filter": {    "bool": {      "filter": [        { "term": { "brand_id": "brand_1001" } },        { "range": { "price": { "gte": 3000, "lte": 8000 } } }      ]    }  }}19.5 排序设计综合排序通常不是纯 _score，而是相关性、销量、信誉、库存、上新时间和商业因子的组合。GET products-v3/_search{  "query": {    "function_score": {      "query": {        "bool": {          "must": [            {              "multi_match": {                "query": "笔记本",                "fields": ["title^4", "keywords^2", "combined_text"]              }            }          ],          "filter": [            { "term": { "status": "on_sale" } }          ]        }      },      "functions": [        {          "filter": { "term": { "tags": "hot" } },          "weight": 1.3        },        {          "gauss": {            "on_shelf_at": {              "origin": "now",              "scale": "30d",              "decay": 0.5            }          },          "weight": 1.1        },        {          "field_value_factor": {            "field": "sales_30d",            "modifier": "log1p",            "factor": 1.2,            "missing": 0          }        }      ],      "score_mode": "sum",      "boost_mode": "multiply"    }  }}排序公式要可解释、可回放、可灰度。建议把每次搜索请求的排序版本、特征值和最终得分写入日志，方便排查“为什么这个商品排第一”。19.6 搜索运营能力搜索运营可以单独设计一个索引，例如 search_rules-v1：PUT search_rules-v1/_doc/rule_10001{  "rule_id": "rule_10001",  "query": "手机",  "type": "top",  "sku_ids": ["sku_1", "sku_2", "sku_3"],  "start_time": "2026-08-01T00:00:00Z",  "end_time": "2026-08-31T23:59:59Z",  "enabled": true}搜索服务先按关键词查询规则，再决定是否追加置顶或屏蔽列表：  置顶：单独查一批运营商品，拼在自然结果前；  沉底：用 must_not 或降权处理；  换词：用户搜索“番茄”时补充“西红柿”；  类目定向：把词路由到某个类目，减少错误召回；  活动加权：只影响排序，不影响可见性，避免搜索结果突兀。置顶结果建议不要硬编码进主查询的 should 里。单独查询更容易控制位置、数量和过期时间。19.7 无结果与坏结果治理搜索质量治理至少要看四类指标：            指标      含义      常见原因                  无结果率      搜索后结果为空的比例      分词错误、词表缺失、过滤条件过严              点击率      有结果但用户不点击      排序错误、标题劣化、价格不合适              换词率      用户快速改写查询      召回不相关、类目识别错误              首屏转化      首屏商品带来的下单比例      排序与业务目标不匹配      无结果降级链路：精确策略  -&gt; 同义词扩展  -&gt; AND 改 OR  -&gt; 去掉低置信度过滤  -&gt; 类目或热销商品兜底  -&gt; 明确提示无结果并推荐搜索词坏结果治理流程：  收集用户反馈、客服工单和搜索日志；  按 query 分组，统计曝光、点击、下单和换词；  复现请求，查看 _score、命中字段和分词结果；  判断是召回问题、过滤问题还是排序问题；  修改词表、Mapping、权重或规则；  用固定评测集回归，避免修复一个 query 损坏一批 query。可以用 _explain 分析单条商品得分：GET products-v3/_explain/sku_10001{  "query": {    "match": { "title": "轻薄笔记本" }  }}19.8 写入与更新链路商品数据通常来自商品主库、库存服务、价格服务、评价服务和搜索运营系统。推荐使用消息队列解耦：flowchart LR    A[商品主库] --&gt; K[Kafka]    B[价格/库存] --&gt; K    C[评价统计] --&gt; K    D[运营平台] --&gt; K    K --&gt; W[搜索同步服务]    W --&gt; E[Elasticsearch]    W --&gt; F[(本地检查点)]同步策略：  商品主信息变更：全量替换 SKU 文档；  库存高频变更：合并更新，必要时只在 0 与非 0 之间变化时同步；  销量统计：分钟级聚合后更新，避免每个订单都打一次 ES；  下架商品：保留文档并更新 status，便于审计和快速重新上架；  词表和 Mapping 变更：新索引重建，通过别名切换。19.9 上线清单  明确 SKU/SPU 建模口径，避免结果重复或遗漏；  status、库存、类目、品牌等过滤条件必须有默认值；  标题、运营词、组合字段的 Analyzer 已经过真实 query 验证；  同义词词表有版本管理和回滚机制；  聚合字段关闭 doc_values 的情况已确认；  不可售商品不会进入默认结果；  搜索接口有超时、降级和限流；  无结果率、慢查询率、空聚合率有监控；  排序版本可灰度、可回滚；  建立固定评测集，任何相关性改动都跑回归。本章小结商品搜索的核心不是一条复杂 DSL，而是一条完整链路：文档建模决定能搜什么，分词和召回决定能找到什么，过滤和聚合决定页面能用什么，排序决定用户先看到什么，监控和评测决定系统能不能持续变好。思考题  为什么商品属性适合用 nested，而不是把属性拍平成对象数组？  如果某个关键词无结果率突然升高，你会按什么顺序排查？  post_filter 和普通 filter 在搜索页中的区别是什么？  搜索阶段同义词和索引阶段同义词各适合什么场景？  如何证明一次排序优化是有效的，而不是只让某个 query 看起来更好？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 中已有字段的类型和分词器不能直接修改。要调整 Mapping，通常需要创建新索引、迁移数据、验证一致性，再通过别名切换。本章讲解别名、Reindex、Mapping 变更、双写、灰度切换、回滚和数据校验。18.1 为什么需要别名不推荐应用直接写死物理索引：products_v1推荐使用别名：products_read -&gt; products_v1products_write -&gt; products_v1别名价值：  应用与物理索引解耦；  支持 Mapping 变更；  支持分片数调整；  支持灰度和回滚；  一个读别名可以查询多个索引；  写别名必须指向唯一写索引。18.2 创建别名创建索引时添加：PUT /products_v1{  "aliases": {    "products_read": {},    "products_write": { "is_write_index": true }  }}后添加：POST /_aliases{  "actions": [    {      "add": {        "index": "products_v1",        "alias": "products_read"      }    },    {      "add": {        "index": "products_v1",        "alias": "products_write",        "is_write_index": true      }    }  ]}查看：GET /_cat/aliases/products*?v18.3 过滤别名POST /_aliases{  "actions": [    {      "add": {        "index": "products_v1",        "alias": "products_on_sale",        "filter": {          "term": { "status": "ON_SALE" }        }      }    }  ]}查询 products_on_sale 时自动附加状态过滤。过滤别名适合逻辑视图，但权限控制仍应使用集群安全能力，而不是只依赖别名。18.4 Reindex创建新索引：PUT /products_v2{  "settings": {    "number_of_shards": 6,    "number_of_replicas": 0  },  "mappings": {    "properties": {      "title": {        "type": "text",        "analyzer": "ik_max_word"      },      "brand": { "type": "keyword" },      "price": { "type": "scaled_float", "scaling_factor": 100 }    }  }}迁移数据：POST /_reindex?wait_for_completion=false{  "source": {    "index": "products_v1",    "size": 1000  },  "dest": {    "index": "products_v2"  }}返回任务 ID：{  "task": "oTUltX4IQMOUUVeiohTt8A:12345"}查看任务：GET /_tasks/oTUltX4IQMOUUVeiohTt8A:12345取消任务：POST /_tasks/oTUltX4IQMOUUVeiohTt8A:12345/_cancel18.5 Reindex 条件与脚本只迁移部分数据：POST /_reindex{  "source": {    "index": "products_v1",    "query": {      "range": {        "updated_at": { "gte": "2026-01-01" }      }    }  },  "dest": {    "index": "products_v2"  }}字段转换：POST /_reindex{  "source": { "index": "orders_v1" },  "dest": { "index": "orders_v2" },  "script": {    "source": """      ctx._source.amount = ctx._source.amount_cents / 100.0;      ctx._source.remove('amount_cents');    """  }}18.6 零停机变更流程1. 保留旧索引 products_v1 和写别名2. 创建新索引 products_v23. 启动 Reindex 迁移存量4. 开启双写或 CDC 同步增量5. 对比 v1/v2 数量和抽样数据6. 用读别名灰度切流量7. 观察查询结果和性能8. 切写别名到 v29. 保留 v1 一段时间用于回滚10. 下线 v1切换读别名：POST /_aliases{  "actions": [    { "remove": { "index": "products_v1", "alias": "products_read" } },    { "add": { "index": "products_v2", "alias": "products_read" } }  ]}切换写别名：POST /_aliases{  "actions": [    { "remove": { "index": "products_v1", "alias": "products_write" } },    { "add": { "index": "products_v2", "alias": "products_write", "is_write_index": true } }  ]}18.7 双写与 CDC双写应用写 MySQL-&gt; 同步写 products_v1-&gt; 同步写 products_v2优点是延迟低；缺点是侵入业务、两份数据可能不一致、失败补偿复杂。CDCMySQL binlog -&gt; Kafka -&gt; Consumer  -&gt; products_v1  -&gt; products_v2优点是与应用解耦、可回放、失败可重试。切换完成后只需保留 v2 消费者。推荐策略大多数系统推荐：事实源 MySQL + CDC/同步服务写当前索引Reindex 迁移存量短窗口双写或增量追赶别名切换18.8 数据校验数量：GET /products_v1/_countGET /products_v2/_count按状态统计：GET /products_v1/_search{  "size": 0,  "aggs": {    "status": { "terms": { "field": "status" } }  }}抽样比较：1. 随机取 1000 个 ID2. 分别读取 v1/v2 文档3. 比较关键字段和版本4. 记录差异并修复搜索对比：1. 收集 100 个线上查询2. 在 v1/v2 分别执行3. 比较命中数、Top20、耗时4. 人工评估差异是否合理18.9 灰度与回滚灰度方式：  内部账号先切 v2；  按用户百分比切；  按机房或服务实例切；  对比新旧结果；  监控无结果率、点击率、延迟和错误率。回滚：POST /_aliases{  "actions": [    { "remove": { "index": "products_v2", "alias": "products_read" } },    { "add": { "index": "products_v1", "alias": "products_read" } }  ]}如果写别名已经切到 v2，回滚前要评估切换窗口内 v2 独有数据的处理方式。18.10 分片数调整Reindex 适合任意调整主分片数：products_v1: 3 shardsproducts_v2: 12 shardsSplit API 只能增加分片数，且原分片数需满足拆分倍数要求：POST /products_v1/_split/products_v2{  "settings": {    "index.number_of_shards": 6  }}Shrink API 减少分片数：POST /products_v1/_shrink/products_v2{  "settings": {    "index.number_of_shards": 1  }}两者要求索引只读并满足分片分布条件。通用 Mapping 变更仍优先 Reindex。18.11 变更管理清单变更前：  明确目标：Mapping、分词器、分片数、索引设置；  评估磁盘容量；  准备新索引模板；  确认同步链路；  备份关键索引；  制定回滚方案；  通知业务方。变更中：  低峰执行 Reindex；  控制任务并发；  观察磁盘、CPU、IO；  增量追赶；  校验数量和质量；  灰度切读。变更后：  保留旧索引；  监控搜索指标；  确认写链路正常；  更新文档和监控面板；  到期删除旧索引。18.12 本章小结  应用应使用读写别名，不直接依赖物理索引；  Mapping 和分词器变更通常需要新索引和 Reindex；  Reindex 建议异步执行并监控任务；  零停机需要存量迁移、增量追赶、校验、灰度和回滚；  CDC 比业务双写更容易保证可回放和低侵入；  切换写别名前必须确认数据一致性；  旧索引应保留一段时间用于回滚。18.13 思考题  为什么读写别名要分开？  Reindex 期间如何处理持续更新的增量数据？  切换写别名前需要做哪些校验？  双写和 CDC 各有什么优缺点？  如果新索引查询结果明显变差，如何回滚和定位？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 写入不是简单调用一次 API。写入链路涉及协调节点、主分片、副本分片、refresh、translog、segment merge 和队列拒绝。设计不当的批量任务可以轻松拖垮一个集群。本章覆盖写入链路、Bulk 策略、refresh 设置、并发限流、失败重试、同步链路和写入监控。17.1 写入链路客户端请求-&gt; coordinating node 解析路由-&gt; 转发主分片-&gt; 主分片写入 Lucene buffer 和 translog-&gt; 并行同步副本分片-&gt; 副本确认-&gt; 主分片返回协调节点-&gt; 协调节点返回客户端写入成功通常表示主分片和满足策略的副本已接收，但文档可搜索还需要 refresh。17.2 refresh 策略查看设置：GET /products/_settings/index.refresh_interval修改：PUT /products/_settings{  "index": {    "refresh_interval": "30s"  }}            场景      建议                  业务搜索      1s 到 30s              高吞吐日志      30s 或更大              批量导入      临时 -1，完成后恢复并手动 refresh              测试      可手动 refresh      批量导入：PUT /import-index/_settings{  "index": {    "refresh_interval": "-1",    "number_of_replicas": 0  }}完成后：POST /import-index/_refreshPUT /import-index/_settings{  "index": {    "refresh_interval": "1s",    "number_of_replicas": 1  }}导入期间副本为 0 有风险，必须确认目标集群和备份策略。17.3 Bulk API 结构POST /_bulk{"index": {"_index": "products", "_id": "10001"}}{"id": 10001, "title": "轻薄笔记本", "price": 6999}{"create": {"_index": "products", "_id": "10002"}}{"id": 10002, "title": "游戏手机", "price": 4999}{"update": {"_index": "products", "_id": "10003"}}{"doc": {"price": 499}}{"delete": {"_index": "products", "_id": "10004"}}要求：  每行必须是完整 JSON；  请求体必须是 NDJSON；  action 行和数据行成对出现；  delete 没有 data 行；  HTTP 200 不代表全部成功。17.4 批次大小常见批次：            指标      常见范围                  文档数      500-5000              请求大小      5-15MB              并发      2-8，视集群能力              单文档大小      1KB-10KB 较常见      不要盲目复制参数。文档大小从 1KB 到 100KB 时，同样的 5000 条批次压力完全不同。17.5 写入队列与拒绝常见错误：{  "type": "es_rejected_execution_exception",  "reason": "rejected execution of coordinating operation"}含义：线程池队列满，当前节点来不及处理写入。处理：  降低客户端并发；  减小批次；  调大 refresh_interval；  检查磁盘和 CPU；  检查 Mapping 解析成本；  检查副本数量；  扩容数据节点；  使用削峰队列。17.6 Java Bulk 示例public BulkResponse bulkProducts(List&lt;Product&gt; products) throws IOException {    BulkRequest.Builder builder = new BulkRequest.Builder()            .refresh(Refresh.False);    for (Product product : products) {        builder.operations(op -&gt; op.index(index -&gt; index                .index("products")                .id(String.valueOf(product.getId()))                .document(product)        ));    }    return client.bulk(builder.build());}失败处理：BulkResponse response = client.bulk(request);if (response.errors()) {    List&lt;FailedOperation&gt; failed = response.items().stream()            .filter(BulkItemResponse::isFailed)            .map(item -&gt; new FailedOperation(                    item.index(),                    item.id(),                    item.error().reason()            ))            .toList();    failedBuffer.addAll(failed);}17.7 自适应反压固定并发容易在集群繁忙时失败。可以根据 429 和耗时调整并发：public void adjustConcurrency(boolean rejected, long costMs) {    if (rejected) {        permits.release(Math.max(1, permits.availablePermits() / 2));        return;    }    if (costMs &gt; 1000) {        return;    }    if (permits.availablePermits() &lt; maxPermits) {        permits.release();    }}更简单可靠的方案：  使用固定小并发压测；  观察队列、CPU、IO、写入耗时；  设置保守上限；  429 指数退避；  不在应用层无限重试。17.8 数据同步模式全量导入适合：首次建索引、数据规模可控、可重复读取事实源步骤：  创建新索引；  调整导入设置；  分页读取数据库；  Bulk 写入；  校验数量和抽样；  恢复设置；  切换别名。增量同步适合：数据有 updated_at 或版本号SELECT *FROM productWHERE updated_at &gt; ?ORDER BY updated_atLIMIT 1000;注意：  时间窗口要重叠，避免边界遗漏；  相同 updated_at 的排序要稳定；  使用上游版本做幂等；  记录 checkpoint；  处理乱序。CDC 同步MySQL -&gt; binlog -&gt; Kafka -&gt; Connector -&gt; Elasticsearch优点：  延迟低；  可回放；  覆盖删除；  与应用解耦。挑战：  schema 变更；  乱序；  重复事件；  大事务；  数据一致性校验。17.9 幂等写入使用业务 ID：{"index": {"_index": "products", "_id": "10001"}}{"id": 10001, "version": 128}使用外部版本：{"index": {"_index": "products", "_id": "10001", "version": 128, "version_type": "external"}}{"id": 10001}软删除：{  "id": 10001,  "deleted": true,  "deleted_at": "2026-08-25T10:00:00Z"}搜索时过滤：{  "term": { "deleted": false }}软删除保留数据便于恢复，但需要清理策略。17.10 乱序与版本冲突CDC 或消息重试可能乱序：事件 v1事件 v3事件 v2处理方式：  使用 version_type=external 拒绝旧版本；  按 ID 分区保证同实体顺序；  使用 updated_at 比较后写入；  对不可比较事件使用状态机；  定期对账修正。17.11 一致性校验数量校验：GET /products/_count抽样校验：GET /products/_search{  "size": 100,  "query": {    "range": {      "updated_at": {        "gte": "now-1h"      }    }  }}对账维度：            维度      示例                  总数      MySQL 有效商品数              版本      ID -&gt; version 抽样              时间桶      每小时更新数              状态分布      status 计数              关键字段      价格、库存、标题      17.12 写入监控指标            指标      说明                  indexing rate      每秒写入文档数              indexing latency      写入耗时              merge time      segment 合并耗时              refresh time      refresh 耗时              flush latency      flush 耗时              rejected      写入拒绝              bulk failure      批量部分失败              queue size      线程池队列              disk usage      磁盘使用              segment count      segment 数量      Prometheus 常用表达式示例：rate(elasticsearch_indices_indexing_index_total[1m])rate(elasticsearch_thread_pool_rejected_count{type="write"}[1m])elasticsearch_indices_segment_count17.13 写入上线清单  Mapping 已评审；  批次大小已压测；  并发上限明确；  429 退避策略明确；  refresh_interval 合理；  translog 策略满足 RPO；  同步任务可断点续跑；  失败数据可重放；  监控和告警已配置；  已演练索引重建。17.14 本章小结  Bulk 是高吞吐写入的标准方式，但必须逐条处理失败；  refresh_interval 影响吞吐和可见性延迟；  批次大小按字节数和压测结果确定；  写入拒绝需要反压，不要无限重试；  数据同步优先考虑幂等、断点、乱序和删除；  CDC 常用 Kafka 作为可靠缓冲和回放层；  写入链路必须监控队列、拒绝、merge、refresh 和磁盘。17.15 思考题  为什么 HTTP 200 的 bulk 响应仍可能是失败？  批量导入前将副本设为 0 有什么收益和风险？  如何设计增量同步的 checkpoint？  CDC 事件乱序时如何避免旧数据覆盖新数据？  写入出现大量 429 时应如何处理？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。索引设计决定了系统未来的容量、性能和可维护性。分片数不合理、副本不足、字段膨胀、无生命周期管理，这些问题在数据量小时不明显，上线后会集中爆发。本章覆盖索引规划、分片与副本、索引模板、Component Template、Data Stream、ILM 和索引治理。16.1 索引设计原则设计前先回答：            问题      说明                  数据规模      当前量和 12 个月增长量              写入模型      持续写入、批量导入、频繁更新              查询模型      搜索、聚合、点查、时间范围              保留周期      天、月、年或永久              可用性      可接受的数据丢失和恢复时间              业务隔离      租户、环境、业务域              成本      磁盘、内存、节点和网络      通用原则：  业务搜索常用大索引加别名；  日志和指标按时间滚动索引；  主分片数按容量和写入吞吐规划；  副本至少 1 个；  Mapping 提前定义；  所有时间序列数据设置生命周期；  索引名可预测但不直接硬编码到应用。16.2 分片规划主分片数量创建后不能直接修改。PUT /products{  "settings": {    "number_of_shards": 6,    "number_of_replicas": 1  }}常见规划步骤：预估总量-&gt; 选择单分片目标大小-&gt; 计算主分片数-&gt; 结合节点数和写入吞吐调整-&gt; 压测验证示例：数据量：600GB单分片目标：50GB主分片数：600 / 50 = 12节点数：6每节点主分片：2副本数：1总分片数：24建议范围：            数据类型      单分片大小                  日志流水      10-50GB              业务搜索      20-75GB              安全审计      20-50GB              高频小索引      可小于 10GB，但避免过多碎索引      分片过多的代价：  更多元数据；  更多文件句柄；  更多次任务调度；  查询扇出更大；  segment merge 更碎；  master 节点压力更高。16.3 副本设计PUT /products/_settings{  "index": {    "number_of_replicas": 1  }}副本作用：  主分片故障时提升为主；  提供读并发；  分布负载到更多节点。副本代价：  磁盘空间翻倍；  写入放大；  更多内存和文件句柄；  分片平衡压力增加。生产建议：            场景      副本数                  生产关键业务      1 或 2              开发测试      0              可搜索快照冷层      视方案而定              临时索引      0 或 1      16.4 索引模板索引模板在创建索引时自动应用设置和 Mapping。PUT /_index_template/applogs{  "index_patterns": ["applogs-*"],  "priority": 100,  "template": {    "settings": {      "number_of_shards": 3,      "number_of_replicas": 1,      "refresh_interval": "30s"    },    "mappings": {      "dynamic": "false",      "properties": {        "@timestamp": { "type": "date" },        "level": { "type": "keyword" },        "service": { "type": "keyword" },        "trace_id": { "type": "keyword" },        "message": { "type": "text" },        "host": {          "properties": {            "name": { "type": "keyword" },            "ip": { "type": "ip" }          }        }      }    }  },  "data_stream": {}}模板匹配多个时，priority 越高越优先。16.5 Component Template把公共配置拆成组件：PUT /_component_template/base-settings{  "template": {    "settings": {      "number_of_shards": 3,      "number_of_replicas": 1,      "refresh_interval": "30s"    }  }}通用 Mapping：PUT /_component_template/common-fields{  "template": {    "mappings": {      "properties": {        "created_at": { "type": "date" },        "updated_at": { "type": "date" },        "source": { "type": "keyword" }      }    }  }}组合模板：PUT /_index_template/applogs{  "index_patterns": ["applogs-*"],  "composed_of": ["base-settings", "common-fields"],  "priority": 100,  "data_stream": {}}优点：  统一配置；  降低复制成本；  便于版本管理；  适合平台化治理。16.6 Data StreamData Stream 适合只追加的时间序列数据：logs-events +-- .ds-logs-events-2026.08.25-000001 +-- .ds-logs-events-2026.08.26-000002创建模板：PUT /_index_template/logs-events-template{  "index_patterns": ["logs-events*"],  "data_stream": {},  "template": {    "settings": {      "number_of_shards": 3,      "number_of_replicas": 1    },    "mappings": {      "properties": {        "@timestamp": { "type": "date" }      }    }  }}创建 Data Stream：PUT /_data_stream/logs-events写入：POST /logs-events/_doc{  "@timestamp": "2026-08-25T10:00:00Z",  "level": "INFO",  "message": "request finished"}要求：  必须有 @timestamp；  写入进入当前 backing index；  只支持追加写；  更新和删除受限；  查询通过 Data Stream 名。16.7 rolloverrollover 按条件滚动新索引：POST /applogs-000001/_rollover{  "conditions": {    "max_age": "1d",    "max_primary_shard_size": "50gb",    "max_docs": 100000000  }}别名必须带 is_write_index：POST /_aliases{  "actions": [    {      "add": {        "index": "applogs-000001",        "alias": "applogs",        "is_write_index": true      }    }  ]}滚动条件建议优先使用 max_primary_shard_size，而不是只看天数。16.8 ILM 索引生命周期ILM 管理索引从热到删除的全生命周期。PUT /_ilm/policy/applogs-policy{  "policy": {    "phases": {      "hot": {        "min_age": "0ms",        "actions": {          "rollover": {            "max_age": "1d",            "max_primary_shard_size": "50gb"          },          "set_priority": { "priority": 100 }        }      },      "warm": {        "min_age": "3d",        "actions": {          "shrink": { "number_of_shards": 1 },          "forcemerge": { "max_num_segments": 1 },          "allocate": { "number_of_replicas": 1 }        }      },      "cold": {        "min_age": "30d",        "actions": {          "set_priority": { "priority": 0 }        }      },      "delete": {        "min_age": "90d",        "actions": {          "delete": {}        }      }    }  }}阶段说明：            阶段      目标                  Hot      接收写入和高频查询              Warm      不写入，仍常查询，可合并和收缩              Cold      偶尔查询，降低成本              Frozen      低频查询，成本更低              Delete      删除索引      查看状态：GET /applogs-*/_ilm/explain16.9 refresh_interval默认约 1s：{  "index": {    "refresh_interval": "1s"  }}高写入日志场景可调大：PUT /applogs-000001/_settings{  "index": {    "refresh_interval": "30s"  }}影响：  调大减少小 segment；  写入吞吐更高；  数据可见延迟增加；  调小会增加刷新压力；  -1 表示禁用自动刷新。16.10 translog 与 flush常用配置：{  "index.translog.durability": "request",  "index.translog.sync_interval": "5s",  "index.translog.flush_threshold_size": "512mb"}            配置      说明                  request      每请求 fsync，可靠性高，成本高              async      周期 fsync，性能更好，丢失窗口更大              flush_threshold_size      translog 达到阈值触发 flush      日志类数据可评估 async；交易和审计类数据应谨慎评估 RPO。16.11 索引命名与别名推荐命名：{domain}.{dataset}.{version}{domain}.logs.{environment}.{date}示例：mall.products.v1mall.products.v2ops.applogs.prod.2026.08.25应用使用别名：POST /_aliases{  "actions": [    {      "add": {        "index": "mall.products.v2",        "alias": "mall.products.read"      }    },    {      "add": {        "index": "mall.products.v2",        "alias": "mall.products.write",        "is_write_index": true      }    }  ]}读写别名分开，便于灰度和回滚。16.12 索引容量治理定期检查：GET /_cat/indices?v&amp;h=index,docs.count,store.size,pri.store.sizeGET /_cat/shards?vGET /_cluster/allocation/explain治理项：            指标      问题                  索引数量过多      元数据压力              单分片过小      浪费调度成本              单分片过大      恢复和迁移慢              副本不足      容灾风险              副本过多      成本过高              Mapping 字段过多      Mapping 爆炸              segment 过多      合并不足      16.13 本章小结  索引设计要提前评估容量、查询、写入、保留和容灾；  主分片数创建后不能直接修改，必须按单分片目标大小规划；  副本提供容灾和读并发，但会增加成本和写入放大；  索引模板和 Component Template 统一配置；  Data Stream 适合只追加时间序列数据；  ILM 管理 hot、warm、cold、delete 生命周期；  应用应使用别名，不直接依赖物理索引名。16.14 思考题  600GB 日志数据如何规划主分片和副本？  为什么索引主分片数不能直接修改？  Data Stream 为什么要求时间字段和只追加写入？  refresh_interval 从 1s 调到 30s 有什么利弊？  如何为一个保留 90 天的日志索引设计 ILM？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 8.x 官方推荐使用 Java API Client。它基于 Jackson，提供强类型请求构建和响应模型，替代了已经废弃的 High Level REST Client。本章覆盖依赖、客户端创建、连接配置、搜索封装、聚合解析、Bulk 写入、异常处理和 Spring Boot 集成。15.1 客户端选型            客户端      状态      建议                  Java API Client      官方新客户端      新项目推荐              High Level REST Client      已废弃      老项目逐步迁移              Low Level REST Client      维护中      特殊场景或底层控制              Spring Data Elasticsearch      社区封装      快速开发，复杂搜索仍可透传 DSL              Jest      非官方，生态弱化      不建议新项目      Java API Client 可以与 Low Level REST Client 配合使用，自定义序列化、传输和拦截。15.2 添加依赖Maven：&lt;dependency&gt;    &lt;groupId&gt;co.elastic.clients&lt;/groupId&gt;    &lt;artifactId&gt;elasticsearch-java&lt;/artifactId&gt;    &lt;version&gt;8.15.0&lt;/version&gt;&lt;/dependency&gt;Spring Boot 3 示例：&lt;dependency&gt;    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;    &lt;groupId&gt;co.elastic.clients&lt;/groupId&gt;    &lt;artifactId&gt;elasticsearch-java&lt;/artifactId&gt;    &lt;version&gt;8.15.0&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;    &lt;groupId&gt;jakarta.json&lt;/groupId&gt;    &lt;artifactId&gt;jakarta.json-api&lt;/artifactId&gt;    &lt;version&gt;2.1.3&lt;/version&gt;&lt;/dependency&gt;版本应与集群版本保持兼容，避免直接使用差距过大的客户端。15.3 创建客户端基础创建：RestClient restClient = RestClient.builder(        new HttpHost("localhost", 9200, "http")).build();ElasticsearchTransport transport = new RestClientTransport(        restClient,        new JacksonJsonpMapper());ElasticsearchClient client = new ElasticsearchClient(transport);带认证：final CredentialsProvider provider = new BasicCredentialsProvider();provider.setCredentials(        AuthScope.ANY,        new UsernamePasswordCredentials("app_user", "password"));RestClient restClient = RestClient.builder(        new HttpHost("localhost", 9200, "https")).setHttpClientConfigCallback(httpClientBuilder -&gt; httpClientBuilder        .setDefaultCredentialsProvider(provider)        .setSSLContext(sslContext)).build();15.4 Spring Boot 配置application.yml：elasticsearch:  hosts: https://es01:9200  username: app_user  password: ${ES_PASSWORD}  connect-timeout: 2s  socket-timeout: 5s  max-conn-total: 100  max-conn-per-route: 20配置类：@Configuration@EnableConfigurationProperties(ElasticsearchProperties.class)public class ElasticsearchConfig {    @Bean(destroyMethod = "close")    public RestClient restClient(ElasticsearchProperties properties) {        return RestClient.builder(HttpHost.create(properties.getHosts()))                .setRequestConfigCallback(builder -&gt; builder                        .setConnectTimeout((int) properties.getConnectTimeout().toMillis())                        .setSocketTimeout((int) properties.getSocketTimeout().toMillis()))                .setHttpClientConfigCallback(builder -&gt; builder                        .setMaxConnTotal(properties.getMaxConnTotal())                        .setMaxConnPerRoute(properties.getMaxConnPerRoute()))                .build();    }    @Bean    public ElasticsearchClient elasticsearchClient(RestClient restClient) {        ElasticsearchTransport transport =                new RestClientTransport(restClient, new JacksonJsonpMapper());        return new ElasticsearchClient(transport);    }}属性类：@ConfigurationProperties(prefix = "elasticsearch")public class ElasticsearchProperties {    private String hosts;    private String username;    private String password;    private Duration connectTimeout = Duration.ofSeconds(2);    private Duration socketTimeout = Duration.ofSeconds(5);    private int maxConnTotal = 100;    private int maxConnPerRoute = 20;}15.5 索引和文档操作创建索引：client.indices().create(c -&gt; c.index("products")        .mappings(m -&gt; m                .properties("title", p -&gt; p.text(t -&gt; t.analyzer("ik_max_word")))                .properties("brand", p -&gt; p.keyword(k -&gt; k.ignoreAbove(64)))                .properties("price", p -&gt; p.scaledFloat(f -&gt; f.scalingFactor(100.0)))        )        .settings(s -&gt; s.numberOfShards(3).numberOfReplicas(1)));写入文档：Product product = new Product(10001L, "轻薄笔记本", "NOVA", 6999.00);IndexResponse response = client.index(i -&gt; i        .index("products")        .id(String.valueOf(product.getId()))        .document(product));读取：GetResponse&lt;Product&gt; response = client.get(g -&gt; g        .index("products")        .id("10001"),        Product.class);if (response.found()) {    Product product = response.source();}15.6 构建查询public SearchRequest buildSearch(ProductSearchQuery query) {    return SearchRequest.of(r -&gt; r            .index("products")            .from(query.from())            .size(query.size())            .query(q -&gt; q.bool(b -&gt; {                b.must(m -&gt; m.multiMatch(mm -&gt; mm                        .query(query.keyword())                        .fields("title^3", "brand", "description")                        .type(TextQueryType.BestFields)));                b.filter(f -&gt; f.term(t -&gt; t.field("status").value("ON_SALE")));                if (query.brand() != null &amp;&amp; !query.brand().isEmpty()) {                    b.filter(f -&gt; f.terms(t -&gt; t.field("brand")                            .terms(v -&gt; v.value(query.brand().stream()                                    .map(FieldValue::of)                                    .toList()))));                }                return b;            }))            .sort(s -&gt; s.field(f -&gt; f.field("sales").order(SortOrder.Desc)))            .source(src -&gt; src.filter(f -&gt; f.includes("id", "title", "brand", "price")))    );}执行：SearchResponse&lt;Product&gt; response =        client.search(buildSearch(query), Product.class);解析：List&lt;Product&gt; products = response.hits().hits().stream()        .map(Hit::source)        .filter(Objects::nonNull)        .toList();15.7 分页与排序封装public record PageCursor(List&lt;Object&gt; values) {}public SearchRequest buildSearchAfter(ProductSearchQuery query, PageCursor cursor) {    return SearchRequest.of(r -&gt; r            .index("products")            .size(query.size())            .query(buildQuery(query))            .sort(s -&gt; s.field(f -&gt; f.field("sales").order(SortOrder.Desc)))            .sort(s -&gt; s.field(f -&gt; f.field("id").order(SortOrder.Asc)))            .searchAfter(cursor == null ? List.of() : cursor.values())    );}返回下一页游标：List&lt;Hit&lt;Product&gt;&gt; hits = response.hits().hits();if (!hits.isEmpty()) {    List&lt;Object&gt; nextCursor = hits.get(hits.size() - 1).sort();}15.8 聚合解析SearchResponse&lt;Void&gt; response = client.search(s -&gt; s        .index("orders")        .size(0)        .query(q -&gt; q.range(r -&gt; r.date(d -&gt; d                .field("created_at")                .gte(JsonData.of("now-30d/d")))))        .aggregations("status", a -&gt; a.terms(t -&gt; t                .field("status")                .size(20))),        Void.class);Aggregate aggregate = response.aggregations().get("status");List&lt;StringTermsBucket&gt; buckets = aggregate.sterms().buckets().array();for (StringTermsBucket bucket : buckets) {    System.out.println(bucket.key().stringValue() + ": " + bucket.docCount());}类型与字段 Mapping 相关，数值字段可能是 lterms，日期 histogram 是 dateHistogram。建议在服务层封装聚合结果 DTO，避免上层直接依赖客户端模型。15.9 Bulk 写入public void bulkUpsert(List&lt;Product&gt; products) throws IOException {    BulkRequest.Builder builder = new BulkRequest.Builder();    for (Product product : products) {        Map&lt;String, Object&gt; doc = Map.of(                "id", product.getId(),                "title", product.getTitle(),                "brand", product.getBrand(),                "price", product.getPrice()        );        builder.operations(op -&gt; op.index(idx -&gt; idx                .index("products")                .id(String.valueOf(product.getId()))                .document(doc)                .version(product.getVersion())                .versionType(VersionType.External)        ));    }    BulkResponse response = client.bulk(builder.build());    if (response.errors()) {        for (BulkItemResponse item : response.items()) {            if (item.isFailed()) {                log.warn("bulk failed index={} id={} error={}",                        item.index(), item.id(), item.error().reason());            }        }    }}批量大小应压测确定，常见 500 到 5000 条或 5 到 15MB。15.10 异常处理常见异常：            异常      场景      处理                  IOException      网络失败      重试或熔断              ElasticsearchException      服务端返回错误      解析 status/reason              429      请求被拒绝      降低并发、退避              401/403      认证或权限错误      不重试，告警              404      索引或文档不存在      按业务处理              409      版本冲突      重新读取或幂等处理      示例：public &lt;T&gt; T executeWithRetry(Supplier&lt;T&gt; action) {    int maxRetry = 2;    for (int i = 0; i &lt;= maxRetry; i++) {        try {            return action.get();        } catch (ElasticsearchException e) {            if (e.status() == 401 || e.status() == 403) {                throw e;            }            if (i == maxRetry) {                throw e;            }            sleepQuietly(Duration.ofMillis(100L &lt;&lt; i));        }    }    throw new IllegalStateException("unreachable");}15.11 搜索服务分层推荐分层：Controller  -&gt; SearchApplicationService     -&gt; QueryBuilder     -&gt; EsClientGateway     -&gt; ResponseAssembler职责：            层      职责                  Controller      参数校验、鉴权、协议转换              ApplicationService      业务流程、缓存、降级              QueryBuilder      业务条件转 DSL              Gateway      ES 客户端、重试、异常转换              ResponseAssembler      ES 响应转业务 DTO      不要在 Controller 中直接拼复杂 DSL。15.12 观测与慢日志long start = System.nanoTime();try {    SearchResponse&lt;Product&gt; response = client.search(request, Product.class);    metrics.searchSuccess(query.type(), response.took());    return response;} catch (Exception e) {    metrics.searchFailure(query.type(), e.getClass().getSimpleName());    throw new SearchUnavailableException(e);} finally {    long cost = System.nanoTime() - start;    if (cost &gt; Duration.ofMillis(500).toNanos()) {        log.warn("slow es query type={} request={}", query.type(), request);    }}记录内容：  traceId；  索引名；  查询类型；  DSL 摘要；  took；  shards 统计；  命中数；  失败原因。15.13 Spring Data Elasticsearch适合简单 CRUD：public interface ProductRepository        extends ElasticsearchRepository&lt;Product, Long&gt; {    List&lt;Product&gt; findByBrandAndStatus(String brand, String status);}复杂查询可以使用 @Query 或直接注入 ElasticsearchOperations。当查询逻辑复杂、性能要求高、需要精细化控制时，直接使用 Java API Client 更清晰。15.14 本章小结  新项目使用 Java API Client，逐步迁移 High Level REST Client；  客户端要统一配置连接池、认证、超时和 TLS；  查询构建、执行、解析分层，避免业务层直接暴露复杂 DSL；  bulk 必须逐条检查失败；  搜索调用要有重试、熔断、指标和慢日志；  Spring Data 适合简单场景，复杂搜索建议原生客户端。15.15 思考题  High Level REST Client 废弃后，新项目应如何选择客户端？  搜索超时应如何设置？超时后是否无限重试？  为什么要在 Gateway 层统一转换 ES 异常？  如何记录一次搜索请求的可观测信息？  Bulk 写入失败后如何设计重试和死信？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。关键词搜索擅长精确匹配词项，向量搜索擅长理解语义相似。随着语义检索、推荐召回和 RAG 系统普及，dense_vector 与 kNN 已经成为 Elasticsearch 的重要能力。本章讲解向量字段、kNN 查询、混合搜索、过滤向量查询、召回与重排，以及向量索引的容量与性能边界。14.1 为什么需要向量搜索用户输入：适合出差的轻便电脑关键词搜索依赖分词词项是否命中，而语义搜索可以把文本映射成向量：适合出差的轻便电脑 -&gt; [0.12, -0.34, 0.56, ...]轻薄笔记本 -&gt; [0.10, -0.30, 0.52, ...]向量距离越近，语义越相似。它适合：  同义表达召回；  图片相似度；  音频相似度；  推荐召回；  知识库问答；  语义去重。向量搜索不能替代所有关键词搜索，例如订单号、错误码、品牌型号仍应使用关键词精确匹配。14.2 dense_vector 字段Mapping：PUT /semantic-docs{  "mappings": {    "properties": {      "title": { "type": "text" },      "content": { "type": "text" },      "content_vector": {        "type": "dense_vector",        "dims": 768,        "index": true,        "similarity": "cosine"      }    }  }}参数说明：            参数      说明                  dims      向量维度              index      是否构建向量索引              similarity      相似度度量      常见维度：            模型类型      维度示例                  小型语义模型      256 / 384              中型通用模型      768 / 1024              大型模型      1536 / 3072      维度越高表达能力通常越强，但内存和计算成本也越高。14.3 相似度度量            similarity      含义      适用                  cosine      夹角相似度      文本语义、归一化向量              dot_product      内积      已归一化向量，通常性能好              l2_norm      欧氏距离      空间坐标、某些图像特征      文本嵌入常使用 cosine。如果模型输出已归一化，使用 dot_product 通常更快，但必须确认向量确实归一化。14.4 写入向量文档PUT /semantic-docs/_doc/1{  "title": "轻薄笔记本",  "content": "适合出差使用，重量轻，续航长。",  "content_vector": [0.12, -0.34, 0.56]}实际 768 维向量会很长，不建议在业务数据库中直接保存完整 JSON 明文。同步服务应在内存中调用 Embedding 模型，然后写入 ES。14.5 kNN 搜索GET /semantic-docs/_search{  "knn": {    "field": "content_vector",    "query_vector": [0.11, -0.32, 0.55],    "k": 10,    "num_candidates": 100  }}参数说明：            参数      说明                  field      向量字段              query_vector      查询向量              k      最终返回近邻数              num_candidates      每个分片候选数量              filter      过滤条件      num_candidates 越大，召回越准确，但查询越慢。常见从 k * 5 或 k * 10 开始压测。14.6 带过滤条件的 kNNGET /semantic-docs/_search{  "knn": {    "field": "content_vector",    "query_vector": [0.11, -0.32, 0.55],    "k": 10,    "num_candidates": 200,    "filter": {      "bool": {        "filter": [          { "term": { "status": "PUBLISHED" } },          { "range": { "publish_at": { "lte": "now" } } }        ]      }    }  }}过滤条件在候选检索时生效，不是先取 Top K 再过滤。这样可以避免某些过滤条件下结果不足。14.7 混合搜索混合搜索同时使用关键词和向量：GET /products/_search{  "query": {    "multi_match": {      "query": "轻薄笔记本",      "fields": ["title^3", "brand", "description"],      "type": "best_fields"    }  },  "knn": {    "field": "title_vector",    "query_vector": [0.11, -0.32, 0.55],    "k": 50,    "num_candidates": 300  },  "rank": {    "rrf": {      "rank_window_size": 100  }  },  "size": 20}RRFReciprocal Rank Fusion 按名次融合，不直接比较原始分数：score = sum(1 / (rank_constant + rank_i))优点：  不需要归一化 BM25 和向量分数；  对不同分值分布稳健；  实现简单；  常作为默认混合策略。线性加权final_score = text_weight * text_score + vector_weight * vector_score优点是可解释、可控；缺点是 BM25 与向量分数分布不同，需要调参。14.8 Rerank粗召回后，可以使用更重模型重排：关键词召回 100 条+ 向量召回 100 条-&gt; 去重合并-&gt; Rerank 模型打分-&gt; 返回 Top 20常见 Rerank 输入：query: 适合出差的轻便电脑document: 轻薄笔记本，重量 1.2kg，续航 18 小时模型输出相关分数。应用层根据分数重新排序。适合：  搜索质量要求高；  召回集较小；  允许额外延迟；  有标注或点击数据；  可以承担模型成本。14.9 多路召回架构flowchart LR    Query[用户查询] --&gt; Analyzer[查询理解]    Analyzer --&gt; Keyword[关键词召回]    Analyzer --&gt; Vector[向量召回]    Analyzer --&gt; Rule[规则/运营召回]    Keyword --&gt; Merge[合并去重]    Vector --&gt; Merge    Rule --&gt; Merge    Merge --&gt; Rerank[重排]    Rerank --&gt; Result[搜索结果]查询理解可以包括：  分词；  改写；  同义词扩展；  类目预测；  语言识别；  意图识别；  向量化。14.10 向量生成链路原始文本 -&gt; 清洗 -&gt; 截断/分块 -&gt; Embedding 模型 -&gt; 向量 -&gt; Elasticsearch注意事项：  写入和查询必须使用同一模型或兼容模型；  模型升级需要重建向量字段；  长文本应分块，每块独立向量；  保留 chunk 元数据，例如文档 ID、位置、标题；  控制 Embedding 调用成本；  向量生成失败要有重试和死信；  批量生成比单条调用更高效。文档分块示例：{  "doc_id": "kb-1001",  "chunk_id": "kb-1001-0003",  "chunk_index": 3,  "title": "退款政策",  "content": "未发货订单可在 30 分钟内取消。",  "content_vector": [0.1, 0.2, 0.3],  "source": "help-center"}14.11 RAG 检索RAG 常用流程：用户问题 -&gt; 查询改写 -&gt; 混合检索 -&gt; 重排 -&gt; 组装上下文 -&gt; LLM 生成答案检索建议：  同时使用关键词和语义召回；  限制上下文长度；  返回来源文档和位置；  权限过滤必须在检索层完成；  对无结果和低置信结果兜底；  记录用户反馈；  不要把私有数据直接暴露给无权限用户。14.12 性能与容量向量检索的主要成本：  内存：HNSW 图和向量数据；  CPU：距离计算；  磁盘：向量字段存储；  网络：多分片候选归并；  构建成本：向量索引写入更慢。优化方式：            优化      说明                  降低维度      选择合适模型，不盲目追求高维              使用量化      依据版本支持选择压缩能力              减少 num_candidates      降低单次候选数              缩小 filter 范围      时间、租户、类目过滤              分索引      按业务或时间拆分              冷热分层      冷向量少查或不留内存              预过滤      避免无权限候选              监控 P99      向量查询延迟单独观测      14.13 关键词与向量选型            查询      建议                  订单号、错误码      keyword              品牌型号      keyword + text              中文自然语言      text + IK              同义表达      dense_vector              图片相似      image embedding + kNN              推荐召回      user/item 向量              知识问答      混合搜索 + Rerank              强权限文档      必须加 filter      14.14 版本边界不同版本对向量检索能力差异较大：  早期版本支持 script score，但性能有限；  8.x 引入近似 kNN 与 HNSW 索引；  后续版本增强过滤、量化、多向量等能力；  9.x 在性能和语义搜索生态上继续演进；  兼容发行版能力可能不同。使用前必须确认当前版本的 Mapping 语法、最大维度、相似度类型和 kNN 参数。14.15 本章小结  dense_vector 用于存储向量，kNN 用于近邻检索；  文本语义常用 cosine，已归一化向量可用 dot_product；  kNN 的 k 和 num_candidates 是效果与性能的关键权衡；  混合搜索通常比单一关键词或单一向量更稳；  RRF 按名次融合，适合作为默认策略；  复杂搜索采用多路召回加重排；  向量检索内存、CPU 和构建成本显著，必须容量规划和压测。14.16 思考题  什么场景必须使用关键词搜索而不是向量搜索？  num_candidates 调大会带来什么影响？  为什么 RRF 比直接相加 BM25 分和向量分更稳？  模型升级后为什么需要重建向量字段？  RAG 系统中权限过滤为什么必须发生在 ES 查询中？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。分页和排序看起来简单，却是最容易把集群打垮的地方之一。深分页会让协调节点请求大量分片、取回大量候选文档，并在内存中归并排序。本章讲解 from/size、search_after、PIT、scroll、排序稳定性、导出方案和分页治理。13.1 from/sizeGET /products/_search{  "from": 0,  "size": 20,  "query": {    "match_all": {}  }}第 100 页：{  "from": 1960,  "size": 20}如果索引有 5 个主分片，协调节点最坏需要从每个分片取 from + size = 1980 条，再归并排序取 20 条。默认限制：from + size &lt;= index.max_result_window，默认 10000查看配置：GET /products/_settings/index.max_result_window不建议为了深分页直接调大该值。13.2 哪些场景适合 from/size适合：  搜索结果前几页；  后台管理小数据集；  查询过滤后数量确定较小；  用户不会持续翻深页；  产品愿意限制最大页数。不适合：  爬虫式全量遍历；  第 1000 页；  无时间范围的大索引；  高并发导出；  排序不稳定的随机结果。13.3 排序稳定性分数或排序值相同时，如果没有唯一排序键，翻页可能出现重复或漏数据。建议业务列表加稳定排序键：GET /products/_search{  "query": {    "match": { "title": "笔记本" }  },  "sort": [    { "_score": { "order": "desc" } },    { "sales": { "order": "desc" } },    { "product_id": { "order": "asc" } }  ]}常见稳定键：  id；  created_at；  updated_at；  版本号；  唯一业务编号。13.4 search_aftersearch_after 使用上一页最后一条的排序值作为游标。第一页：GET /products/_search{  "size": 20,  "query": {    "match": { "title": "笔记本" }  },  "sort": [    { "sales": { "order": "desc" } },    { "product_id": { "order": "asc" } }  ]}假设最后一条：{  "sort": [1200, "10042"]}下一页：GET /products/_search{  "size": 20,  "query": {    "match": { "title": "笔记本" }  },  "search_after": [1200, "10042"],  "sort": [    { "sales": { "order": "desc" } },    { "product_id": { "order": "asc" } }  ]}要求：  排序字段必须稳定；  search_after 值必须与排序字段一一对应；  只能向后翻页；  不能直接跳转到任意页；  查询条件应保持不变。13.5 Point In Time搜索过程中数据可能变化，导致翻页重复或漏掉文档。PIT 保存一个时间点视图。打开 PIT：POST /products/_pit?keep_alive=2m返回：{  "id": "pit-id-base64"}带 PIT 查询：GET /_search{  "size": 20,  "pit": {    "id": "pit-id-base64",    "keep_alive": "2m"  },  "query": {    "match": { "title": "笔记本" }  },  "sort": [    { "sales": { "order": "desc" } },    { "product_id": { "order": "asc" } }  ],  "search_after": [1200, "10042"]}关闭 PIT：DELETE /_pit{  "id": "pit-id-base64"}PIT 会占用资源，keep_alive 应设置合理过期时间，应用异常退出时也要有清理任务。13.6 scrollscroll 是旧版本常用的批量遍历方式：POST /products/_search?scroll=2m{  "size": 1000,  "query": { "match_all": {} }}返回 scroll_id 后继续：POST /_search/scroll{  "scroll": "2m",  "scroll_id": "..."}清理：DELETE /_search/scroll{  "scroll_id": ["..."]}scroll 适合离线全量导出或重建索引，不适合用户实时分页。新系统优先使用 PIT + search_after。13.7 排序与 Doc Values排序通常依赖 Doc Values。{  "sort": [    { "price": { "order": "asc" } },    { "sales": { "order": "desc" } }  ]}如果字段没有 Doc Values，ES 可能需要 fielddata，把倒排结构反转到堆内存，容易导致 OOM。生产原则：  排序和聚合字段使用 keyword 或数值；  不对高基数字段随意开启 fielddata；  text 排序使用 keyword 子字段；  大字段不参与排序；  新 Mapping 阶段确定排序需求。13.8 多级排序设计商品搜索示例：{  "sort": [    { "in_stock": { "order": "desc" } },    { "_score": { "order": "desc" } },    { "sales_7d": { "order": "desc" } },    { "product_id": { "order": "asc" } }  ]}常见排序因子：            因子      说明                  _score      文本相关性              sales_7d      近期销量              in_stock      是否可售              updated_at      新鲜度              quality_score      商品质量分              ctr      点击率              conversion_rate      转化率              product_id      稳定 tie-breaker      排序因子应在写入时预计算，不要在查询时用脚本实时计算复杂分数。13.9 无限滚动移动端常用无限滚动：{  "size": 20,  "search_after": ["2026-08-25T10:00:00Z", "product_10001"],  "sort": [    { "created_at": { "order": "desc" } },    { "id": { "order": "asc" } }  ]}限制：  设置最大深度，例如 1000 条；  前端提示推荐刷新；  记录最后游标；  游标过期后回到第一页；  避免同一用户并发翻深页。13.10 导出与全量遍历小数据量：search_after + PIT大导出：异步任务 -&gt; 按时间或 ID 分片 -&gt; 并发受控 -&gt; 输出到对象存储 -&gt; 记录进度按时间切分：{  "query": {    "range": {      "created_at": {        "gte": "2026-08-25T00:00:00Z",        "lt": "2026-08-25T01:00:00Z"      }    }  }}导出建议：  限制并发；  限制字段；  关闭不需要的高亮；  使用离线集群；  记录任务进度；  失败可重试；  不影响在线查询。13.11 分页治理清单  是否限制最大页数；  是否使用 search_after；  排序是否有唯一 tie-breaker；  是否需要 PIT；  from+size 是否可能超过 10000；  是否监控深分页请求；  导出是否异步；  查询是否有时间范围；  _source 是否只取必要字段；  是否存在并发导出任务冲突。13.12 本章小结  from/size 只适合浅分页；  深分页使用 search_after，实时一致性要求高时配合 PIT；  scroll 适合离线导出，不适合用户分页；  排序字段必须有稳定唯一键，否则翻页可能重复或漏数据；  排序和聚合依赖 Doc Values，不要对 text 开启 fielddata；  大导出应异步、分片、限流并输出到对象存储。13.13 思考题  为什么 from=100000 会拖垮集群？  search_after 为什么不能跳页？  PIT 解决了翻页中的什么问题？  如何设计一个稳定的商品搜索排序？  需要导出一亿条订单时，你会采用什么方案？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Query DSL 适合搜索和复杂控制，但很多数据分析师更熟悉 SQL。Elasticsearch 提供 SQL 能力；8.11 之后又逐步推出 ES|QL，用更贴近管道的语言做查询和分析。本章介绍两者的能力、语法、访问方式和选型边界。12.1 Elasticsearch SQLPOST /_sql{  "query": "SELECT brand, COUNT(*) AS cnt, AVG(price) AS avg_price FROM products GROUP BY brand ORDER BY cnt DESC LIMIT 10"}返回结构类似表格：{  "columns": [    { "name": "brand", "type": "keyword" },    { "name": "cnt", "type": "long" },    { "name": "avg_price", "type": "double" }  ],  "rows": [    ["NOVA", 120, 5999.5]  ]}12.2 SQL 常用语法条件过滤：SELECT product_id, title, priceFROM productsWHERE status = 'ON_SALE'  AND price BETWEEN 3000 AND 9000ORDER BY price DESCLIMIT 20全文搜索：SELECT product_id, title, SCORE()FROM productsWHERE MATCH(title, '笔记本')ORDER BY SCORE() DESC聚合：SELECT status, COUNT(*) AS cnt, SUM(amount) AS totalFROM ordersWHERE created_at &gt; NOW() - INTERVAL 30 DAYGROUP BY statusORDER BY total DESC嵌套字段的支持程度与版本、Mapping 和字段结构有关，复杂嵌套建议仍使用 Query DSL。12.3 SQL TRANSLATE查看 SQL 翻译后的 DSL：POST /_sql/translate{  "query": "SELECT status, COUNT(*) FROM orders GROUP BY status"}用途：  学习 SQL 与 DSL 的对应关系；  检查 SQL 是否被下推为高效查询；  排查执行成本；  把 BI SQL 逐步迁移为 DSL。12.4 SQL 分页与游标POST /_sql{  "query": "SELECT product_id, title FROM products ORDER BY product_id",  "filter": {    "range": {      "price": { "gte": 1000 }    }  },  "fetch_size": 1000}返回 cursor 后可以继续：POST /_sql{  "cursor": "..."}清理游标：POST /_sql/close{  "cursor": "..."}12.5 SQL 限制Elasticsearch SQL 不是完整的关系型数据库 SQL。            限制      说明                  Join 支持有限      只支持有限类型和条件              事务不存在      只有查询和写入，不是 ACID              复杂子查询受限      并非所有 SQL 都可翻译              深分页受限      使用 cursor 或过滤条件              字段类型敏感      text 聚合需要 keyword 子字段              性能仍受 DSL 影响      慢查询同样会拖垮集群      它适合临时分析和简单报表，不适合替代数仓或 OLAP 引擎。12.6 什么是 ES|QL            ES      QL 是 Elasticsearch 8.11 引入、后续版本持续增强的管道式查询语言。它以一条事件流为中心，通过 | 管道逐层处理。      FROM orders| WHERE created_at &gt; NOW() - 30 DAYS| EVAL profit = amount - cost| STATS total_amount = SUM(amount), order_count = COUNT(*) BY status| SORT total_amount DESC| LIMIT 10执行思路：读取数据 -&gt; 过滤 -&gt; 派生字段 -&gt; 聚合 -&gt; 排序 -&gt; 限制输出12.7 ES|QL 常用命令查询：FROM logs-*| LIMIT 10过滤：FROM logs-*| WHERE log.level == "ERROR"| LIMIT 100选择字段：FROM orders| KEEP order_id, user_id, amount, status, created_at| LIMIT 100派生字段：FROM orders| EVAL amount_wan = amount / 10000.0| KEEP order_id, amount, amount_wan统计：FROM orders| STATS total = SUM(amount), cnt = COUNT(*), avg_amount = AVG(amount) BY status时间处理：FROM logs-*| EVAL minute = DATE_TRUNC(1 MINUTE, @timestamp)| STATS error_count = COUNT(*) BY minute| SORT minute ASC具体函数和能力与版本强相关，使用前应确认目标集群版本。12.8 SQL 与 ES|QL 对比            维度      SQL      ES      QL                  用户熟悉度      高      中等，需要学习管道语法                     查询形态      传统 SELECT      管道式                     生态      BI 工具支持较好      新生态逐步增强                     分析能力      常用 SQL 聚合      适合事件流和可观测性分析                     版本边界      较早版本可用      8.11+，能力随版本变化                     性能模型      翻译为 ES 查询      有专用执行引擎                     复杂查询      DSL 仍更强      持续演进             12.9 选型建议            场景      建议                         应用搜索接口      Query DSL                     临时排查数据      SQL 或 ES      QL              运维日志分析      ES      QL 或 Kibana 查询              BI 简单报表      SQL                     复杂多维分析      DSL 聚合或数仓                     精确财务报表      数仓/数据库，ES 只做加速                     高并发线上接口      预聚合成物化结果             12.10 权限与资源控制            SQL 和 ES      QL 仍然是查询集群的入口，必须做权限控制：        只开放必要索引；  限制查询时间范围；  禁止普通用户全量扫描；  限制返回行数和并发；  对 BI 用户使用独立角色；  开启慢查询日志；  敏感字段使用字段级安全策略。12.11 本章小结  SQL 适合临时分析和 BI 简单报表，底层仍会翻译为 ES 查询；                              ES          QL 是 8.11+ 的管道式查询语言，适合事件流和可观测性分析；                                                  SQL/ES          QL 不能绕过 Mapping、分片、聚合和深分页的性能约束；                      复杂搜索仍以 Query DSL 为主；  精确财务和大规模扫描应使用数仓。12.12 思考题  SQL MATCH 和普通 LIKE 有什么区别？  如何查看 SQL 翻译后的 Query DSL？  SQL 游标适合什么场景？为什么不能无限翻页？                              ES          QL 的管道模型有什么优势？                      哪些报表不应该使用 Elasticsearch 实时聚合？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。搜索回答“哪些文档匹配”，聚合回答“这些文档有什么规律”。Elasticsearch 的聚合体系非常强大，也是性能问题的高发区。本章覆盖 Bucket、Metric、Pipeline、嵌套聚合、排序、nested 聚合、近似算法和聚合性能基础。11.1 聚合请求结构只看聚合结果：GET /orders/_search{  "size": 0,  "query": {    "range": {      "created_at": { "gte": "now-30d/d" }    }  },  "aggs": {    "by_status": {      "terms": { "field": "status" }    },    "total_amount": {      "sum": { "field": "amount" }    }  }}size=0 表示不返回文档，只返回聚合结果。生产聚合应尽量加时间范围或其他过滤条件。11.2 三类聚合            类型      作用      示例                  Bucket      分桶      按状态、品牌、日期分组              Metric      计算指标      sum、avg、max、cardinality              Pipeline      基于其他聚合结果再计算      derivative、cumulative_sum      11.3 Metric 聚合GET /orders/_search{  "size": 0,  "aggs": {    "avg_amount": { "avg": { "field": "amount" } },    "max_amount": { "max": { "field": "amount" } },    "min_amount": { "min": { "field": "amount" } },    "sum_amount": { "sum": { "field": "amount" } },    "value_count": { "value_count": { "field": "amount" } },    "stats_amount": { "stats": { "field": "amount" } },    "percentiles_amount": {      "percentiles": {        "field": "amount",        "percents": [50, 90, 95, 99]      }    }  }}常用指标：            聚合      说明                  avg      平均值              sum      总和              min / max      最小/最大值              value_count      数量              stats      一次性返回多项基础指标              extended_stats      更多统计量              percentiles      分位数              percentile_ranks      值所处百分位              cardinality      去重计数，近似算法              top_hits      桶内 Top N 文档      11.4 terms 聚合GET /orders/_search{  "size": 0,  "aggs": {    "by_brand": {      "terms": {        "field": "brand",        "size": 20,        "order": { "_count": "desc" }      }    }  }}按指标排序：{  "aggs": {    "by_brand": {      "terms": {        "field": "brand",        "size": 20,        "order": { "total_sales": "desc" }      },      "aggs": {        "total_sales": {          "sum": { "field": "sales" }        }      }    }  }}terms 聚合是近似聚合。每个分片先取本地 Top N，再在协调节点归并。如果某个词项在每个分片都排不进本地前 N，可能被漏掉。提高准确性：  增大 size；  增大 shard_size；  减少分片数量；  使用更适合的索引设计；  对关键报表离线精确计算。11.5 filter 聚合GET /orders/_search{  "size": 0,  "aggs": {    "paid_orders": {      "filter": {        "term": { "status": "PAID" }      },      "aggs": {        "amount": { "sum": { "field": "amount" } }      }    }  }}多个互斥分组：{  "aggs": {    "order_groups": {      "filters": {        "filters": {          "paid": { "term": { "status": "PAID" } },          "closed": { "term": { "status": "CLOSED" } },          "refunded": { "term": { "status": "REFUNDED" } }        }      }    }  }}11.6 range 聚合GET /products/_search{  "size": 0,  "aggs": {    "price_ranges": {      "range": {        "field": "price",        "ranges": [          { "to": 1000 },          { "from": 1000, "to": 3000 },          { "from": 3000, "to": 7000 },          { "from": 7000 }        ]      }    }  }}日期范围：{  "aggs": {    "created_ranges": {      "range": {        "field": "created_at",        "format": "yyyy-MM-dd",        "ranges": [          { "to": "2026-01-01" },          { "from": "2026-01-01", "to": "2026-07-01" },          { "from": "2026-07-01" }        ]      }    }  }}11.7 histogram 与 date_histogram直方图：{  "aggs": {    "price_histogram": {      "histogram": {        "field": "price",        "interval": 1000,        "min_doc_count": 0      }    }  }}按天统计：GET /orders/_search{  "size": 0,  "aggs": {    "daily_orders": {      "date_histogram": {        "field": "created_at",        "calendar_interval": "day",        "format": "yyyy-MM-dd",        "time_zone": "+08:00",        "min_doc_count": 0      }    }  }}            参数      特点      示例                  calendar_interval      日历感知      1 day、1 month、1 quarter              fixed_interval      固定毫秒数      30s、1h、7d      月份长度不同，应使用 calendar_interval: "month"，不要用 fixed_interval: "30d" 代替自然月。11.8 嵌套聚合按品牌分组，再统计每个品牌的类目：GET /products/_search{  "size": 0,  "aggs": {    "brands": {      "terms": { "field": "brand", "size": 10 },      "aggs": {        "categories": {          "terms": { "field": "category", "size": 10 }        },        "avg_price": {          "avg": { "field": "price" }        }      }    }  }}日期加状态：{  "aggs": {    "daily": {      "date_histogram": {        "field": "created_at",        "calendar_interval": "day"      },      "aggs": {        "status": {          "terms": { "field": "status" }        },        "gmv": {          "sum": { "field": "amount" }        }      }    }  }}嵌套层级越多，内存和 CPU 消耗越大。交互式看板建议控制聚合层级和桶数量。11.9 cardinality 去重GET /events/_search{  "size": 0,  "aggs": {    "unique_users": {      "cardinality": {        "field": "user_id",        "precision_threshold": 40000      }    }  }}cardinality 使用 HyperLogLog++ 近似算法。precision_threshold 越高越准确，但内存开销越大。适合 UV、独立设备数等趋势统计；财务、库存等精确指标不要使用近似算法。11.10 top_hitsGET /orders/_search{  "size": 0,  "aggs": {    "by_user": {      "terms": {        "field": "user_id",        "size": 10,        "order": { "total_amount": "desc" }      },      "aggs": {        "total_amount": {          "sum": { "field": "amount" }        },        "latest_orders": {          "top_hits": {            "size": 3,            "sort": [              { "created_at": { "order": "desc" } }            ],            "_source": ["order_id", "amount", "created_at"]          }        }      }    }  }}适合“每个分组取前 N 条”。top_hits 需要取回文档内容，开销比纯 Metric 聚合高。11.11 Pipeline 聚合按天累计销售额：GET /orders/_search{  "size": 0,  "aggs": {    "daily": {      "date_histogram": {        "field": "created_at",        "calendar_interval": "day"      },      "aggs": {        "gmv": {          "sum": { "field": "amount" }        },        "cumulative_gmv": {          "cumulative_sum": {            "buckets_path": "gmv"          }        }      }    }  }}环比：{  "aggs": {    "daily": {      "date_histogram": {        "field": "created_at",        "calendar_interval": "day"      },      "aggs": {        "gmv": { "sum": { "field": "amount" } },        "daily_derivative": {          "derivative": { "buckets_path": "gmv" }        }      }    }  }}常用 Pipeline：            聚合      作用                  derivative      差值              cumulative_sum      累计求和              moving_fn      移动函数              bucket_script      桶内脚本计算              bucket_selector      过滤桶              bucket_sort      桶排序              stats_bucket      对桶指标统计      11.12 nested 聚合统计 SKU 颜色：GET /products/_search{  "size": 0,  "aggs": {    "skus": {      "nested": { "path": "skus" },      "aggs": {        "colors": {          "terms": { "field": "skus.color" }        }      }    }  }}从 nested 结果回到父文档：{  "aggs": {    "skus": {      "nested": { "path": "skus" },      "aggs": {        "colors": {          "terms": { "field": "skus.color" },          "aggs": {            "back_to_product": {              "reverse_nested": {},              "aggs": {                "avg_price": { "avg": { "field": "price" } }              }            }          }        }      }    }  }}11.13 post_filter常见搜索需求是：文档结果受品牌过滤影响，但品牌聚合不受该过滤影响。GET /products/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "filter": [        { "term": { "status": "ON_SALE" } }      ]    }  },  "aggs": {    "brands": {      "terms": { "field": "brand" }    }  },  "post_filter": {    "term": { "brand": "NOVA" }  }}post_filter 在查询和聚合后执行，只影响返回文档，不影响聚合结果。11.14 composite 聚合分页terms 聚合没有通用深分页。可以使用 composite 游标：GET /orders/_search{  "size": 0,  "aggs": {    "composite_buckets": {      "composite": {        "size": 100,        "sources": [          { "brand": { "terms": { "field": "brand" } } },          { "status": { "terms": { "field": "status" } } }        ]      }    }  }}带 after_key 继续：{  "aggs": {    "composite_buckets": {      "composite": {        "size": 100,        "sources": [          { "brand": { "terms": { "field": "brand" } } },          { "status": { "terms": { "field": "status" } } }        ],        "after": {          "brand": "NOVA",          "status": "PAID"        }      }    }  }}11.15 聚合性能因素            因素      影响                  分片数      分片越多，归并开销越大              桶数量      桶越多，内存和 CPU 越高              高基数字段      terms/cardinality 成本高              时间范围      范围越大扫描越多              nested 聚合      需要处理隐藏子文档              top_hits      取回文档，开销高              脚本      逐文档计算              冷数据      Page Cache 未命中，磁盘 IO 高      优化路径：  缩小时间范围；  降低 terms size；  减少嵌套聚合；  预先计算派生字段；  使用 rollover 缩小单索引范围；  冷热分层；  大报表放数仓；  对高频报表做结果缓存；  限制并发看板查询；  压测聚合请求。11.16 本章小结  Bucket 分桶，Metric 计算指标，Pipeline 对聚合结果再计算；  terms 是近似聚合，结果受 shard size 和桶数量影响；  date_histogram 应区分 calendar interval 和 fixed interval；  cardinality 是近似去重，不适合财务精确统计；  nested 聚合需要显式进入 nested path；  post_filter 只影响文档结果，不影响聚合；  聚合性能取决于分片数、桶数、时间范围、高基数字段和缓存命中。11.17 思考题  为什么 terms 聚合可能漏掉部分低频词项？  size=0 在聚合请求中有什么意义？  如何让品牌聚合不受当前品牌筛选影响？  cardinality 为什么不适合金额统计？  一个看板聚合从 200ms 变成 8s，你会从哪些角度排查？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。真实业务查询很少只有一个条件。本章讲解 bool 的进阶用法、constant_score、function_score、nested、父子关系、脚本查询和查询组织原则。10.1 bool 查询再深入GET /orders/_search{  "query": {    "bool": {      "must": [        { "multi_match": { "query": "笔记本", "fields": ["item_names", "title"] } }      ],      "filter": [        { "range": { "created_at": { "gte": "now-30d/d" } } },        { "terms": { "status": ["PAID", "SHIPPED", "COMPLETED"] } }      ],      "must_not": [        { "term": { "is_deleted": true } }      ],      "should": [        { "term": { "vip_level": "HIGH" } },        { "term": { "channel": "APP" } }      ],      "minimum_should_match": 0    }  }}查询语义：必须匹配商品词且最近 30 天且状态有效且未删除VIP 或 APP 命中可以加分10.2 嵌套 bool{  "query": {    "bool": {      "filter": [        { "range": { "created_at": { "gte": "now-7d/d" } } },        {          "bool": {            "should": [              { "term": { "brand": "NOVA" } },              { "term": { "brand": "MARS" } }            ],            "minimum_should_match": 1          }        }      ]    }  }}常见错误是把“品牌 A 且价格大于 100”和“品牌 B 且价格小于 50”写成扁平 bool：{  "bool": {    "must": [      { "terms": { "brand": ["A", "B"] } },      {        "bool": {          "should": [            { "range": { "price": { "gt": 100 } } },            { "range": { "price": { "lt": 50 } } }          ]        }      }    ]  }}这个查询会错误匹配“品牌 A 价格 30”。正确方式是外层 should，内层 must：{  "bool": {    "should": [      {        "bool": {          "must": [            { "term": { "brand": "A" } },            { "range": { "price": { "gt": 100 } } }          ]        }      },      {        "bool": {          "must": [            { "term": { "brand": "B" } },            { "range": { "price": { "lt": 50 } } }          ]        }      }    ],    "minimum_should_match": 1  }}10.3 constant_score{  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "should": [        {          "constant_score": {            "filter": { "term": { "in_stock": true } },            "boost": 1.5          }        }      ]    }  }}适合把过滤条件转换成固定加分，而不是让条件参与 BM25 文本打分。10.4 function_scorefunction_score 用于把文本相关性与业务指标混合。GET /products/_search{  "query": {    "function_score": {      "query": {        "multi_match": {          "query": "笔记本",          "fields": ["title^3", "brand", "description"]        }      },      "functions": [        {          "filter": { "term": { "status": "ON_SALE" } },          "weight": 1.5        },        {          "field_value_factor": {            "field": "sales",            "factor": 1.2,            "modifier": "log1p",            "missing": 1          }        },        {          "gauss": {            "created_at": {              "origin": "now",              "scale": "30d",              "decay": 0.5            }          }        }      ],      "score_mode": "sum",      "boost_mode": "multiply",      "min_score": 1    }  }}参数说明：            参数      说明                  score_mode      多个 function 得分如何合并              boost_mode      查询得分与 function 得分如何合并              min_score      过滤最低分              weight      固定权重              field_value_factor      使用字段值参与打分              gauss / exp / linear      衰减函数              random_score      随机打散      典型业务：  销量加成；  库存商品优先；  新品加权；  距离衰减；  赞助商品固定加权；  随机推荐。注意事项：  权重需要持续评估；  过多函数会增加查询成本；  field_value_factor 应使用对数修饰避免大值淹没文本分；  赞助内容应可识别、可审计。10.5 script_score当内置 function 不够时，可以使用 Painless：GET /products/_search{  "query": {    "script_score": {      "query": {        "match": { "title": "笔记本" }      },      "script": {        "source": """          double score = _score;          if (doc['sales'].size() &gt; 0) {            score += Math.log1p(doc['sales'].value) * params.salesWeight;          }          if (doc['in_stock'].size() &gt; 0 &amp;&amp; doc['in_stock'].value) {            score += params.stockWeight;          }          return score;        """,        "params": {          "salesWeight": 2.0,          "stockWeight": 1.0        }      }    }  }}script_score 灵活但昂贵。更复杂的精排通常在应用层或独立排序服务中完成。10.6 nested 查询Mapping：PUT /products{  "mappings": {    "properties": {      "skus": {        "type": "nested",        "properties": {          "color": { "type": "keyword" },          "size": { "type": "keyword" },          "price": { "type": "scaled_float", "scaling_factor": 100 },          "stock": { "type": "integer" }        }      }    }  }}组合条件：GET /products/_search{  "query": {    "nested": {      "path": "skus",      "query": {        "bool": {          "must": [            { "term": { "skus.color": "black" } },            { "term": { "skus.size": "16G" } },            { "range": { "skus.price": { "lte": 7000 } } }          ]        }      },      "score_mode": "max"    }  }}score_mode 决定多个匹配 nested 对象的得分合并方式，常用 max、sum、avg。10.7 nested 聚合统计 SKU 颜色：GET /products/_search{  "size": 0,  "aggs": {    "skus": {      "nested": { "path": "skus" },      "aggs": {        "colors": {          "terms": { "field": "skus.color" }        }      }    }  }}从 nested 结果回到父文档：{  "aggs": {    "skus": {      "nested": { "path": "skus" },      "aggs": {        "colors": {          "terms": { "field": "skus.color" },          "aggs": {            "back_to_product": {              "reverse_nested": {},              "aggs": {                "avg_price": { "avg": { "field": "price" } }              }            }          }        }      }    }  }}10.8 父子查询has_child 查询父文档：GET /company/_search{  "query": {    "has_child": {      "type": "employee",      "query": {        "term": { "city": "Shanghai" }      },      "score_mode": "sum"    }  }}has_parent 查询子文档：GET /company/_search{  "query": {    "has_parent": {      "parent_type": "department",      "query": {        "term": { "name.keyword": "Search Platform" }      }    }  }}父子查询的约束：  父子必须在同一分片；  查询成本高于普通文档；  更新父或子都会增加索引压力；  大量层级不适合使用 join。多数商品搜索场景更推荐把 SKU、类目、品牌在同步层冗余到商品文档，或为 SKU 建独立索引。10.9 脚本查询script query：GET /orders/_search{  "query": {    "bool": {      "filter": {        "script": {          "script": {            "source": "doc['amount'].value &gt; doc['paid_amount'].value",            "lang": "painless"          }        }      }    }  }}脚本会绕过部分索引优化，成本高。替代方式：  写入时计算差额字段；  使用 runtime field；  使用 range 或 term 表达；  在数据库中先过滤。10.10 查询改写实践慢查询：{  "query": {    "bool": {      "must": [        { "wildcard": { "order_no": "*0001" } },        { "match_all": {} }      ]    }  }}优化思路：  order_no 是否应有后缀字段；  是否能使用 term 精确查；  是否能限制时间范围；  是否应该放数据库；  是否要建补全索引。优化后：{  "query": {    "bool": {      "filter": [        { "term": { "order_no_suffix": "0001" } },        { "range": { "created_at": { "gte": "now-1d/d" } } }      ]    }  }}10.11 查询代码封装建议public SearchRequest buildProductSearch(ProductSearchQuery query) {    List&lt;Query&gt; filters = new ArrayList&lt;&gt;();    filters.add(termQuery("status", "ON_SALE"));    if (query.brandIds() != null) {        filters.add(termsQuery("brand_id", query.brandIds()));    }    if (query.minPrice() != null || query.maxPrice() != null) {        filters.add(rangeQuery("price", query.minPrice(), query.maxPrice()));    }    Query textQuery = query.keyword().isBlank()            ? matchAll()            : multiMatch(query.keyword(), "title^3", "brand", "description");    return SearchRequest.of(r -&gt; r            .index("products")            .from(query.from())            .size(query.size())            .query(q -&gt; q.bool(b -&gt; b.must(textQuery).filter(filters))));}原则：  查询构造与业务参数校验分离；  不允许用户直接传任意 DSL；  限制字段和 size；  输出结构化慢日志；  为每个查询类型建立监控。10.12 本章小结  bool 可以表达复杂与或非逻辑，但要注意嵌套语义；  不参与相关性的条件放 filter；  function_score 用于融合文本相关性与业务指标；  nested 保留对象数组内部关系，查询和聚合都需显式声明；  父子 join 成本高，优先反范式；  脚本查询和通配符查询要严格治理。10.13 思考题  如何表达“(品牌 A 且价格&gt;100) 或 (品牌 B 且价格&lt;50)”？  function_score 的 boost_mode 和 score_mode 有什么区别？  普通 object 为什么无法正确查询 SKU 属性组合？  nested 聚合中 reverse_nested 的作用是什么？  哪些场景应该避免 script query？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。全文搜索是 Elasticsearch 最核心的能力。它不是简单的字符串包含，而是包括分词、召回、评分、字段权重、短语距离、高亮和多字段组合的完整链路。本章覆盖 match、operator、phrase、multi-match、高亮、相关性和常见搜索问题。9.1 match 查询GET /products/_search{  "query": {    "match": {      "title": "轻薄笔记本"    }  }}执行过程：查询文本 -&gt; search analyzer 分词 -&gt; 多个词项 -&gt; 倒排索引匹配 -&gt; 计算得分 -&gt; 排序返回如果分词为：轻薄笔记本默认等价于：轻薄 OR 笔记本9.2 operator 与 minimum_should_match要求所有词都匹配：{  "match": {    "title": {      "query": "轻薄 笔记本",      "operator": "and"    }  }}等价于：轻薄 AND 笔记本更灵活的匹配比例：{  "match": {    "title": {      "query": "轻薄 高刷新率 笔记本 16G",      "minimum_should_match": "75%"    }  }}选择建议：            场景      建议                  搜索召回优先      默认 OR              精确短语类查询      and 或 phrase              长查询多关键词      minimum_should_match              搜索无结果率高      降低匹配门槛或补词表              无关结果多      提高匹配要求或调整字段权重      9.3 match_phrase短语查询要求词项按顺序且距离接近：GET /products/_search{  "query": {    "match_phrase": {      "title": "轻薄笔记本"    }  }}允许中间插入其他词：{  "match_phrase": {    "title": {      "query": "轻薄 笔记本",      "slop": 2    }  }}slop 是词项移动次数，不是简单“中间允许几个字”。值越大召回越多，但也更容易误召回。适合：  品牌加型号；  固定专有名词；  错误码短语；  标题精确组合。不适合作为泛搜索的唯一查询，召回会偏少。9.4 match_phrase_prefix最后一个词按前缀匹配：{  "match_phrase_prefix": {    "title": "Elasticsearch sea"  }}适合输入框即时搜索，但性能不如专门的 edge_ngram 或 completion suggester。生产搜索建议将补全索引与主搜索索引分开设计。9.5 multi_match多字段搜索：GET /products/_search{  "query": {    "multi_match": {      "query": "轻薄笔记本",      "fields": ["title^3", "brand^2", "category", "description"],      "type": "best_fields"    }  }}^3 表示字段权重提升。常用类型：            type      行为      适用                  best_fields      取最佳字段得分      标题、描述等字段竞争              most_fields      多字段得分相加      多分词器、多语言召回              cross_fields      跨字段匹配同一组词      姓名、地址拆字段              phrase      多字段短语匹配      精确组合              phrase_prefix      多字段前缀短语      搜索补全      best_fields{  "multi_match": {    "query": "笔记本",    "fields": ["title", "description"],    "type": "best_fields",    "tie_breaker": 0.3  }}tie_breaker 会把其他匹配字段得分按比例并入总分，避免只看单一字段。cross_fields适合：first_name = Tomlast_name = Smith查询：Tom Smith{  "multi_match": {    "query": "Tom Smith",    "fields": ["first_name", "last_name"],    "type": "cross_fields",    "operator": "and"  }}9.6 query_string 与 simple_query_stringquery_string 支持 Lucene 语法：{  "query_string": {    "query": "title:(笔记本 OR 电脑) AND price:[3000 TO 9000]"  }}语法错误会导致查询失败，且暴露给用户有风险。simple_query_string 更宽容：{  "simple_query_string": {    "query": "笔记本 +轻薄 -二手",    "fields": ["title", "description"]  }}用户搜索框通常使用 simple_query_string 或程序生成的 bool/multi_match，不直接暴露 query_string。9.7 高亮基础高亮：GET /products/_search{  "query": {    "match": { "title": "笔记本" }  },  "highlight": {    "fields": {      "title": {}    }  }}自定义标签：{  "highlight": {    "pre_tags": ["&lt;em class='highlight'&gt;"],    "post_tags": ["&lt;/em&gt;"],    "fields": {      "title": {},      "description": { "fragment_size": 120, "number_of_fragments": 2 }    }  }}参数：            参数      说明                  fragment_size      片段长度              number_of_fragments      返回片段数              pre_tags      前标签              post_tags      后标签              require_field_match      是否要求字段匹配才高亮      高亮会带来额外 CPU 和内存开销，长文本应限制片段数量和长度。9.8 相关性基础默认打分算法是 BM25，核心因素：  词项频率 TF：词在文档中出现越多，越相关；  逆向文档频率 IDF：词越罕见，权重越高；  字段长度归一化：同样匹配下，短字段通常更重要；  查询词项权重；  字段 boost；  查询结构。查看得分解释：GET /products/_explain/10001{  "query": {    "match": { "title": "笔记本" }  }}常见问题：            现象      可能原因                  热门商品排后      BM25 只看文本，不理解销量              长标题排后      字段长度归一化              常见词权重过高      词表或停用词不合理              品牌不优先      字段权重未设置              结果不稳定      分数相同缺少第二排序键      业务排序不能完全依赖默认 BM25，通常需要结合销量、库存、新品、转化率、业务权重和人工规则。9.9 字段权重设计{  "multi_match": {    "query": "Nova 笔记本",    "fields": [      "title^4",      "brand^3",      "category^2",      "tags^2",      "description"    ],    "type": "best_fields"  }}权重不是越大越好。过大的 boost 会让文本分数淹没业务分数，建议从 1、2、3 这类小值开始，用真实查询集评估。9.10 搜索无结果排查排查步骤：1. 确认文档存在：GET by id2. 确认索引和别名正确3. 检查 refresh 时间4. 检查 Mapping 类型5. _analyze 写入分词6. _analyze 搜索分词7. 用 match 验证文本召回8. 用 term 验证 keyword 过滤9. 检查 bool 是否互相矛盾10. 检查权限过滤和软删除条件常见根因：  text 使用 term；  keyword 大小写不一致；  分词器没有业务词；  filter 条件冲突；  文档尚未 refresh；  索引切换后别名错误；  数据同步失败；  nested 查询结构错误。9.11 召回率与精确率            指标      含义      提升方式                  召回率      应该返回的结果中被召回比例      同义词、多字段、多分词、降低匹配门槛              精确率      返回结果中相关结果比例      字段权重、短语查询、过滤、重排      搜索优化通常是两者权衡：宽召回 -&gt; 粗排序 -&gt; 业务过滤 -&gt; 精排/重排 -&gt; 返回不要只看某几个关键词的手工结果，应建立查询样本集和标注数据。9.12 搜索日志埋点推荐记录：{  "trace_id": "abc",  "query_id": "q-10001",  "user_id": "u8888",  "keyword": "轻薄笔记本",  "filters": { "brand": ["NOVA"], "price": [3000, 9000] },  "took_ms": 35,  "total_hits": 123,  "returned": 20,  "es_indices": ["products_v2"],  "request_json": {},  "created_at": "2026-08-25T10:00:00Z"}用于：  慢查询分析；  无结果查询统计；  热门查询词治理；  相关性评估；  搜索效果迭代。9.13 本章小结  match 会先分析查询文本，再与倒排索词项匹配；  operator 和 minimum_should_match 控制多词匹配门槛；  match_phrase 强调顺序和距离，适合精确组合；  multi_match 支持多字段和字段权重；  搜索框避免直接暴露 query_string；  BM25 只理解文本相关性，业务排序需要额外模型或规则；  搜索问题要用 _analyze、_explain 和搜索日志系统排查。9.14 思考题  match 和 term 的本质区别是什么？  什么场景用 operator=and，什么场景用 minimum_should_match？  best_fields 和 most_fields 的差异是什么？  为什么商品搜索不能只按 BM25 排序？  搜索无结果时，你的排查清单是什么？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 查询使用 JSON 描述，官方称为 Query DSL。它表达力强，但也容易写出“结果正确、性能很差”的查询。本章先建立查询结构、上下文、叶子查询、组合查询、过滤缓存和验证工具的基础，后续章节再深入全文搜索、聚合和排序。8.1 查询请求结构GET /products/_search{  "from": 0,  "size": 20,  "timeout": "800ms",  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "filter": [        { "term": { "status": "ON_SALE" } }      ]    }  },  "sort": [    { "_score": "desc" },    { "sales": "desc" }  ],  "_source": ["id", "title", "price", "brand"]}常用顶层参数：            参数      说明                  from      跳过文档数              size      返回文档数              query      查询条件              aggs      聚合定义              sort      排序规则              _source      返回字段              highlight      高亮              timeout      查询超时              track_total_hits      精确或限制总命中统计              search_after      深分页游标      8.2 Query 与 Filter 上下文同一个查询子句在不同上下文中行为不同。GET /products/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "轻薄笔记本" } }      ],      "filter": [        { "term": { "brand": "NOVA" } },        { "range": { "price": { "gte": 3000, "lte": 9000 } } }      ]    }  }}            上下文      是否计算得分      是否可缓存      适合                  Query      是      较少      全文搜索、相关性匹配              Filter      否      常用      状态、时间、范围、权限      经验规则：业务上不需要影响相关性的条件都放 filter。8.3 match_all 与 match_noneGET /products/_search{  "query": {    "match_all": {}  }}GET /products/_search{  "query": {    "match_none": {}  }}match_all 常用于只看聚合结果：GET /products/_search{  "size": 0,  "query": { "match_all": {} },  "aggs": {    "avg_price": { "avg": { "field": "price" } }  }}8.4 term 查询term 对精确词项查询，不会分析查询文本。GET /products/_search{  "query": {    "term": {      "status": {        "value": "ON_SALE"      }    }  }}keyword 字段：{  "term": { "brand.keyword": "Nova" }}常见错误：{  "term": { "title": "笔记本电脑" }}如果 title 是 text，索引里可能没有完整词项“笔记本电脑”，导致查不到。text 字段应使用 match，或使用专门的 keyword 子字段。8.5 terms 查询相当于多个 term 的 OR：GET /products/_search{  "query": {    "terms": {      "brand": ["NOVA", "MARS", "LUMI"]    }  }}terms 数量不宜过大。权限过滤如果包含几千个租户 ID，会导致查询膨胀，应改用索引隔离、角色模型或专门权限结构。8.6 range 查询数值范围：{  "range": {    "price": {      "gte": 3000,      "lt": 9000    }  }}时间范围：{  "range": {    "created_at": {      "gte": "now-7d/d",      "lt": "now"    }  }}日期数学表达式：            表达式      含义                  now      当前时间              now-1h      一小时前              now-7d/d      七天前并取整天              2026-08-01||+1M      日期加一个月      8.7 exists、ids、prefix、wildcardexists：{  "exists": { "field": "coupon_id" }}ids：{  "ids": { "values": ["10001", "10002"] }}prefix：{  "prefix": { "brand.keyword": "NO" }}wildcard：{  "wildcard": { "order_no": "O20260825*" }}wildcard 性能风险较高，尤其是前置通配符：{  "wildcard": { "order_no": "*0001" }}这类查询可能扫描大量词项，生产中应优先使用 edge_ngram、专门的冗余字段或数据库查询。8.8 bool 组合查询GET /products/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "笔记本" } }      ],      "should": [        { "term": { "brand": "NOVA" } },        { "term": { "tags": "新品" } }      ],      "must_not": [        { "term": { "status": "DELETED" } }      ],      "filter": [        { "range": { "price": { "lte": 9000 } } }      ],      "minimum_should_match": 1    }  }}            子句      作用      得分                  must      必须匹配      参与              should      可选匹配，满足则加分      参与              must_not      必须不匹配      不参与              filter      必须匹配      不参与      minimum_should_match 规则：  bool 中只有 should 时，默认至少匹配 1 个；  同时存在 must 或 filter 时，默认可以为 0；  需要强制 should 生效时显式设置。8.9 constant_score 与 dis_maxconstant_score 让查询不参与相关性评分，通常用于包装过滤：{  "constant_score": {    "filter": {      "term": { "status": "ON_SALE" }    },    "boost": 1.2  }}dis_max 取匹配得分最高的字段：{  "dis_max": {    "queries": [      { "match": { "title": "笔记本" } },      { "match": { "description": "笔记本" } }    ],    "tie_breaker": 0.3  }}适合多字段召回时避免重复求和导致分值过度膨胀。8.10 查询嵌套组织bool 可以任意嵌套：GET /orders/_search{  "query": {    "bool": {      "filter": [        { "range": { "created_at": { "gte": "now-30d" } } },        {          "bool": {            "should": [              { "term": { "status": "PAID" } },              { "term": { "status": "SHIPPED" } }            ],            "minimum_should_match": 1          }        }      ],      "must": [        { "match": { "item_names": "笔记本" } }      ]    }  }}组织原则：  高选择性过滤条件放前面；  精确条件放 filter；  文本召回放 must 或 should；  避免过深的嵌套；  查询结构应与业务语义一致，便于日志分析。8.11 URI 查询简单查询也可以通过 URL 参数：GET /products/_search?q=title:笔记本&amp;size=10适合调试，不建议生产使用。生产应使用请求体 Query DSL，便于版本管理和治理。8.12 验证与解释查询validate queryGET /products/_validate/query?explain=true{  "query": {    "term": { "status": "ON_SALE" }  }}用于检查语法和字段 Mapping 是否匹配。explainGET /products/_explain/10001{  "query": {    "match": { "title": "笔记本" }  }}用于解释某文档为什么匹配、得分如何计算。profileGET /products/_search{  "profile": true,  "query": {    "match": { "title": "笔记本" }  }}profile 会增加开销，只用于测试和问题定位，不要长期打开。8.13 track_total_hits默认情况下，总命中数在超过一定数量后可能是近似值。精确统计：GET /products/_search{  "track_total_hits": true,  "query": { "match_all": {} }}限制统计：GET /products/_search{  "track_total_hits": 1000,  "query": { "match_all": {} }}搜索列表通常只需要显示“999+”或前 1000 条，无需为了一个精确总数扫描大量分片。8.14 常见查询错误            错误      原因      处理                  查不到 text 文档      term 未分词匹配      使用 match 或 keyword 字段              聚合 text 报错      text 默认无 Doc Values      使用 keyword 子字段              bool should 不生效      与 must/filter 同时存在      设置 minimum_should_match              查询超时      扇出过大、通配符、深分页      缩小范围、优化结构              字段无法查询      index=false      重建 Mapping              结果顺序不稳定      分数相同缺少 tie-breaker      增加稳定排序字段      8.15 查询设计清单  每个条件是搜索还是过滤；  字段类型是否正确；  是否能命中 filter 缓存；  时间范围是否必要；  terms 数量是否过大；  是否存在 wildcard 和脚本；  是否需要精确 total；  是否只取必要 _source；  是否限制返回字段和大小；  是否记录慢查询和业务 traceId。8.16 本章小结  Query DSL 由叶子查询和复合查询组成；  Query 上下文计算相关性，Filter 上下文不计算且更易缓存；  term 适合 keyword，不适合直接查 text；  bool 是最常用的组合查询；  wildcard、大 terms、深分页是常见性能风险；  validate、explain、profile 是查询排查三件套。8.17 思考题  为什么业务过滤条件应放在 filter 而不是 must？  term 查询 text 字段为什么经常查不到？  bool 中 should 的默认行为在什么情况下会变化？  track_total_hits=true 可能带来什么成本？  如果查询变慢，你会先用哪些 API 定位？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。中文搜索效果往往取决于分词。同一个查询“轻薄高性能笔记本”，不同的 Analyzer 可能产生完全不同的词项，进而影响召回和排序。本章讲解 Analyzer 组成、内置分词器、自定义分词、中文分词、多字段分词、搜索与写入分词差异，以及分词排查方法。7.1 什么是 AnalyzerAnalyzer 是文本分析流程，负责把原始字符串转换为倒排索引用的词项。Character Filters -&gt; Tokenizer -&gt; Token Filters            组件      作用      示例                  Character Filter      处理原始字符      去 HTML 标签              Tokenizer      切分词元      按空格、英文单词、中文词              Token Filter      处理词元      小写、停用词、同义词、词干      输入：Elasticsearch 搜索引擎实战可能输出：elasticsearch搜索搜索引擎实战7.2 _analyze API这是排查分词最重要的工具。POST /_analyze{  "analyzer": "standard",  "text": "Elasticsearch 搜索引擎实战"}返回：{  "tokens": [    {      "token": "elasticsearch",      "start_offset": 0,      "end_offset": 13,      "type": "&lt;ALPHANUM&gt;",      "position": 0    }  ]}指定字段 Analyzer：POST /products/_analyze{  "field": "title",  "text": "轻薄高性能笔记本电脑"}自定义测试：POST /_analyze{  "tokenizer": "standard",  "filter": ["lowercase"],  "text": "Elasticsearch SEARCH Engine"}7.3 内置 Analyzer            Analyzer      特点                  standard      默认，英文效果好，中文常按单字切              simple      按非字母切分并转小写              whitespace      按空白切分，不转小写              stop      类似 simple，并移除停用词              keyword      不分词              pattern      按正则切分              fingerprint      排序并拼接词项，适合去重      standard 测试：POST /_analyze{  "analyzer": "standard",  "text": "高性能笔记本电脑"}常见输出：高性能笔记本电脑它对英文处理较好，但中文搜索会产生大量单字词项，通常需要中文分词器。7.4 Tokenizer            Tokenizer      说明                  standard      Unicode 文本分词              whitespace      按空白切分              letter      按字母切分              lowercase      等价 letter + lowercase              keyword      不分词              pattern      正则切分              path_hierarchy      路径分词              edge_ngram      前缀切分      路径分词：POST /_analyze{  "tokenizer": "path_hierarchy",  "text": "/usr/local/elasticsearch"}输出：/usr/usr/local/usr/local/elasticsearch适合类目路径、部门路径、文件路径。7.5 Token Filter            Filter      作用                  lowercase      小写              uppercase      大写              stop      停用词              stemmer      英文词干              asciifolding      音符字符转换              trim      去首尾空白              synonym      同义词              ngram      N-gram              edge_ngram      前缀 N-gram              word_delimiter      拆分复合词              unique      词项去重      示例：POST /_analyze{  "tokenizer": "standard",  "filter": ["lowercase", "stop"],  "text": "The Quick Brown Fox"}输出：quickbrownfox7.6 自定义 Analyzer在索引设置中定义：PUT /articles{  "settings": {    "analysis": {      "filter": {        "my_stopwords": {          "type": "stop",          "stopwords": ["的", "了", "和"]        }      },      "analyzer": {        "custom_analyzer": {          "type": "custom",          "tokenizer": "standard",          "filter": ["lowercase", "my_stopwords"]        }      }    }  },  "mappings": {    "properties": {      "content": {        "type": "text",        "analyzer": "custom_analyzer"      }    }  }}也可以加入 Character Filter，例如移除 HTML 标签："analyzer": {  "html_analyzer": {    "type": "custom",    "char_filter": ["html_strip"],    "tokenizer": "standard",    "filter": ["lowercase"]  }}注意：Analyzer 属于索引设置，修改已索引字段的 Analyzer 通常需要 Reindex。7.7 中文分词：IK常用模式：            Analyzer      特点                  ik_max_word      尽可能多切分，召回更全              ik_smart      尽量少切分，词项更粗      写入测试：POST /_analyze{  "analyzer": "ik_max_word",  "text": "轻薄高性能笔记本电脑"}可能输出：轻薄高性能笔记本电脑搜索测试：POST /_analyze{  "analyzer": "ik_smart",  "text": "轻薄笔记本电脑"}常见组合：{  "title": {    "type": "text",    "analyzer": "ik_max_word",    "search_analyzer": "ik_smart"  }}思路是索引时多切分提升召回，搜索时少切分减少无关组合。具体效果必须按业务查询评估。7.8 IK 自定义词典安装目录下通常有配置文件：config/analysis-ik/ +-- IKAnalyzer.cfg.xml +-- main.dic +-- stopword.dic +-- custom/添加自定义词：程序员云原生高刷新率轻薄本添加停用词：请问一下怎么样修改后需要重启或调用插件提供的 reload API，具体能力取决于插件版本。词表治理示例：            类型      示例      目的                  业务词      轻薄本、高刷新率      避免拆错              品牌词      Elasticsearch、云原生      保持专有名词              停用词      请问、一下、怎么样      降低噪声              同义词      笔记本=笔记本电脑      扩展召回              屏蔽词      违规词      合规控制      7.9 同义词搜索时同义词：PUT /products{  "settings": {    "analysis": {      "filter": {        "search_synonym": {          "type": "synonym_graph",          "synonyms": [            "笔记本,笔记本电脑",            "手机,智能手机"          ]        }      },      "analyzer": {        "search_synonym_analyzer": {          "tokenizer": "ik_max_word",          "filter": ["search_synonym"]        }      }    }  }}Mapping：{  "mappings": {    "properties": {      "title": {        "type": "text",        "analyzer": "ik_max_word",        "search_analyzer": "search_synonym_analyzer"      }    }  }}写入时同义词会增加索引体积；搜索时同义词灵活但增加查询成本。业务词表频繁变化时，通常优先使用搜索时同义词。7.10 搜索补全edge_ngram适合输入前缀补全：PUT /suggests{  "settings": {    "analysis": {      "filter": {        "edge_ngram_filter": {          "type": "edge_ngram",          "min_gram": 1,          "max_gram": 20        }      },      "analyzer": {        "edge_ngram_analyzer": {          "tokenizer": "standard",          "filter": ["lowercase", "edge_ngram_filter"]        }      }    }  },  "mappings": {    "properties": {      "keyword": {        "type": "text",        "analyzer": "edge_ngram_analyzer",        "search_analyzer": "standard"      }    }  }}写入 elasticsearch 后索引：eelelaelas...completion suggesterPUT /suggests-completion{  "mappings": {    "properties": {      "suggest": {        "type": "completion"      }    }  }}写入：PUT /suggests-completion/_doc/1{  "suggest": {    "input": ["Elasticsearch 教程", "ES 入门"],    "weight": 10  }}查询：GET /suggests-completion/_search{  "suggest": {    "product-suggest": {      "prefix": "elastic",      "completion": {        "field": "suggest"      }    }  }}7.11 多字段分词策略同一文本可以用不同 Analyzer 建多个子字段：{  "title": {    "type": "text",    "analyzer": "ik_max_word",    "fields": {      "smart": {        "type": "text",        "analyzer": "ik_smart"      },      "english": {        "type": "text",        "analyzer": "english"      },      "keyword": {        "type": "keyword",        "ignore_above": 128      }    }  }}查询：{  "multi_match": {    "query": "elasticsearch search",    "fields": ["title", "title.smart", "title.english"],    "type": "most_fields"  }}适合中英混合、品牌词、缩写、拼音等场景。7.12 拼音搜索可使用拼音分词插件。常见三层召回：中文原词 -&gt; 拼音全拼 -&gt; 拼音缩写示例：用户输入：bjb候选结果：笔记本拼音搜索要重点治理：  多音字；  拼音切分歧义；  英文品牌词；  缩写误召回；  词表更新流程。7.13 写入分词与搜索分词搜索前，查询文本也会被分析。GET /products/_search{  "query": {    "match": {      "title": {        "query": "高性能笔记本",        "analyzer": "ik_smart"      }    }  }}常见问题：            问题      原因                  文档搜不到      写入与搜索词项不同              召回太多      分词过细              召回太少      分词过粗或词表缺失              大小写不匹配      未 lowercase              同义词无效      Analyzer 未生效或未重建              前缀搜不到      未使用 edge_ngram/completion      排查步骤：1. _analyze 写入 Analyzer2. _analyze 搜索 Analyzer3. 对比词项4. 查看 Mapping5. 用 term query 验证词项6. 检查词库和同义词7.14 分词与相关性分词结果直接影响 BM25：  词项越多，单字段匹配概率越高；  罕见词通常权重更高；  字段长度影响归一化；  多字段重复内容会提高匹配度；  停用词和同义词会改变召回。不要只追求“能搜到”，还要观察无关结果是否变多、长标题是否被过度惩罚、品牌词是否被拆错、缩写是否有专门字段、热门词是否需要业务加权。7.15 本章小结  Analyzer 由 Character Filter、Tokenizer、Token Filter 组成；  _analyze 是排查分词问题的第一工具；  中文通常使用 IK 等分词器，并按业务维护词表；  索引分词和搜索分词可以不同；  多字段分词可以兼顾中文、英文、拼音、精确匹配和补全；  同义词、edge_ngram、completion 是搜索体验常用能力；  分词质量直接决定召回、排序和用户体验。7.16 思考题  ik_max_word 和 ik_smart 应如何组合使用？  为什么修改索引 Analyzer 通常需要重建索引？  写入时同义词和搜索时同义词各有什么优缺点？  用户输入“ysb”想搜“笔记本”，如何设计拼音搜索？  如果某个查询无结果，你会按什么顺序排查分词？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Mapping 决定数据如何被解析、索引、搜索、排序和聚合。很多线上搜索问题的根因不是查询写错，而是 Mapping 设计不合理。本章覆盖字段类型、动态映射、多字段、嵌套结构、日期数值、元字段、索引参数和 Mapping 治理。6.1 Mapping 能控制什么一个字段的 Mapping 通常决定：  JSON 值如何解析；  是否生成倒排索引；  使用什么分词器；  是否保存 Doc Values；  是否参与评分；  是否可以聚合和排序；  日期和数值格式；  超长 keyword 的处理方式。示例：PUT /products{  "mappings": {    "properties": {      "title": {        "type": "text",        "analyzer": "ik_max_word"      },      "brand": {        "type": "keyword",        "ignore_above": 64      },      "price": {        "type": "scaled_float",        "scaling_factor": 100      },      "description": {        "type": "text",        "norms": false      },      "snapshot": {        "type": "object",        "enabled": false      }    }  }}6.2 text 与 keywordtexttext 会被分词，适合全文搜索。{  "title": {    "type": "text",    "analyzer": "ik_max_word"  }}写入“轻薄高性能笔记本电脑”后可能索引出：轻薄高性能笔记本电脑适合标题、描述、正文、评论内容、错误消息，不适合状态、订单号、手机号、标签精确匹配和聚合字段。keywordkeyword 不分词，适合精确匹配、排序和聚合。{  "order_no": {    "type": "keyword",    "ignore_above": 64  }}适合枚举状态、ID 字符串、品牌、类目、标签、主机名、日志级别。多字段同一字段同时需要全文搜索和精确聚合时，使用 fields：{  "title": {    "type": "text",    "analyzer": "ik_max_word",    "fields": {      "keyword": {        "type": "keyword",        "ignore_above": 128      }    }  }}搜索：{  "match": { "title": "笔记本" }}聚合：{  "terms": { "field": "title.keyword" }}6.3 数值类型            类型      说明      适用                  long      64 位整数      订单数、ID、计数              integer      32 位整数      状态、数量              short      16 位整数      少用              byte      8 位整数      少用              double      64 位浮点      精度要求不高的金额              float      32 位浮点      指标、评分              half_float      半精度浮点      低精度指标              scaled_float      带固定缩放因子      金额、价格              unsigned_long      无符号 64 位      特大整数      价格建议：{  "price": {    "type": "scaled_float",    "scaling_factor": 100  }}scaling_factor=100 会把 99.99 存为整数 9999，排序更稳定，也更利于范围查询。不要把所有数值都定义成 double。数值类型会影响磁盘、排序精度和范围查询效率。6.4 日期类型{  "created_at": {    "type": "date",    "format": "strict_date_optional_time||epoch_millis"  }}常见输入：2026-08-252026-08-25T10:00:00Z2026-08-25T18:00:00+08:001787642400000查询：{  "range": {    "created_at": {      "gte": "now-7d",      "lt": "now"    }  }}建议：  统一使用 UTC 或明确时区；  混合系统时显式声明 format；  日志索引名带日期，同时保留 @timestamp 字段；  不要把日期同时作为 text。6.5 布尔、IP 与范围布尔：{ "is_deleted": { "type": "boolean" } }IP：{ "client_ip": { "type": "ip" } }IP 查询：{  "term": { "client_ip": "192.168.1.10" }}范围字段：{  "valid_period": {    "type": "date_range",    "format": "strict_date_optional_time"  }}范围字段适合日程、价格区间、有效期等场景。6.6 object 与 nested普通对象：{  "seller": {    "id": 100,    "name": "Nova 旗舰店",    "city": "Shanghai"  }}Mapping：{  "seller": {    "properties": {      "id": { "type": "long" },      "name": { "type": "keyword" },      "city": { "type": "keyword" }    }  }}普通对象数组会被扁平化：{  "skus": [    { "color": "red", "size": "S" },    { "color": "blue", "size": "L" }  ]}近似变成：skus.color = [red, blue]skus.size = [S, L]查询 color=red AND size=L 会错误匹配，应使用 nested：{  "skus": {    "type": "nested",    "properties": {      "color": { "type": "keyword" },      "size": { "type": "keyword" },      "price": { "type": "scaled_float", "scaling_factor": 100 }    }  }}查询：{  "nested": {    "path": "skus",    "query": {      "bool": {        "must": [          { "term": { "skus.color": "red" } },          { "term": { "skus.size": "S" } }        ]      }    }  }}nested 的成本：  每个 nested 对象是隐藏子文档；  数量大会增加索引体积；  nested query 和聚合更复杂；  返回文档时需要重建完整结构。如果数组对象只展示、不参与条件查询，可以设置：{  "skus": {    "type": "object",    "enabled": false  }}6.7 join 字段ES 的 join 不是数据库 Join，而是父子文档关系。PUT /company{  "mappings": {    "properties": {      "relation": {        "type": "join",        "relations": {          "department": "employee"        }      }    }  }}父文档：PUT /company/_doc/dept_1{  "name": "Search Platform",  "relation": "department"}子文档：PUT /company/_doc/emp_1?routing=dept_1{  "name": "Tom",  "relation": {    "name": "employee",    "parent": "dept_1"  }}查询：GET /company/_search{  "query": {    "has_child": {      "type": "employee",      "query": {        "term": { "name.keyword": "Tom" }      }    }  }}限制：  父子必须路由到同一分片；  查询成本高；  更新和重建复杂；  大多数场景建议反范式或同步期展开。6.8 index、doc_values 与 storeindex{  "remark": {    "type": "keyword",    "index": false  }}index=false 不能被搜索，但仍会出现在 _source。适合原始报文、大对象快照、只用于展示的扩展字段。doc_values{  "price": {    "type": "double",    "doc_values": false  }}Doc Values 支持排序和聚合，默认开启。确定不排序、不聚合的字段可以关闭以节省磁盘。store{  "title": {    "type": "text",    "store": true  }}默认 _source 已保存原始文档，通常不需要 store=true。只有 _source 很大且只需要取少量字段时才考虑。6.9 norms 与 term_vectorsnorms 参与字段长度归一化和 BM25 评分：{  "description": {    "type": "text",    "norms": false  }}关闭后可节省空间，但相关性计算会改变，适合纯召回或过滤型 text 字段。term_vectors：{  "content": {    "type": "text",    "term_vector": "with_positions_offsets"  }}term_vectors 支持更多高亮和分析能力，但会增加索引体积，应按需开启。6.10 dynamic mapping动态映射策略：{  "mappings": {    "dynamic": "strict",    "properties": {      "title": { "type": "text" }    }  }}            值      行为                  true      自动添加字段              false      忽略新字段              runtime      新字段作为运行时字段              strict      写入未知字段直接拒绝      动态模板：{  "mappings": {    "dynamic_templates": [      {        "strings_as_keywords": {          "match_mapping_type": "string",          "mapping": {            "type": "keyword",            "ignore_above": 128          }        }      },      {        "ids_as_keyword": {          "match": "*_id",          "mapping": {            "type": "keyword"          }        }      },      {        "times_as_date": {          "match": "*_at",          "mapping": {            "type": "date"          }        }      }    ]  }}生产建议：  主业务索引使用 strict；  日志索引起始 Mapping 明确核心字段；  未知字段可使用 runtime 或 false；  限制字段总数；  定期检查字段增长。6.11 runtime field运行时字段在查询时计算，不占用索引存储。PUT /orders/_mapping{  "runtime": {    "amount_with_tax": {      "type": "double",      "script": {        "source": "emit(doc['amount'].value * 1.13)"      }    }  }}查询：GET /orders/_search{  "query": {    "range": {      "amount_with_tax": {        "gte": 100      }    }  }}优点是不需要重建索引，适合临时字段；缺点是每次查询计算，数据量大时性能差。长期使用的字段应固化到 Mapping 并重新索引。6.12 Mapping 修改规则不能直接修改：  字段类型；  分词器；  index 从 false 改 true；  doc_values 从 false 改 true。可以新增字段：PUT /products/_mapping{  "properties": {    "season": {      "type": "keyword"    }  }}如果必须修改已有字段类型，需要创建新索引、Reindex、校验、切换别名。详见第 18 章。6.13 元字段            字段      说明                  _id      文档 ID              _source      原始 JSON              _routing      路由              _seq_no      序列号              _primary_term      主分片任期              _ignored      被 ignore_above 忽略的字段      建议保留 _source。禁用后更新、高亮、重建索引都会受限。大字段可以使用 index=false 或 enabled=false，不要轻易关闭 _source。6.14 Mapping 设计流程推荐步骤：收集字段来源和业务含义-&gt; 标记查询类型：搜索/过滤/聚合/排序/展示-&gt; 选择字段类型-&gt; 设计多字段结构-&gt; 确定 dynamic 策略-&gt; 确定 object/nested-&gt; 确定日期和金额格式-&gt; 建立索引模板-&gt; 写入样本数据-&gt; 验证查询、排序、聚合字段设计表模板：            字段      类型      搜索      过滤      聚合      排序      说明                  title      text + keyword      是      否      keyword 可以      否      ik_max_word              brand      keyword      否      是      是      是      品牌              price      scaled_float      否      是      是      是      两位小数              status      keyword      否      是      是      否      枚举              description      text      是      否      否      否      norms 可关      6.15 本章小结  Mapping 是搜索系统的基础设计，必须在写入前明确；  text 用于分词搜索，keyword 用于精确过滤、排序和聚合；  金额优先使用 scaled_float，日期统一格式和时区；  对象数组需要保留内部关系时使用 nested；  父子 join 成本高，优先反范式设计；  已有字段类型和分词器不能直接修改，需要重建索引；  runtime field 灵活但昂贵，长期字段应固化。6.16 思考题  为什么状态字段用 keyword 而不是 text？  title 需要“搜索标题”和“按完整标题聚合”两种能力，应如何设计？  商品 SKU 的颜色和尺码组合查询为什么必须使用 nested？  禁用 _source 会影响哪些功能？  Mapping 错误后为什么不能原地修改？标准处理流程是什么？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。第 4 章已经见过基础 API。本章系统讲解文档写入语义、自动 ID 与手动 ID、创建、替换、局部更新、批量保存、乐观并发、路由、刷新和失败处理。5.1 文档写入 API 对比            API      行为      适用                  POST /{index}/_doc      自动生成 ID      日志、事件、无需业务主键              PUT /{index}/_doc/{id}      不存在创建，存在整体替换      数据库同步、幂等写入              PUT /{index}/_create/{id}      只创建，存在则报错      防止覆盖              POST /{index}/_update/{id}      合并局部字段或执行脚本      少量字段更新              POST /_bulk      批量执行 index/create/update/delete      高吞吐写入      5.2 自动 ID 与业务 ID自动 ID：POST /events/_doc{  "level": "INFO",  "message": "user login",  "timestamp": "2026-08-25T10:00:00Z"}优点是写入路径简单，适合只追加的日志和事件；缺点是不直观、无法天然幂等。业务 ID：PUT /products/_doc/10001{  "product_id": 10001,  "title": "轻薄笔记本电脑",  "price": 6999,  "status": "ON_SALE"}第一次返回 result=created、_version=1；再次写入同 ID 返回 result=updated、_version=2。这里的 updated 表示替换了旧文档，即使 JSON 完全相同也会生成新版本。商品、订单、用户等数据库实体同步通常使用业务 ID；日志、埋点、审计事件通常使用自动 ID。5.3 创建语义只允许创建：PUT /products/_create/10002{  "product_id": 10002,  "title": "游戏手机",  "price": 4999}已存在时返回 409 Conflict。等价写法：PUT /products/_doc/10002?op_type=create{  "product_id": 10002,  "title": "游戏手机",  "price": 4999}适合初始化、迁移和首次导入。5.4 局部更新使用 doc 覆盖部分字段：POST /products/_update/10001{  "doc": {    "price": 6899,    "tags": ["轻薄", "新品"]  }}删除字段：POST /products/_update/10001{  "script": {    "source": "ctx._source.remove('tags')"  }}增量更新：POST /products/_update/10001{  "script": {    "source": "ctx._source.sales += params.count",    "params": { "count": 1 }  }}数组去重添加：POST /products/_update/10001{  "script": {    "source": """      if (ctx._source.tags == null) {        ctx._source.tags = new ArrayList();      }      if (!ctx._source.tags.contains(params.tag)) {        ctx._source.tags.add(params.tag);      }    """,    "params": { "tag": "轻薄" }  }}doc_as_upsert：POST /products/_update/10003{  "doc": {    "product_id": 10003,    "title": "机械键盘",    "price": 499,    "status": "ON_SALE"  },  "doc_as_upsert": true}如果文档不存在则插入，存在则合并更新。5.5 更新的内部流程Elasticsearch 文档不可变，局部更新流程近似为：定位主分片-&gt; 读取现有 _source-&gt; 合并修改-&gt; 标记旧文档删除-&gt; 写入新文档-&gt; 写 translog-&gt; 同步副本-&gt; 返回客户端因此 _update 不是原地修改某个字段，而是重新索引整个文档。设计约束：  大文档频繁更新代价高；  nested 对象越多更新成本越高；  高频计数和状态变更应先聚合；  并发更新会出现版本冲突；  原始字段最好能在同步层完整重建。5.6 乐观并发控制ES 使用 _seq_no 和 _primary_term 实现乐观并发。先读取文档并记录：_seq_no = 42_primary_term = 1带条件更新：POST /products/_update/10001?if_seq_no=42&amp;if_primary_term=1{  "doc": {    "price": 6799  }}如果中间已有其他更新，本次请求返回 409 Conflict。简单增量更新可以自动重试：POST /products/_update/10001?retry_on_conflict=3{  "script": {    "source": "ctx._source.sales += params.count",    "params": { "count": 1 }  }}如果上游数据库有版本号，可使用外部版本：PUT /products/_doc/10001?version=10&amp;version_type=external{  "product_id": 10001,  "title": "轻薄笔记本电脑",  "price": 6799}只有新版本号大于当前版本号才写入，适合数据库到 ES 的单向同步。5.7 自定义路由默认路由值是 _id：shard = hash(routing) % number_of_primary_shards自定义路由：PUT /orders/_doc/O10001?routing=U8888{  "order_id": "O10001",  "user_id": "U8888",  "amount": 199}读取也必须携带：GET /orders/_doc/O10001?routing=U8888优点：  同一用户文档落在同一分片；  按用户查询时只命中部分分片；  可优化租户隔离。风险：  路由值分布不均导致数据倾斜；  忘记 routing 读取不到文档；  大租户可能打爆单个分片；  查询灵活性下降。多租户系统更常见的是按租户建索引或使用权限过滤，而不是过度依赖自定义路由。5.8 刷新策略写入时可以指定 refresh：PUT /products/_doc/10004?refresh=true{  "title": "无线鼠标"}可选值：            值      行为      适用                  true      请求后立即 refresh      测试、低频关键数据              wait_for      等待下一次 refresh      需要可见性，又不想强制刷新              false      默认，不触发      大多数批量写入      示例：PUT /products/_doc/10005?refresh=wait_for{  "title": "显示器"}高吞吐批量写入不要每条都 refresh=true，否则会产生大量小 segment，导致性能下降。5.9 Bulk 与失败处理批量写入：POST /_bulk{"index": {"_index": "products", "_id": "10001"}}{"product_id": 10001, "title": "轻薄笔记本", "price": 6999, "status": "ON_SALE"}{"index": {"_index": "products", "_id": "10002"}}{"product_id": 10002, "title": "游戏手机", "price": 4999, "status": "ON_SALE"}{"index": {"_index": "products", "_id": "10003"}}{"product_id": 10003, "title": "机械键盘", "price": 499, "status": "OFF_SALE"}返回中必须检查 errors 和每个 item.status：{  "took": 30,  "errors": true,  "items": [    {      "index": {        "status": 429,        "error": {          "type": "es_rejected_execution_exception",          "reason": "rejected execution of coordinating operation"        }      }    }  ]}Java 侧示意：BulkResponse response = client.bulk(request);if (response.errors()) {    for (BulkItemResponse item : response.items()) {        if (item.isFailed()) {            log.warn("bulk failed index={} id={} reason={}",                    item.index(), item.id(), item.failure().message());            failedBuffer.add(item);        }    }}HTTP 200 只表示请求被处理，不代表所有操作成功。5.10 删除文档按 ID 删除：DELETE /products/_doc/10003按查询删除：POST /products/_delete_by_query{  "query": {    "term": {      "status": "OFF_SALE"    }  }}生产建议：  时间序列数据优先整索引删除；  _delete_by_query 会产生标记删除和合并压力；  低峰执行；  控制批次和并发；  任务失败要记录断点。5.11 同步文档元数据实践推荐携带：{  "id": 10001,  "version": 128,  "updated_at": "2026-08-25T10:00:00Z",  "synced_at": "2026-08-25T10:00:01Z",  "source": "mysql",  "deleted": false,  "title": "轻薄笔记本"}价值：  排查同步链路；  判断数据新旧；  支持重放和幂等；  支持软删除过滤；  重建索引时校验一致性。5.12 常见错误            错误      原因      处理                  409 Conflict      版本冲突      重新读取或使用上游版本              document_missing_exception      更新时文档不存在      使用 upsert 或先初始化              mapper_parsing_exception      字段类型不匹配      修正 Mapping 和数据              illegal_argument_exception      脚本或参数错误      检查 Painless 语法              es_rejected_execution_exception      写入队列满      降低并发或扩容              cluster_block_exception      磁盘满或索引只读      处理磁盘和 block      5.13 本章小结  自动 ID 适合日志和事件，业务 ID 适合实体同步；  _doc/{id} 是整体替换，_create 是只创建，_update 是局部合并；  更新本质是重新索引整个文档，高频更新应先聚合；  使用 _seq_no、_primary_term 或外部版本实现并发控制；  自定义路由可以优化局部查询，但会带来数据倾斜风险；  bulk 响应必须逐条检查失败项。5.14 思考题  为什么 Elasticsearch 无法像 MySQL 一样原地更新一个字段？  上游 MySQL 有 updated_at 和自增版本，你会如何设计 ES 幂等同步？  refresh=true 和 refresh=wait_for 有什么区别？  高频订单状态变更是否适合每次 _update？如何优化？  为什么 bulk HTTP 200 不代表全部写入成功？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章通过一组可执行请求熟悉 Elasticsearch 的基础 API：集群检查、创建索引、写入文档、查询文档、更新文档、删除文档、批量操作和搜索分析。4.1 请求格式Elasticsearch 提供 REST API，常见形式：GET /_cluster/healthPUT /productsPOST /products/_search使用 curl：curl -X GET "http://localhost:9200/_cluster/health?pretty"curl -X PUT "http://localhost:9200/products" \  -H 'Content-Type: application/json' \  -d '{"settings":{"number_of_shards":1}}'使用 Kibana Dev Tools 时可以省略主机地址：GET /_cluster/health4.2 检查集群GET /GET /_cluster/health?prettyGET /_cat/nodes?vGET /_cat/indices?vGET /_cat/shards?v解释：            API      用途                  GET /      查看版本与集群名              _cluster/health      查看健康状态              _cat/nodes      查看节点              _cat/indices      查看索引              _cat/shards      查看分片分布      4.3 创建索引PUT /books{  "settings": {    "number_of_shards": 1,    "number_of_replicas": 0  },  "mappings": {    "properties": {      "title": {        "type": "text",        "analyzer": "standard"      },      "author": { "type": "keyword" },      "price": { "type": "double" },      "tags": { "type": "keyword" },      "published_at": { "type": "date" }    }  }}查看设置：GET /books/_settings查看 Mapping：GET /books/_mapping删除索引：DELETE /books4.4 写入第一条文档自动生成 ID：POST /books/_doc{  "title": "Elasticsearch 实战教程",  "author": "张三",  "price": 99.0,  "tags": ["搜索", "Elasticsearch"],  "published_at": "2026-08-25T10:00:00Z"}指定 ID：PUT /books/_doc/1{  "title": "分布式搜索原理",  "author": "李四",  "price": 129.0,  "tags": ["分布式", "搜索"],  "published_at": "2026-08-20T10:00:00Z"}返回示例：{  "_index": "books",  "_id": "1",  "_version": 1,  "result": "created",  "_shards": {    "total": 2,    "successful": 1,    "failed": 0  },  "_seq_no": 0,  "_primary_term": 1}PUT /_doc/{id} 是 Index 操作：不存在则创建，存在则整体替换并版本加一。4.5 读取文档按 ID 读取：GET /books/_doc/1只读 _source：GET /books/_source/1选择部分字段：GET /books/_doc/1?_source=title,price判断存在：HEAD /books/_doc/1不存在文档时：{  "_index": "books",  "_id": "999",  "found": false}4.6 更新文档局部更新：POST /books/_update/1{  "doc": {    "price": 119.0  }}脚本更新：POST /books/_update/1{  "script": {    "source": "ctx._source.price += params.increase",    "lang": "painless",    "params": {      "increase": 10    }  }}upsert：POST /books/_update/2{  "doc": {    "price": 89.0  },  "doc_as_upsert": true}ES 的更新并不是原地修改字段。流程近似为：读取旧文档 -&gt; 合并修改 -&gt; 删除旧版本 -&gt; 写入新版本频繁更新会带来额外索引成本。4.7 删除文档DELETE /books/_doc/1按查询删除：POST /books/_delete_by_query{  "query": {    "term": {      "author": "李四"    }  }}_delete_by_query 会占用较多资源，生产环境应控制批次和并发，避免影响在线查询。4.8 批量写入_bulk API 可以一次提交多个操作。POST /_bulk{"index": {"_index": "books", "_id": "3"}}{"title": "日志检索与可观测性", "author": "王五", "price": 89.0, "tags": ["日志", "运维"], "published_at": "2026-08-19T10:00:00Z"}{"index": {"_index": "books", "_id": "4"}}{"title": "聚合分析入门", "author": "赵六", "price": 79.0, "tags": ["分析"], "published_at": "2026-08-18T10:00:00Z"}{"update": {"_index": "books", "_id": "3"}}{"doc": {"price": 85.0}}{"delete": {"_index": "books", "_id": "4"}}注意：  每行必须是完整 JSON；  请求体不能格式化为多行 JSON；  批量请求不宜过大，常见 5-15MB；  返回中需要逐条检查失败项；  bulk 是批量提交，不保证所有操作在同一事务中成功。4.9 第一条搜索请求GET /books/_search{  "query": {    "match": {      "title": "搜索"    }  }}返回结构：{  "took": 12,  "timed_out": false,  "hits": {    "total": {      "value": 1,      "relation": "eq"    },    "max_score": 0.87,    "hits": [      {        "_index": "books",        "_id": "3",        "_score": 0.87,        "_source": {          "title": "日志检索与可观测性"        }      }    ]  }}关键字段：            字段      含义                  took      查询耗时，单位毫秒              timed_out      是否超时收集完结果              hits.total      命中数量              max_score      最高得分              hits.hits      当前页文档              _score      相关性得分              _source      原始文档      4.10 组合过滤与排序GET /books/_search{  "query": {    "bool": {      "must": [        { "match": { "title": "搜索" } }      ],      "filter": [        { "range": { "price": { "lte": 100 } } }      ]    }  },  "sort": [    { "published_at": { "order": "desc" } }  ],  "size": 10}must 参与相关性计算，filter 不参与评分且更容易被缓存。4.11 简单聚合GET /books/_search{  "size": 0,  "aggs": {    "by_author": {      "terms": {        "field": "author",        "size": 10      }    },    "avg_price": {      "avg": {        "field": "price"      }    }  }}说明：  size: 0 表示不要返回文档，只要聚合结果；  terms 聚合用于分组；  avg 聚合用于计算平均值；  terms 默认按桶内文档数倒序；  聚合字段需要 Doc Values，通常是 keyword 或数值类型。4.12 分页GET /books/_search{  "from": 0,  "size": 20,  "query": {    "match_all": {}  }}GET /books/_search{  "size": 20,  "query": { "match_all": {} },  "sort": [    { "published_at": "desc" },    { "_id": "asc" }  ],  "search_after": ["2026-08-25T10:00:00Z", "1"]}浅分页适合产品搜索前几页；深分页应使用 search_after，不要使用巨大的 from。4.13 请求超时与条件刷新手动刷新：POST /books/_refresh带超时查询：GET /books/_search?timeout=500ms{  "query": { "match_all": {} }}注意：查询超时并不等于取消所有底层执行，也不保证返回完整结果。生产上应结合慢日志、查询分析和资源隔离治理。4.14 常见错误            错误      原因      处理                  index_not_found_exception      索引不存在      检查索引名              mapper_parsing_exception      字段类型不匹配      检查 Mapping              illegal_argument_exception      text 直接聚合      使用 keyword 子字段              search_phase_execution_exception      查询语法或字段错误      查看返回 failure              es_rejected_execution_exception      写入或查询队列满      限流、扩容、降低并发              cluster_block_exception      磁盘水位或只读      检查磁盘和 block      4.15 本章小结  ES 通过 REST API 管理集群、索引、文档和搜索；  PUT _doc/{id} 是整体替换，POST _update 是局部更新；  更新和删除都有额外成本，频繁更新场景要谨慎设计；  _bulk 是批量写入的核心 API，但必须逐条检查失败；  match 用于全文搜索，bool.filter 用于不影响评分的条件过滤；  from/size 只适合浅分页，深分页使用 search_after。4.16 思考题  PUT _doc/1 连续执行三次，_version 会如何变化？  为什么 _bulk 不能把 JSON 格式化成多行？  must 和 filter 的核心区别是什么？  为什么按 ID GET 通常比搜索更快、更确定？  如果 bulk 返回 HTTP 200，是否代表所有操作都成功？为什么？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章搭建一个学习环境：Elasticsearch + Kibana，并介绍 Docker Compose、基础配置、安全认证、目录结构和常用验证命令。生产部署不只是“能启动”，还要规划 JVM、磁盘、网络、角色和安全策略。3.1 版本选择建议学习环境选择 8.x 最新稳定版。若公司仍使用 7.x，需要注意：            变化      7.x      8.x                                安全      可选，默认弱      默认开启 TLS 与认证                            客户端      High Level REST Client 常见      推荐 Java API Client                            类型      已基本移除      完全移除                            向量      支持有限      dense_vector 与 kNN 能力增强                            SQL/ES      QL      SQL 支持较成熟      ES      QL 逐步增强      示例版本：ELASTIC_VERSION: 8.15.03.2 Docker Compose 启动创建 docker-compose.yml：services:  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0    container_name: es01    environment:      - discovery.type=single-node      - xpack.security.enabled=false      - ES_JAVA_OPTS=-Xms1g -Xmx1g    ulimits:      memlock:        soft: -1        hard: -1    volumes:      - es-data:/usr/share/elasticsearch/data    ports:      - "9200:9200"  kibana:    image: docker.elastic.co/kibana/kibana:8.15.0    container_name: kibana01    environment:      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200    ports:      - "5601:5601"    depends_on:      - elasticsearchvolumes:  es-data:启动：docker compose up -ddocker compose psdocker logs -f es01访问：Elasticsearch: http://localhost:9200Kibana:        http://localhost:5601验证：curl http://localhost:9200curl http://localhost:9200/_cluster/health?pretty3.3 本机安装3.3.1 下载和解压wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.15.0-linux-x86_64.tar.gztar -xzf elasticsearch-8.15.0-linux-x86_64.tar.gzcd elasticsearch-8.15.03.3.2 创建运行用户Elasticsearch 不允许 root 直接启动。useradd elasticchown -R elastic:elastic /opt/elasticsearch-8.15.0su elastic3.3.3 修改配置编辑 config/elasticsearch.yml：cluster.name: es-learningnode.name: node-1path.data: /opt/elasticsearch-8.15.0/datapath.logs: /opt/elasticsearch-8.15.0/logsnetwork.host: 127.0.0.1http.port: 9200discovery.type: single-nodexpack.security.enabled: false启动：bin/elasticsearch后台启动：bin/elasticsearch -d -p pid.lock3.4 Kibana 安装解压并编辑 config/kibana.yml：server.name: kibanaserver.host: "0.0.0.0"server.port: 5601elasticsearch.hosts: ["http://127.0.0.1:9200"]启动：bin/kibana打开 Dev Tools：Management -&gt; Dev ToolsKibana Dev Tools 是学习 ES 最方便的入口，支持自动补全和格式化。3.5 三节点学习集群学习分布式行为时，单节点环境不够。可用 Docker Compose 启动三节点。示例 es01 配置：services:  es01:    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0    container_name: es01    environment:      - node.name=es01      - cluster.name=es-learning      - discovery.seed_hosts=es01,es02,es03      - cluster.initial_master_nodes=es01,es02,es03      - bootstrap.memory_lock=true      - xpack.security.enabled=false      - ES_JAVA_OPTS=-Xms1g -Xmx1g    ulimits:      memlock:        soft: -1        hard: -1    ports:      - "9200:9200"es02 和 es03 类似，修改：container_name: es02 / es03node.name: es02 / es03ports: 9201:9200 / 9202:9200cluster.initial_master_nodes 只在集群首次启动时使用，生产集群扩容后不要随意修改。3.6 基础配置详解3.6.1 集群与节点cluster.name: es-learningnode.name: node-1network.host: 0.0.0.0http.port: 9200transport.port: 9300discovery.seed_hosts:  - 10.0.0.11:9300  - 10.0.0.12:9300  - 10.0.0.13:9300cluster.initial_master_nodes:  - node-1  - node-2  - node-3说明：            配置      作用                  cluster.name      集群名称，同集群必须一致              node.name      节点名称，必须唯一              discovery.seed_hosts      种子节点列表              cluster.initial_master_nodes      首次启动时候选 master              network.host      绑定地址              http.port      REST API 端口              transport.port      节点通信端口      3.6.2 路径path.data: /data/elasticsearchpath.logs: /var/log/elasticsearch生产要求：  数据目录使用独立磁盘或卷；  避免数据目录和日志目录放满；  目录属主必须是运行用户；  不要把数据目录放在 NFS 等不满足一致性语义的文件系统上。3.6.3 内存bootstrap.memory_lock: true环境变量：ES_JAVA_OPTS=-Xms8g -Xmx8g推荐通过 jvm.options 或 jvm.options.d 配置：-Xms8g-Xmx8g经验：  Xms 与 Xmx 设置一致，避免堆动态伸缩；  堆内存通常不超过物理内存 50%，剩余留给操作系统 Page Cache；  单节点堆通常不超过 31GB，具体与压缩指针边界有关；  交换内存应禁用或严格控制。3.6.4 系统 vm.max_map_countsudo sysctl -w vm.max_map_count=262144echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-elasticsearch.confLucene 需要大量内存映射文件，该参数过小会导致启动失败。3.6.5 文件描述符ulimit -n 65535生产建议至少 65535，可通过 systemd 配置：[Service]LimitNOFILE=65535LimitNPROC=4096LimitMEMLOCK=infinity3.7 安全启动Elasticsearch 8 默认开启安全。首次启动时会输出密码和 enrollment token。重置 elastic 用户密码：bin/elasticsearch-reset-password -u elastic生成 Kibana enrollment token：bin/elasticsearch-create-enrollment-token -s kibana带认证访问：curl -u elastic:'password' https://localhost:9200 -k正式环境建议：  启用 TLS；  使用独立 CA；  每个节点有独立证书；  Kibana 与 ES 之间加密通信；  应用使用独立用户和最小权限；  不在公网暴露 9200。开发环境可以临时关闭安全：xpack.security.enabled: false生产环境不要这样做。3.8 目录结构elasticsearch-8.15.0/ +-- bin/          可执行脚本 +-- config/       配置文件 +-- data/         索引数据 +-- jdk/          内置 JDK +-- lib/          Java 依赖 +-- logs/         日志 +-- modules/      内置模块 +-- plugins/      插件常用命令：bin/elasticsearchbin/elasticsearch-plugin listbin/elasticsearch-plugin install analysis-ikbin/elasticsearch-reset-password -u elasticbin/elasticsearch-shard3.9 安装中文分词插件以 IK 为例，版本必须与 Elasticsearch 一致：bin/elasticsearch-plugin install \  https://get.infini.cloud/elasticsearch/analysis-ik/8.15.0重启：bin/elasticsearch -d测试：POST /_analyze{  "analyzer": "ik_max_word",  "text": "轻薄高刷新率笔记本电脑"}结果应包含：轻薄高刷新率笔记本笔记本电脑实际词库与插件版本有关，需以输出为准。3.10 常用验证命令查看节点GET /_cat/nodes?v输出示例：ip        heap.percent ram.percent cpu node.role master name127.0.0.1           23          67   8 cdfhilmrstw *      node-1查看索引GET /_cat/indices?v查看分片GET /_cat/shards?v查看健康GET /_cluster/health?pretty查看分配失败原因GET /_cluster/allocation/explain?pretty3.11 常见启动问题            问题      原因      处理                  vm.max_map_count 太小      系统参数未调整      调整为 262144              启动后立刻退出      root 用户启动      切换非 root 用户              内存不足      JVM 或容器限制过小      增加内存或降低学习配置              文件描述符不足      ulimit 太低      调整 LimitNOFILE              集群一直加入失败      seed hosts 或网络不通      检查 transport 端口              Kibana 连不上      地址或证书错误      检查 elasticsearch.hosts              磁盘水位导致分片不分配      磁盘占用高      清理或扩容磁盘      3.12 学习数据准备创建索引：PUT /products{  "settings": {    "number_of_shards": 1,    "number_of_replicas": 0  },  "mappings": {    "properties": {      "title": { "type": "text" },      "brand": { "type": "keyword" },      "price": { "type": "double" },      "status": { "type": "keyword" },      "created_at": { "type": "date" }    }  }}写入文档：POST /products/_bulk{"index": {"_id": "1"}}{"title": "轻薄笔记本电脑", "brand": "Nova", "price": 6999, "status": "ON_SALE", "created_at": "2026-08-25T10:00:00Z"}{"index": {"_id": "2"}}{"title": "游戏手机", "brand": "Mars", "price": 4999, "status": "ON_SALE", "created_at": "2026-08-24T10:00:00Z"}{"index": {"_id": "3"}}{"title": "机械键盘", "brand": "Nova", "price": 499, "status": "OFF_SALE", "created_at": "2026-08-23T10:00:00Z"}查询：GET /products/_search{  "query": {    "match": {      "title": "笔记本"    }  }}更完整的查询语法见第 8 章。3.13 本章小结  学习环境推荐使用 Docker Compose 启动 Elasticsearch 与 Kibana；  分布式学习需要三节点集群，生产集群要正确配置 discovery 和 initial master；  JVM 堆与 Page Cache 都重要，堆通常不超过物理内存一半；  vm.max_map_count、文件描述符和内存锁定是常见系统准备项；  8.x 默认开启安全，开发可关闭，生产必须启用 TLS、认证和权限；  IK 等插件版本必须与 ES 版本严格匹配。3.14 思考题  为什么 Elasticsearch 堆内存不建议超过物理内存的一半？  discovery.seed_hosts 和 cluster.initial_master_nodes 分别什么时候使用？  单节点环境索引副本设为 1 会出现什么现象？为什么？  如果 Kibana 无法连接 ES，你会按什么顺序排查？  生产环境关闭 security 可能带来哪些风险？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章把 Elasticsearch 的核心概念串成一条线：集群由节点组成，节点承载分片，分片是 Lucene 索引，索引用 Mapping 描述字段，字段决定了文档如何被索引和查询。理解这条线后，后面学习 CRUD、Query DSL、分片策略和性能调优都会顺畅很多。2.1 集群与节点集群是一组 Elasticsearch 进程的集合，由 cluster.name 标识。每个进程是一个节点，由 node.name 标识。cluster.name: es-learningnode.name: node-1network.host: 0.0.0.0节点类型：            节点角色      说明                  master      参与主节点选举，维护集群元数据              data      保存分片，执行写入、搜索和聚合              data_content      保存内容型数据，适合低更新、查询频繁索引              data_hot      保存热数据，高 IO 与 CPU              data_warm      保存温数据，查询仍频繁但写入少              data_cold      保存冷数据，读少、可接受较慢              data_frozen      保存冻结数据，部分依赖可搜索快照              ingest      执行 ingest pipeline 数据预处理              coordinating      接收请求、分发分片请求、归并结果              ml      执行机器学习作业              transform      执行转换任务      任何节点都可以承担 coordinating 职责。即使没有指定角色的节点，也能接收请求并协调分片执行。2.2 Index、Document 与 Field2.2.1 IndexIndex 是文档的逻辑集合，类似关系型数据库中的表。示例：productsordersapplogs-2026.08.25user-events-2026.08日志场景常按天或月建索引：applogs-2026.08.25applogs-2026.08.26业务搜索场景常使用一个较大的主索引加别名：products_v1products_v2alias: products2.2.2 DocumentDocument 是一条 JSON 文档，必须属于某个 Index。{  "product_id": 10001,  "title": "轻薄高性能笔记本电脑",  "brand": "Nova",  "price": 6999.00,  "status": "ON_SALE",  "tags": ["轻薄", "高刷新率"],  "created_at": "2026-08-25T10:00:00Z"}每个文档有元字段：            字段      说明                  _index      所属索引              _id      文档 ID              _version      版本号，用于乐观并发              _seq_no      主分片上的序列号              _primary_term      主分片任期              _source      原始 JSON              _routing      分片路由值      2.2.3 FieldField 是文档中的字段。Mapping 决定字段的类型、是否分词、是否聚合、日期格式等。同一字段在不同索引里可以有不同 Mapping，但在同一个索引中类型必须稳定。2.3 MappingMapping 类似表结构，但更关注“如何建索引”。PUT /products{  "mappings": {    "properties": {      "product_id": { "type": "long" },      "title": {        "type": "text",        "analyzer": "ik_max_word",        "fields": {          "keyword": { "type": "keyword", "ignore_above": 128 }        }      },      "brand": { "type": "keyword" },      "price": { "type": "scaled_float", "scaling_factor": 100 },      "status": { "type": "keyword" },      "tags": { "type": "keyword" },      "created_at": { "type": "date" }    }  }}常见字段类型：            类型      用途      能否分词      能否聚合                  text      全文搜索      是      默认不能直接聚合              keyword      精确值、排序、聚合      否      是              long / integer      数值      否      是              double / float      数值      否      是              scaled_float      固定小数精度的数值      否      是              date      时间      否      是              boolean      布尔      否      是              object      JSON 对象      按字段处理      视字段而定              nested      对象数组，保留内部关系      视字段而定      支持 nested 聚合              dense_vector      向量检索      否      专用 kNN      text 与 keyword 是最容易被误解的类型。手机号、订单号、状态、标签、类目 ID 通常用 keyword；标题、描述、正文通常用 text。2.4 动态映射如果不提前定义 Mapping，Elasticsearch 会根据第一次写入的字段值推断类型。PUT /dynamic-demo/_doc/1{  "title": "Elasticsearch 书籍",  "price": 89.5,  "tags": ["搜索", "ES"],  "created_at": "2026-08-25T10:00:00Z"}查看 Mapping：GET /dynamic-demo/_mapping动态映射适合快速实验，但生产环境应显式定义。原因：  字符串可能被推断成 text + keyword，不符合业务预期；  数字字符串可能被推断成 long；  错误 Mapping 后续不能原地修改；  字段无限增长会带来 Mapping 爆炸风险；  分词器无法按业务需求自动选择。可以限制动态行为：PUT /products{  "mappings": {    "dynamic": "strict",    "properties": {      "title": { "type": "text" }    }  }}dynamic 选项：            值      行为                  true      自动添加字段              false      忽略新字段，文档可写入但不索引该字段              runtime      新字段作为运行时字段处理              strict      写入未知字段直接拒绝      2.5 分片与副本每个索引由一个或多个主分片组成。products: 3 primary shards, 1 replica each示意：P0 P1 P2       主分片R0 R1 R2       副本分片主分片数量在索引创建后不能直接修改，只能通过 Reindex 或 Split/Shrink 调整。副本数量可以随时修改。PUT /products/_settings{  "index": {    "number_of_replicas": 1  }}分片的作用：  水平拆分数据；  并行执行查询；  将数据分布到多个节点；  通过副本提升读吞吐和容灾。分片的代价：  每个分片是一个 Lucene 实例，有内存和文件句柄开销；  查询需要扇出到更多分片，归并成本上升；  分片过多会加重主节点元数据压力；  小分片过多会造成磁盘碎文件和合并效率下降；  副本会增加磁盘占用和写入放大。经验建议：            指标      建议范围                  单分片大小      日志类 10-50GB，业务类 20-75GB              单节点分片数      小集群低于 600，具体取决于版本和内存              主分片数      按数据规模和写入吞吐规划              副本数      生产至少 1      2.6 路由与文档归属写入文档时，Elasticsearch 根据路由值决定文档进入哪个主分片：shard = hash(routing) % number_of_primary_shards默认 routing 是文档 _id。这意味着：  同一文档始终路由到同一主分片；  主分片数变化会改变路由结果；  自定义路由可以让相关文档落在同一分片；  自定义路由后读取也要携带相同 routing；  不合理的路由会造成数据倾斜。自定义路由：PUT /orders/_doc/10001?routing=user_8888{  "order_id": 10001,  "user_id": "user_8888",  "amount": 199.00}2.7 Segment 与近实时每个分片内部由多个 segment 组成。segment 是不可变的 Lucene 索引文件，写入后的数据经过 refresh 生成 segment 后才可搜索。Shard +-- Segment 0 +-- Segment 1 +-- Segment 2 +-- translog特点：  segment 不可修改；  删除是标记删除；  更新等价于新文档写入加旧文档标记删除；  segment merge 会合并小段，物理清理删除文档；  查询需要扫描多个 segment 并归并结果。相关机制：            机制      说明                  refresh      打开新 segment，使文档可搜索              flush      将 segment 持久化到磁盘并清空 translog              translog      事务日志，用于崩溃恢复              merge      合并 segment，降低文件数量      2.8 AliasAlias 是索引别名，可以指向一个或多个索引。POST /_aliases{  "actions": [    { "add": { "index": "products_v2", "alias": "products" } },    { "remove": { "index": "products_v1", "alias": "products" } }  ]}别名价值：  应用不直接依赖物理索引名；  支持 Reindex 后零停机切换；  一个别名可以查询多个时间索引；  写入别名只能指向单个索引；  可以配置过滤别名，实现逻辑视图。过滤别名示例：POST /_aliases{  "actions": [    {      "add": {        "index": "products_v2",        "alias": "on-sale-products",        "filter": { "term": { "status": "ON_SALE" } }      }    }  ]}2.9 Ingest PipelineIngest Pipeline 是写入前的数据预处理管道，可以解析字段、补默认值、转换格式、丰富数据。PUT /_ingest/pipeline/product-pipeline{  "processors": [    {      "set": {        "field": "ingested_at",        "value": "{{{_ingest.timestamp}}}"      }    },    {      "lowercase": {        "field": "brand"      }    }  ]}使用：PUT /products/_doc/1?pipeline=product-pipeline{  "title": "轻薄笔记本",  "brand": "NOVA"}适合轻量处理。复杂清洗、关联和标准化通常放在 Logstash、Flink 或业务服务中。2.10 数据模型设计原则2.10.1 面向查询设计Elasticsearch 不建议为了“像表结构”而设计，而应该围绕查询设计。问自己：  哪些字段需要全文搜索；  哪些字段需要精确过滤；  哪些字段需要聚合；  哪些字段需要排序；  哪些字段只需要保存不需要索引；示例：{  "title": {    "type": "text",    "analyzer": "ik_max_word",    "fields": {      "keyword": { "type": "keyword", "ignore_above": 128 }    }  },  "description": {    "type": "text",    "index": true  },  "status": { "type": "keyword" },  "price": { "type": "scaled_float", "scaling_factor": 100 },  "snapshot": { "type": "object", "enabled": false }}2.10.2 反范式Elasticsearch 不适合频繁 Join。常用做法是写入时把查询需要的冗余字段直接放到文档中。订单搜索文档示例：{  "order_id": "O10001",  "user_id": "U8888",  "user_name": "Tom",  "shop_id": "S100",  "shop_name": "Nova 旗舰店",  "amount": 6999.00,  "status": "PAID",  "items": [    { "sku_id": 1, "title": "轻薄笔记本", "price": 6999.00, "qty": 1 }  ]}如果冗余字段经常变化，需要评估更新成本和数据一致性。2.10.3 对象关系普通 object 会把数组中的对象扁平化：{  "items": [    { "sku": "A", "price": 10 },    { "sku": "B", "price": 20 }  ]}实际索引结构近似：items.sku = ["A", "B"]items.price = [10, 20]查询 sku=A AND price=20 会错误匹配。应使用 nested：{  "items": {    "type": "nested",    "properties": {      "sku": { "type": "keyword" },      "price": { "type": "scaled_float", "scaling_factor": 100 }    }  }}nested 保留对象内部关系，但每个 nested 对象会作为隐藏子文档索引，数量过多会增加内存和查询成本。2.11 集群状态与健康查看健康：GET /_cluster/health返回示例：{  "cluster_name": "es-learning",  "status": "green",  "timed_out": false,  "number_of_nodes": 3,  "active_primary_shards": 30,  "active_shards": 60,  "relocating_shards": 0,  "initializing_shards": 0,  "unassigned_shards": 0}            状态      含义                  green      主分片和副本都正常              yellow      主分片正常，部分副本未分配              red      部分主分片不可用，存在数据风险      健康状态是结果，不是原因。排查时要继续查看节点、磁盘、分配解释和索引级健康。2.12 本章小结  集群由节点组成，节点可以承担 master、data、ingest、coordinating 等角色；  Index 是文档集合，Document 是 JSON，Mapping 决定字段如何索引；  分片是物理存储和并行执行单元，副本提供容灾和读扩展；  主分片数量不能直接修改，路由公式决定文档归属；  Segment 不可变，refresh 使文档可搜索，flush 持久化，merge 合并段；  别名是零停机变更和逻辑视图的重要工具；  ES 数据模型应面向查询设计，多数场景选择反范式，复杂关系使用 nested 或同步期关联。2.13 思考题  为什么说 ES 的 Index 和数据库的“索引”不是同一个概念？  主分片数为什么不能随时修改？  text 与 keyword 分别适合什么字段？举出你业务中的五个例子。  普通 object 和 nested 的区别是什么？  如果一个索引有 1000 个 1GB 小分片，会带来哪些问题？</li>
  <li>这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Elasticsearch 常被简称 ES。它是一个基于 Lucene 构建的分布式搜索与分析引擎，对外提供 REST API，底层把数据拆成多个分片，分布在多个节点上执行搜索和聚合。本章回答四个问题：  Elasticsearch 到底是什么；  它和 MySQL、Redis、Kafka 有什么区别；  它擅长和不擅长什么场景；  学习它应该掌握哪些核心概念。1.1 从一个搜索场景开始假设电商系统里有 1 亿商品，用户在搜索框输入：轻薄 高刷新率 笔记本如果使用 MySQL 的 LIKE '%轻薄%'，会遇到几个问题：  前置 % 无法利用普通 B+Tree 索引；  无法分词，搜“笔记本”不一定能匹配“笔记本电脑”；  无法计算相关性，结果没有业务排序；  无法高效组合全文匹配、类目过滤、价格排序和聚合统计；  大量模糊查询会给数据库带来较大压力。Elasticsearch 的处理方式不同：GET /products/_search{  "query": {    "bool": {      "must": [        {          "multi_match": {            "query": "轻薄 高刷新率 笔记本",            "fields": ["title^3", "category", "brand", "description"]          }        }      ],      "filter": [        { "term": { "status": "ON_SALE" } },        { "range": { "price": { "gte": 3000, "lte": 12000 } } }      ]    }  },  "aggs": {    "brands": {      "terms": { "field": "brand.keyword" }    }  }}这条请求会同时完成：  对多个字段做全文匹配；  按状态和价格过滤；  计算相关性得分；  统计品牌分布；  将查询分发到相关分片并行执行。这就是 Elasticsearch 的典型能力。1.2 Elasticsearch 是什么可以把 Elasticsearch 理解为四层能力的组合：            层      能力      说明                  存储层      JSON 文档存储      面向文档，弱 Schema，支持 Mapping              索引层      Lucene 倒排索引      支持分词、全文搜索、词项定位              分析层      聚合与指标      Bucket、Metric、Pipeline、时序分析              分布式层      分片与副本      水平扩展、容灾、并行查询      Elasticsearch 不是“带搜索接口的 MySQL”，也不是“更快的 Redis”。它为了搜索和分析做了大量专用结构，因此有些操作非常快，有些操作非常昂贵。1.3 典型使用场景1.3.1 站内搜索场景：  商品搜索；  内容搜索；  用户搜索；  企业知识库；  代码搜索。核心需求：分词准确 -&gt; 召回相关 -&gt; 业务过滤 -&gt; 排序合理 -&gt; 聚合筛选项 -&gt; 响应低延迟1.3.2 日志检索与可观测性场景：  应用日志；  访问日志；  安全审计；  链路追踪；  指标与事件关联分析。典型链路：应用/Agent -&gt; Filebeat/OpenTelemetry -&gt; Kafka -&gt; Logstash/Connector -&gt; Elasticsearch -&gt; Kibana这类场景通常是写多读少、按时间分区、保留周期明确，适合索引生命周期管理。1.3.3 数据分析场景：  订单统计；  用户行为分析；  漏斗分析；  安全告警聚合；  运营看板。Elasticsearch 可以对大量文档执行聚合，但不是替代所有数仓的工具。它的优势是低延迟交互式分析，劣势是复杂 Join、大规模扫描和精确去重成本较高。1.3.4 安全与风控场景：  登录异常检测；  黑名单检索；  风控规则命中；  威胁狩猎；  审计追踪。常见组合是规则引擎、实时特征、Elasticsearch 检索和离线模型训练。1.3.5 向量与混合搜索Elasticsearch 8.x 增强 dense_vector 与 kNN 搜索，可用于：  语义搜索；  图片相似度；  推荐召回；  RAG 知识检索；  多路召回后统一排序。向量搜索通常和关键词搜索组合使用，也就是混合搜索。1.4 与其他系统对比1.4.1 Elasticsearch vs MySQL            维度      Elasticsearch      MySQL                         数据模型      JSON 文档      关系表                     查询语言      Query DSL、SQL、ES      QL      SQL              全文搜索      强，倒排索引与分词      弱，LIKE 或全文索引有限                     事务      不支持通用 ACID 事务      支持完整事务                     关联查询      弱，避免复杂 Join      强                     聚合分析      强，适合交互式分析      适合确定型报表                     更新      文档级替换，代价较高      行级更新成熟                     一致性      近实时，默认秒级可见      事务提交后可见             常见分工：MySQL：事实源、事务、账务、唯一约束Elasticsearch：搜索投影、分析视图、日志检索1.4.2 Elasticsearch vs Redis            维度      Elasticsearch      Redis                  定位      搜索与分析引擎      内存键值与缓存              延迟      通常毫秒到几百毫秒      亚毫秒到毫秒              数据结构      文档、倒排、向量      String、Hash、ZSet 等              全文搜索      强      基本不具备              聚合      强      有限              内存成本      JVM Heap + Page Cache      核心数据在内存      常见组合：Redis 缓存热点结果，Elasticsearch 承担搜索召回和聚合。1.4.3 Elasticsearch vs Kafka            维度      Elasticsearch      Kafka                  定位      搜索与分析存储      分布式事件日志              数据保留      索引生命周期      retention 滚动保留              消费模型      查询式拉取      offset 连续消费              回放      不适合作为原始事件回放源      强项              分析      强      本身不做分析      常见链路：业务 -&gt; MySQL -&gt; CDC -&gt; Kafka -&gt; ElasticsearchKafka 保证传输与回放，Elasticsearch 提供查询视图。1.4.4 Elasticsearch vs OpenSearchOpenSearch 是从 Elasticsearch 7.10 分叉出的开源项目，两者核心思想相近，但版本演进、许可证、API 细节、插件生态和云服务支持逐渐分化。选型时要关注：  目标版本许可证；  是否需要官方 Elasticsearch 能力；  Kibana 与 OpenSearch Dashboards 生态；  安全、SQL、向量检索能力；  云厂商托管方案；  团队运维经验。1.5 为什么搜索不能只靠数据库搜索系统通常分为四个阶段：理解查询 -&gt; 召回候选 -&gt; 过滤排序 -&gt; 返回结果与统计数据库更擅长“精确条件查询”，例如：SELECT * FROM product WHERE id = 10001;SELECT * FROM product WHERE category_id = 3 AND price &lt; 5000;搜索更擅长“模糊语义匹配”，例如：用户输入：苹果手机充电快匹配字段：title、brand、category、tags、description期望结果：相关品牌、相关类目、相关描述排序依据：文本相关性 + 销量 + 新品 + 库存 + 业务权重这需要分词、倒排索引、词频、文档频率、字段权重、过滤器缓存和聚合能力。Elasticsearch 正是围绕这些问题设计的。1.6 近实时是什么意思Elasticsearch 通常被称为近实时搜索引擎。写入文档后，数据先进入内存 buffer 和 translog，经过 refresh 生成可搜索的 segment。默认情况下，refresh 大约每秒一次，所以写入后通常需要等待不到一秒才能被搜索到。写入 -&gt; translog + buffer -&gt; refresh -&gt; 可搜索                  |                  +-&gt; flush -&gt; fsync + 清空 translog这不是“实时数据库”的强一致可见性。业务如果要求写入后立即可见，需要：  调用 GET 按 _id 读取；  手动 refresh，但这会带来性能成本；  在业务层等待或异步确认；  重新设计一致性链路。不要把 Elasticsearch 当作 MySQL 的强一致替代品。1.7 Elasticsearch 的基本组件            概念      类比      说明                  Node      数据库实例      一个 ES 进程              Cluster      数据库集群      一组 Node              Index      表或文档集合      逻辑搜索命名空间              Document      一行数据      JSON 文档              Field      列      文档中的字段              Mapping      表结构      字段类型与分析方式              Shard      分区      Lucene 实例，数据物理分片              Replica      副本      主分片拷贝，提供容灾和读并发              Segment      内部索引文件      不可变倒排索引单元              Alias      视图或软链接      指向一个或多个索引      注意：Index 与数据库中的“索引”不是同一个概念。ES 的 Index 更像“表的集合”，真正用于搜索的是每个分片里的 Lucene 索引。1.8 学习 Elasticsearch 的常见误区            误区      实际情况                  ES 可以替代数据库      大多数业务仍需要数据库作为事实源              分片越多越好      分片过多会增加内存、调度和查询开销              ES 查询永远很快      深分页、大聚合、 wildcard、脚本都可能很慢              Mapping 可以随便动态生成      错误类型会导致后续重建索引              写入后立刻能查到      默认近实时，通常有刷新延迟              副本越多越好      副本增加磁盘、写入放大和内存开销              聚合结果是绝对精确的      有些场景存在近似算法与终端归并误差      1.9 一个最小学习环境本章不展开安装细节，只给出学习路径：1. 启动 Elasticsearch2. 启动 Kibana3. 打开 Dev Tools4. 创建 products 索引5. 写入 3 条商品文档6. 执行 match 查询7. 执行 terms 聚合具体安装与配置见第 3 章。1.10 本章小结  Elasticsearch 是基于 Lucene 的分布式搜索与分析引擎，核心能力是全文搜索、聚合分析和水平扩展；  它适合站内搜索、日志检索、可观测性、交互式分析、安全风控和向量检索；  它不擅长通用事务、复杂关联、强一致写入和作为原始事实源；  常见架构是 MySQL 保存事实，Kafka 传输事件，Elasticsearch 提供搜索与分析视图；  ES 是近实时系统，写入可见性依赖 refresh；  学习 ES 必须同时理解文档模型、分词、倒排索引、分片、副本和查询执行成本。1.11 思考题  你的系统里哪些数据适合放 Elasticsearch？哪些必须留在 MySQL？  “写入成功”和“写入后立即可搜索”在 Elasticsearch 中是否等价？为什么？  商品搜索和订单报表对 ES 的要求有什么不同？  如果让你把 MySQL 商品表同步到 ES，你会选择全量、增量还是 CDC？  为什么不能简单说“Elasticsearch 比 MySQL 快”？</li>
</ul>
