ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Elasticsearch 8.x RESTful API 核心操作与避坑指南

Elasticsearch 8.x RESTful API 核心操作与避坑指南 我做 Elasticsearch 相关项目也有七八年了从 1.x 一路用到 8.x。这几年被问得最多的问题几乎都是同一个网上找的Elasticsearch 基本操作教程照着敲PUT /index/type/id怎么在 8.x 里直接报错答案很简单——Elasticsearch 8.x 这一代把 RESTful API 里很多沿用多年的习惯改了。这篇文章我就基于 Elasticsearch 8.x把最核心的 RESTful API 操作从头到尾捋一遍索引怎么建、文档怎么读写、查询怎么写、聚合怎么做、集群怎么查全部用实际请求和返回说话。适合刚上手 8.x 的开发者也适合从旧版本迁移过来、想确认哪些命令已经失效的老人。1. 为什么 8.x 的基本操作和你想的不一样1.1 安全认证默认开启第一次访问就被拦住了8.x 和之前版本最大的体感差异不是 API 变多而是安全认证默认开启了。7.x 及更早版本装完直接curl localhost:9200就能看到那个经典的 You Know, for Search 欢迎信息。8.x 装完你再这么干返回的是 401 未认证错误。这意味着你正式学习第一组 RESTful API 之前得先解决两个东西用户名密码以及证书信任。安装 8.x 后首次启动Elasticsearch 会在控制台打印一段高亮信息里面有elastic超级用户的初始密码还有一个 CA 证书指纹。很多人看到一长串随机密码直接无视等第二天重启就傻眼了——初始密码是一次性的丢了就得重置。我的建议是启动后先把这段输出保存到本地笔记本或者立刻用elasticsearch-setup-passwords工具手动改一套自己记得住的强密码。测试环境更省事的做法是在elasticsearch.yml里加一行xpack.security.enabled: false然后重启相当于回到 7.x 的无认证模式。不过这只是本地实验的临时方案生产环境千万别这么干。1.2 类型被移除URL 路径的变化比安全认证更隐蔽的变化是types类型彻底没了。ES 6.x 开始就已经官宣每个索引只能有一个类型7.x 开始建议用_doc占位8.x 直接移除。所以你在老教程里看到的这种写法PUT /product/electronics/1在 8.x 里会直接报404之类的错误。正确写法是PUT /product/_doc/1product是索引名_doc是固定端点1是文档 ID。别小看这个变化很多从 6.x 迁过来的人其他操作都没问题偏偏在这类老路径上浪费一晚上。记住一条铁律8.x 里索引名后面直接跟操作端点_doc、_search、_update、_delete不会再出现类型名。1.3 环境准备拿到密码和证书再动手在你敲第一条命令之前把环境准备好。我用的是 8.14 版本Linux / macOS 都适用。解压之后配置好JAVA_HOME8.x 自带 JDK所以这一步通常可忽略然后启动bin/elasticsearch看到日志里的status: green或者至少yellow就说明服务起来了。验证最基本的 RESTful APIcurl -k https://localhost:9200 -u elastic:你的密码-k是跳过证书校验本地测试图省事可以这么干。正式一点的写法是用自带的 CA 证书curl --cacert config/certs/http_ca.crt https://localhost:9200 -u elastic:你的密码响应里会出现cluster_name、cluster_uuid和version.number能看到8.14.0就说明连通成功。后面所有示例我都用 HTTP 方法和请求体来表示你在 Kibana 的 Dev Tools 里面可以直接粘贴执行如果要转成 curl在请求体外面包一层-d即可。Kibana Dev Tools 是我强烈推荐的学习工具自带语法高亮和格式化对路径参数也会提示哪些地方是非法的。2. 索引操作创建、查看、修改与删除2.1 创建索引从空索引到带映射Elasticsearch 的索引和 MySQL 的 database 加 table 差不多是一个混合体。创建索引最简单的方式是PUT /product返回{ acknowledged: true, shards_acknowledged: true, index: product }但实际工作中基本不会造这种空索引。因为索引的核心是 mapping——字段类型定义。没有 mapping 的索引写入数据时会触发动态映射虽然能用但经常给出出乎意料的类型后期很难改。更标准的做法是创建索引时就带上 mappingPUT /product { mappings: { properties: { title: { type: text, fields: { keyword: { type: keyword, ignore_above: 256 } } }, brand: { type: keyword }, price: { type: double }, stock: { type: integer }, created_at: { type: date } } } }这里有个经典设计title被定义成text加keyword子字段。text负责全文检索也就是搜索时可以把智能手机拆成智能和手机来匹配keyword负责精确匹配、聚合、排序。如果你只定义text而没有加.keyword后续想做按标题排序或聚合会非常痛苦。2.2 查询索引_cat系列命令索引建完怎么看它存不存在、状态好不好用_cat接口。GET /_cat/indices?v输出类似health status index uuid pri rep docs.count yellow open product xxxx 1 1 0health列是重点。green表示主分片和副本都正常yellow表示主分片正常但副本未分配red表示主分片出问题数据可能不可用。单节点集群里新建的索引默认是yellow因为副本要分配到另一个节点但只有一个节点副本放不出去。这不是故障后续要消除它要么增加节点要么把副本数改成 0。想查看某个索引的 mapping 和 settingsGET /product返回索引的mappings、settings和aliases。排查字段类型问题基本都靠这个接口。2.3 修改与删除索引一些限制索引有些参数可以动态调整比如副本数和刷新间隔PUT /product/_settings { number_of_replicas: 0, refresh_interval: 30s }但有几个东西创建后就不能改主分片数量、已有字段的类型。拿主分片数量来说5 个分片的索引想改成 10 个分片做不到只能重建索引。这也是为什么生产环境对分片规划要提前想清楚。删除索引的命令是DELETE /product返回acknowledged: true之后索引及内部数据就彻底没了。这条命令没有回收站除非你有快照备份否则误删就是事故。我以前见过有人把DELETE请求的索引名写错直接删掉了线上一个月的数据最后靠_reindex从备份里捞回来的。所以生产环境执行 DELETE强烈建议先GET /_cat/indices?v确认索引名。3. 文档的增删改查CUD 的完整链路3.1 写入文档三种方式的选择逻辑索引有了接下来就是往里写文档。文档在 Elasticsearch 里就是个 JSON 对象。指定 ID 写入PUT /product/_doc/1 { title: 智能手机, brand: 华为, price: 5999.00, stock: 100, created_at: 2024-06-01T10:00:00Z }返回结果里有几个关键信息{ _index: product, _id: 1, _version: 1, result: created }指定 ID 的写入是幂等的同一个 ID 再次写入result会变成updated版本号递增文档被整体覆盖。自动生成 IDPOST /product/_doc { title: 蓝牙耳机, brand: 索尼, price: 1200.00, stock: 50, created_at: 2024-06-02T10:00:00Z }区别在于用POST加/结尾ES 自动生成一个 20 字符的 ID。自动 ID 的好处是写入压力分散不需要做 UUID 生成坏处是你没法通过 ID 直接定位这个文档只能查出来之后再拿 ID。日志类数据我通常用自动 ID业务数据一般都用我们自己生成的业务 ID。强制新建PUT /product/_create/2 { title: 笔记本电脑, brand: 联想, price: 6999.00, stock: 30, created_at: 2024-06-03T10:00:00Z }如果 ID 已存在返回 409 Conflict文档不会被覆盖。_create适合做幂等写入场景比如消息队列消费者重复投递时第二次会直接失败省得你写判断逻辑。3.2 更新与删除底层机制和并发控制更新文档有两类常见写法。第一类是局部更新POST /product/_update/1 { doc: { price: 5199.00 } }只更新price字段其他字段不动。第二类是脚本更新适合需要基于当前值计算的场景比如库存减一POST /product/_update/1 { script: { source: ctx._source.stock - 1 } }理解底层机制比记住命令更重要Elasticsearch 的文档是不可变的。你执行 update底层其实是把旧文档标记为删除再写入一份新文档。这也是为什么每次更新后_version都会加一。所以频繁 update 的文档会带来额外的段合并开销写入密集场景要尽量避免无谓的小更新。删除很简单DELETE /product/_doc/1返回result: deleted。这里要注意的是被删除的文档并不是物理上立刻消失它和更新逻辑一样先标记删除后续由后台 merge 真正清理。刚删除完立刻做精确 ID 查询得到的是找不到的响应但做全文搜索时短期内可能仍能搜到刚删掉的文档这是正常现象。多实例并发修改同一文档时ES 提供了乐观并发控制。更新时带上上次返回的_seq_no和_primary_termPUT /product/_doc/1?if_seq_no27if_primary_term1 { ...完整文档... }如果期间文档被改过_seq_no对不上服务端返回 409请求失败。这比最后写入覆盖前一次的方式安全得多。3.3 批量操作_bulk的正确姿势单条写入在数据量小的时候没问题一旦有大量文档要灌进来一条条发 HTTP 请求就是灾难。_bulk接口是批量写入的标配。_bulk的格式非常特殊它不是标准 JSON而是NDJSON——每两行一组第一行是操作类型和文档元数据第二行是文档内容POST /product/_bulk {index: {_id: 3}} {title: 平板电脑, brand: 苹果, price: 3999.00, stock: 20, created_at: 2024-06-04T10:00:00Z} {update: {_id: 2}} {doc: {price: 999.00}}第一行可以用index、create、update、delete分别对应写入、强制新建、局部更新、删除。第三行及以后可以继续成对追加一次几百对完全没问题。用 curl 时_bulk请求体会很大-d参数容易遇到转义问题我用的是--data-binarycurl -k -X POST https://localhost:9200/product/_bulk \ -u elastic:你的密码 \ -H Content-Type: application/x-ndjson \ --data-binary bulk.json批量返回结果里会有errors: true/false这个字段。即使总体是 true也不代表全部失败可能是其中几条失败。真正需要逐条检查的是items数组里每条记录的status比如 409 说明版本冲突400 说明单条数据格式有问题。我在实际项目里批量导数据批次大小一般控制在 5000 到 10000 条之间太大反而会因为单次请求体过大触发内存问题。4. 查询与检索从 match 到 bool 的一整套 DSL4.1 搜索请求的基本结构搜索通过_search端点完成POST /product/_search请求体是一个 JSON最核心的是query对象。但搜索的返回结构也要搞清楚{ hits: { total: { value: 2, relation: eq }, hits: [ ...文档数组... ] } }total.value是命中的文档总数。8.x 里默认情况下这个数字是准确计数但如果你对超大索引设置了rest_total_hits_as_int或者用了track_total_hits参数它可能变成阈值估计值这点在对比返回条数和实际预期时要注意。_source默认会把完整文档返回。如果不想看到某个大字段或者只想要个别字段可以在请求体里过滤POST /product/_search { _source: [title, price], query: { match_all: {} } }match_all就是返回所有文档的意思适合先看数据概貌。实际查询中很少用它更多是配合空query来统计文档数。4.2 常用查询match、term、range、boolmatch 查询全文检索作用于text字段。POST /product/_search { query: { match: { title: 智能手机 } } }ES 会对智能手机进行分词然后匹配包含任一词或全部词的文档并计算相关度分数。适合搜标题、描述这类自然语言文本。term 查询精确匹配作用于keyword、数值、日期字段。POST /product/_search { query: { term: { brand: 华为 } } }term不会分词搜华为就是华为所以不能拿它对text字段做精确匹配——text字段已经被分词器切成一个个词了term 查不到完整字符串。range 查询范围过滤。POST /product/_search { query: { range: { price: { gte: 2000, lte: 6000 } } } }gt大于、gte大于等于、lt小于、lte小于等于灵活组合就行。bool 查询组合查询包含must、should、must_not、filter四种子句。它是我实际项目里用得最多的查询类型。POST /product/_search { query: { bool: { must: [ { match: { title: 手机 } } ], filter: [ { term: { brand: 华为 } }, { range: { price: { gte: 3000, lte: 8000 } } } ] } } }这里的重点是must和filter的区别must参与相关度评分会影响_scorefilter只做过滤不参与评分而且因为结果可以缓存性能通常更优。所以纯过滤条件尽量放进filter别全堆在must里。4.3 分页与排序深分页怎么避坑分页最直观的方式是from加sizePOST /product/_search { query: { match_all: {} }, sort: [ { price: { order: desc } } ], from: 0, size: 10 }from表示跳过多少条size表示返回多少条。配合sort按价格降序、升序都行。但这里坑很大from跳过的文档不是真跳过ES 要把每个分片上的from size条都收集到协调节点再统一排序、截断。from跑到 100000 以后性能和内存都会明显恶化。所以 8.x 里默认from size的最大值是 10000。深分页的正确姿势是search_after先按某个唯一排序值定位再取它后面的数据。第一次请求同样带排序POST /product/_search { query: { match_all: {} }, sort: [ { price: desc }, { _id: asc } ], size: 10 }返回的 hits 里每个文档的sort数组就是游标。下一页请求POST /product/_search { query: { match_all: {} }, sort: [ { price: desc }, { _id: asc } ], size: 10, search_after: [5999.0, 1] }注意排序中加一个唯一字段比如_id否则价格相同的文档之间顺序不稳定翻页会重或漏。search_after不能往前翻页只适合顺序遍历的业务场景比如后台导出或者瀑布流加载。5. 字段类型与映射规划5.1 核心字段类型怎么选字段类型的选择决定了后期能否高效查询和聚合。我整理了一张常用对照表类型适用场景典型示例注意事项text全文搜索、分词匹配标题、描述、正文不能被聚合/排序除非加.keyword子字段keyword精确匹配、聚合、排序品牌、状态、标签、ID不会分词整体作为一整个词匹配integer/long整数数值库存、数量取值范围不同超范围会报错double/float小数价格、评分精度敏感场景用scaled_float更可控date日期时间创建时间、更新时间默认支持多种格式但混用会出问题boolean布尔值上架/下架聚合、条件过滤都很直接objectJSON 子对象用户信息{ name: ... }默认扁平化存储子字段按user.name访问nested数组对象订单明细列表每个对象独立索引查询不会串数据geo_point经纬度门店位置支持距离排序、范围过滤选型的核心原则就一句话给查询和聚合用的字段选对类型只是存储展示的字段别过度设计。5.2 动态映射和显式映射8.x 写入新字段时如果索引里没有对应 mapping会自动动态映射。比如写入price: 99.9ES 自动设为float写入name: 张三自动设为text加keyword子字段。动态映射确实方便但坑也在这里。举个例子某个字段一开始全是数字ES 自动设为long。后来有人往里面写了N/AES 会拒绝这条文档吗默认会报错除非你在 mapping 里配置ignore_malformed: true。再比如某个字段一开始是字符串被映射成text后面你想对它做 range 查询直接报错。生产环境的建议非常明确核心索引都用显式映射创建动态映射留给开发环境。尤其是对接业务方数据时字段类型不是拍脑袋定的要跟查询场景走。往已有的索引里加新字段没问题PUT /product/_mapping { properties: { category: { type: keyword } } }这个操作只增不改对已有数据不会产生重建。但如果要改已有字段类型比如把price从long改成doubleES 是禁止的只能走 reindex。5.3 修改映射reindex 的完整流程改字段类型最稳的思路就是新建索引、同步数据、切换访问。流程分四步。第一步新建一个字段类型正确的新索引product_v2mapping 按正确方式定义。第二步用_reindex把旧索引数据拷贝过来POST _reindex { source: { index: product }, dest: { index: product_v2 } }_reindex会保留_id、_source内容但已经存在的分词结果、倒排索引不会带过来新索引会重新处理数据。这也是为什么 reindex 后搜索行为可能和原来略有差异。第三步确认新索引数据量一致GET /_cat/indices/product_v2?v看看docs.count和旧索引是否对得上。第四步切换访问。最简单的方式是 alias别名POST /_aliases { actions: [ { remove: { index: product, alias: product_read } }, { add: { index: product_v2, alias: product_read } } ] }切换之后应用层继续访问product_read底层已经指向新索引。旧索引先保留几天确认无异常再 DELETE。reindex 在高并发场景下会占用不少资源建议控制requests_per_second参数POST _reindex { source: { index: product }, dest: { index: product_v2 }, conflicts: proceed }conflicts: proceed表示更新过程中如果遇到版本冲突跳过而不是中断整个任务。数据从几万到百万级别都能这么跑几千万以上建议用 split 成多次 slice 并发执行以后再单独聊。6. 聚合分析从统计视角看数据6.1 聚合的语法结构Elasticsearch 不只会查文档还自带统计分析能力用aggs节点表达。基本结构长这样POST /product/_search { size: 0, aggs: { 自定义聚合名: { 聚合类型: { ...参数... } } } }size: 0是聚合查询的常用套路意思是不看命中明细只看聚合结果。如果你不写size: 0返回里会带上大量文档明细既浪费带宽又拖慢响应。聚合分两大类。桶聚合bucket把数据按规则分到不同的组里相当于 SQL 的GROUP BY。指标聚合metric对一组数据做统计相当于AVG、SUM、MIN、MAX。两个经常组合使用先分桶再对桶内做指标统计。6.2 常用聚合示例按品牌统计商品数量POST /product/_search { size: 0, aggs: { brand_count: { terms: { field: brand } } } }brand是keyword类型才能聚合。如果你用的是动态映射出来的text字段这里就该写brand.keyword。输出里buckets数组会列出每个品牌的key和doc_count。统计价格平均值POST /product/_search { size: 0, aggs: { avg_price: { avg: { field: price } } } }一次拿到多种统计指标可以用statsPOST /product/_search { size: 0, aggs: { price_stats: { stats: { field: price } } } }返回count、min、max、avg、sum五个值省得写五个聚合。聚合和查询可以同时存在。典型场景是先限定某个价格区间再按品牌分桶同时统计每个品牌的平均价格POST /product/_search { size: 0, query: { range: { price: { gte: 1000 } } }, aggs: { brand_count: { terms: { field: brand }, aggs: { avg_price: { avg: { field: price } } } } } }先跑query过滤再跑aggs这是聚合查询性能优化的基本姿势。不管数据有多少条先缩小范围再聚合能省下大量计算资源。7. 集群健康与节点管理7.1 集群健康状态单节点玩得差不多了就该看看集群层面的东西。最常用的健康检查命令GET /_cluster/health返回{ cluster_name: elasticsearch, status: yellow, number_of_nodes: 1, active_shards: 5, active_shards_percentage_as_number: 50.0 }status是你最需要盯的指标green所有主分片和副本都正常。yellow所有主分片正常但有副本未分配。单节点集群最常见因为副本没地方放。red有主分片未分配直接意味着部分数据不可读写。出现 red 时优先检查是哪个索引、哪个分片用GET /_cat/shards?v往下查。我这里多说一句很多人一看到 yellow 就慌其实在单节点测试环境里 yellow 是常态。生产环境出现 yellow 才需要重视原因通常是节点宕机、磁盘满、副本配置错误。7.2 节点、分片和索引状态排查查看节点状态GET /_cat/nodes?v输出包含 ip、heap.percent、ram.percent、cpu、load_1m 等指标。排查性能问题时第一个动作就是看这里。比如heap.percent长期超过 85%就要考虑要不要加节点、调 JVM 堆大小。查看分片分布GET /_cat/shards?v可以看到每个索引的分片都落在哪个节点上、是主分片还是副本、占用磁盘多大。索引变红时_cat/shards会显示哪些分片没有分配到节点或者显示UNASSIGNED状态。查看索引层面的状态GET /_cat/indices?v这个命令前面提过但它也是排查的起点。pri.store.size可以看索引占用空间docs.count可以看文档数。当磁盘告警时先用它找出占用最大的索引再决定做删除、归档还是 reindex。8. 生产环境要避开的坑8.1 安全配置别裸奔8.x 默认安全是开着的但总有人为了图省事把xpack.security.enabled设成 false。如果是公司公网环境这是非常危险的操作。我见过不止一次集群裸奔在公网上被扫描到之后直接被删库里面被塞了一个勒索提醒。至少要做到几件事保存好elastic初始密码或者用密码重置工具改成自己的强密码。给应用申请专用账号或 API key不要所有调用都拿超级用户。开启 TLS/HTTPS应用连接时配置证书。访问连接配置里用 API key 比用户名密码更适合应用侧因为 API key 可以设置过期时间、只授权特定索引的权限泄露后也方便吊销。创建 API key 的接口POST /_security/api_key { name: my_app_key, role_descriptors: { product_only: { indices: [ { names: [product*], privileges: [read, write] } ] } } }返回的id和api_key组合成Authorization: ApiKey base64(id:api_key)头发送请求即可。8.2 分片与写入性能调优分片多少合适这个问题没有绝对标准但有一条原则分片不是越多越好。每个分片都有对应的 Lucene 段文件、文件句柄、内存开销分片过多会导致小请求被放大到很多分片执行集群协调压力增大。经验上单个分片的数据量控制在 20GB 到 50GB 之间具体取决于你的查询模式。数据量小的话单节点索引设置 1 个主分片副本看可用性需求设 1 或 0完全够用。大批量写入时还有两个常见调优项副本先设为 0。副本写入和数据写入是并行的副本越多写入越慢。导完数据再把副本改回来。refresh_interval调大。默认 1 秒刷新一次意味着每秒都有一次 refresh会产生段文件。导数据时可以把刷新间隔调到 30 秒甚至 -1关闭让段更少、写入更快。PUT /product/_settings { number_of_replicas: 0, refresh_interval: 30s }数据导入完成后再调回来PUT /product/_settings { number_of_replicas: 1, refresh_interval: 1s }我第一次做性能压测时这两条参数改动让写入吞吐提升了将近五倍。别小看 settings 的作用。8.3 常见报错的排查思路最后分享几个高频报错和排查思路。集群变红先GET /_cat/shards?v找UNASSIGNED的分片再用GET /_cluster/allocation/explain看具体原因通常是磁盘水位满、节点不可达等。磁盘水位满时ES 会主动停止分配分片。清理磁盘空间后状态会自动恢复。索引只读磁盘水位过高时ES 会把索引设置成只读防止进一步扩大磁盘占用。此时写请求会报错。解决方式先释放磁盘空间然后手动解除只读PUT /product/_settings { index.blocks.read_only_allow_delete: false }搜索超时有的查询在开发环境很快到生产就超时。先看_cat/nodes的 CPU 和 load再排查具体索引的分片数是否太少或太多最后看查询里有没有should过多、深分页这类问题。调大 timeout 只是治标真正要优化的是查询逻辑。内存熔断报错信息里出现CircuitBreakingException说明内存使用超过限额。常见原因是聚合的size设得太大或者一次性加载了太多大字段。把聚合桶数量调小或者加大节点内存都可以缓解。做完这些基本操作你会发现自己已经在拿 RESTful API 干很多正经事了。我个人在实际项目里的体会是8.x 的核心操作并不复杂真正拉开差距的是对 mapping 和分片规划的理解。宁可花半小时把字段类型想清楚也别在数据量上来之后折腾 reindex。先把上面这些请求多敲几遍ES 的基本功就稳了。
返回列表