拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Elasticsearch文档操作全解:从增删改查到Query DSL实战

刚接触 Elasticsearch 的开发者尤其是从 MySQL 这类关系型数据库转过来的朋友第一次听到文档这个词心里多半会犯嘀咕这不就是一行记录吗换个叫法有什么意义我最初也是这么想的直到真把数据写进去、查出来才发现 ES 的文档虽然名字叫文档但它的增删改查逻辑和关系型数据库有着很大的区别——最明显的一点ES 的更新根本不是就地改数据而是先删旧、再写新。这篇文章就把 Elasticsearch 文档的增删改查从头到尾捋一遍从环境准备、写入、读取、更新、删除到真正日常用得最多的结构化查询。内容会尽量贴着实际操作走所有示例都是我能直接跑通的命令和说法。不管你是刚装好 ES 还没写过一条数据还是已经接入了索引但总在更新和删除上踩坑这篇都值得看完再动手。1. 动手前先解决的两个环境问题安装选型与启动验证光看 API 不实操增删改查永远停留在纸面上。ES 的 REST API 虽然可以直接用 curl 调但前提是你得有一个能跑的 Elasticsearch 实例。这一步看着简单实际上坑不少尤其是 Windows 环境下启动 Elasticsearch 的细节很多人都在这里耽误过时间。1.1 版本选型别盲目追新先看 JDK 兼容性Elasticsearch 8.x 是目前主流7.x 依然有大量存量项目在用。新手入门我建议直接选 8.x因为 8.0 之后默认开启了安全认证虽然多了一步用户名密码配置但从一开始就按生产环境的方式去理解反而省掉将来迁移的折腾。选版本时最容易被忽略的是 JDK 兼容性。ES 8.x 官方捆绑了 OpenJDK所以理论上你不需要单独安装 Java。但你如果是从 7.x 时代过来的老鸟习惯了自己配置 JAVA_HOME就会遇到一个经典问题系统 Java 版本太老ES 启动直接报错提示Unsupported Java version。这时候有两种处理方式一是把 JAVA_HOME 指到 ES 自带的 JDK 目录二是在config/jvm.options里确认内存参数没问题再启动。我自己踩过的另一个坑是 ES 8.x 首次启动时会在控制台输出一串初始化信息其中包含 elastic 用户的初始密码。这个密码只会显示这一次很多人没留意直接关掉终端结果后面想登录 Kibana 死活进不去。如果你遇到了解决办法是进入bin目录执行elasticsearch-reset-password命令重新设置密码而不是重装整个 ES。1.2 Windows 11 环境下的启动验证与常见报错Windows 11 安装 Elasticsearch 和 Kibana 的流程在官方文档里写得很简略但有三个点它不会提醒你第一ES 的运行目录不能有空格和中文。比如你放在D:\Program Files\elasticsearch-8.15.0这种路径下启动大概率会出问题。我自己吃过亏之后习惯直接放到D:\elasticsearch这种最简路径。第二内存设置。ES 默认的堆内存是 1GB在config/jvm.options里有-Xms1g和-Xmx1g两行。开发机 16GB 内存的话建议改成 2g尤其后面要跑聚合查询1G 的堆经常触发 CircuitBreakingException查询直接报错。第三Windows 下启动会弹出一个控制台窗口ES 的所有日志都在这个窗口里打印。如果启动失败不要着急去翻日志文件先看控制台最后几行报错——80% 的问题比如端口占用、Data 目录权限不对都能在这里直接定位。启动验证非常简单Windows 下我习惯先调curlcurl -X GET localhost:9200/如果是 ES 8.x因为默认开启了 HTTPS 和认证命令要变成curl -k -u elastic:你的密码 https://localhost:9200/只要能返回一串 JSON里面包含cluster_name和tagline之类的字段说明实例就绪了。1.3 为什么我建议你打开 Kibana 的 Dev Tools命令行调 curl 能帮我们理解 HTTP 请求的本质但实际调试时效率太低。Kibana 自带的 Dev Tools 控制台是我日常操作 ES 最顺手的工具它内置了代码补全、格式校验还能高亮显示响应体中的错误信息。这里说一个实用的联动习惯先在 Dev Tools 里调通请求再把同样的请求原样改成项目里的RestHighLevelClient或者 ES 8.x 新客户端代码。这个流程能帮你把调试时间至少压缩一半特别是后面要写复杂的布尔查询时Dev Tools 直接提示语法错误省得在代码里反编译猜测问题出处。2. 写入文档Index API 与 Create API 的正确打开方式想理解 ES 的增先要接受一个认知转变ES 里没有插入一条记录这个说法只有索引一个文档Indexing a Document。所谓索引就是告诉 ES 把这个 JSON 文档存到某个索引里并让它变得可搜索。2.1 索引、文档、映射和 MySQL 对应的那一套概念ES 的 index索引对应 MySQL 的 database 或者 tabledocument文档对应一行记录field字段对应列mapping映射对应表结构。但这个对应关系是粗略的因为 ES 是面向 JSON 文档的——同一个索引下的文档理论上可以有不同的字段只是在实际使用中我们通常会定义好 mapping 来约束字段类型。写入时ES 的核心操作就是往某一个索引的_doc端点发送 JSON。我用一个最基础的通配符展开来说明PUT /student/_doc/1 { name: 张三, age: 22, major: 计算机科学 }执行完ES 返回的 JSON 里有几个关键字段新人容易忽略但必须看懂_index文档落在哪个索引。_id文档唯一标识相当于主键。_version文档版本号每次修改/删除后递增。result本次操作结果created表示新建updated表示覆盖更新。_shards分片写入成功数total为 2 时successful通常也是 2如果successful小于total意味着写入有副本失败要引起重视。2.2 PUT 与 POST 的区别指定 ID 与自动生成 ID 的取舍上面我用的是PUT /student/_doc/1这里 ID 是手动指定的。实际场景里如果你的数据源本身就带唯一主键比如数据库自增 ID、订单号、用户 ID就百分百应该用这种方式。好处是幂等同样的请求发两次结果都是更新同一份文档不会有重复数据。如果数据源没有现成的 ID就需要用 POSTPOST /student/_doc { name: 李四, age: 23, major: 软件工程 }ES 会自动生成一个 20 位随机 ID 返回给你你可以在返回结果里看到它。自动生成 ID 的缺点是如果重复执行同样的 POST会生成两条完全不同的文档这在批量同步数据时要特别注意去重逻辑。2.3 Index 与 Create 的本质区别Index API 和 Create API 都可以写入文档但语义完全不同。Create 只做新增如果文档已存在就直接报错Index 则是存在就覆盖不存在就新增。实际项目中这两种操作有非常明确的使用场景日志采集、全量覆盖同步时用 Index因为旧数据本来就该被新数据替换而订单支付回调、用户首次注册这种只允许第一次写入成功的场景就必须用 Create 来防止重复写入。指定 ID 的 Create 操作长这样PUT /student/_create/1 { name: 王五, age: 20 }如果 ID 为 1 的文档已经存在ES 会返回 409 冲突错误。这就是 ES 用 HTTP 状态码表达业务逻辑的经典例子——409 意味着你的意图和当前状态冲突了别覆盖我。2.4 写入原理简讲为什么返回里有一个_version字段ES 每次写入文档_version都会加 1。这个字段的存在让更新这件事进入了一个真正的版本控制时代——你不是在覆盖垃圾数据而是在用版本号管理文档的每次变化。理解这一点最好的方式就是亲手验证对同一个文档重复执行两次 PUT第一次返回_version是 1第二次是 2。ES 的_version是从 1 开始递增的而且不会因为删除而回退。这个版本号还是 ES 乐观锁控制的基础——如果你在用version参数做并发写入控制旧版本号的请求会被拒绝这比直接覆盖更安全但很多团队并没有利用到这个机制。3. 读取文档Get API 并不只是按 ID 取数据查在 ES 世界里分成两种按 ID 直接取文档Get以及按条件搜文档Search。前者实时性极强后者走倒排索引。很多新人把 Get 和 Search 混为一谈导致查询需求一变就不知道怎么办了。3.1 按 ID 获取文档_doc端点的读取姿势最简单的读取命令是对应的GET /student/_doc/1返回的 JSON 里有几个字段需要理解好found为true表示找到了_source是文档的真实数据也就是你当初写入的 JSON 内容_index和_id都不是数据而是元信息。如果文档不存在ES 返回 HTTP 200 但found为false。这一点非常反直觉——明明是没查到状态码却是 200。我之前给前端接口做查询直接拿 HTTP 状态码判断数据是否存在结果查不存在的 ID 时接口依然走成功分支搞了半天才发现问题出在这。正确做法是判断响应体里的found字段而不是 HTTP 状态码。3.2 只取_source减少无效数据传输如果查询只需要文档数据不需要元信息可以用GET /student/_doc/1/_source这个请求返回的直接是文档原始 JSON没有外层的_index、_id等包装。在写数据同步脚本时我经常用这个接口来批量取源数据配合_mget还能一次取多个文档GET /student/_mget { ids: [1, 2, 3] }响应里每个文档会对应一个返回对象同样需要注意found: false的文档不会让整个请求失败。3.3 HEAD 请求判断文档是否存在最省资源还有一个实用技巧值得养成习惯如果只是想知道某个文档是否存在不关心内容用 HEAD 请求最合适。它返回的 HTTP 状态码非常干净200 表示存在404 表示不存在没有任何响应体极大地节约了 网络和解析开销HEAD /student/_doc/1但注意HEAD 依然是根据文档 ID 判断是否存在不是按某个业务字段判断。如果你需要判断一个 user 邮箱是否存在还是要走 Search 查询。3.4 为什么很多查的需求不能靠 Get 解决举个最常见的场景你想查所有计算机专业、年龄大于 20 的学生。这个需求用 Get API 实现不了——Get 只能按 ID 取而你不能预知所有符合条件的学生 ID。这时候需要的是 Search API。Search 不走_doc端点而是走_search端点GET /student/_search { query: { bool: { filter: [ { term: { major: 计算机科学 } }, { range: { age: { gt: 20 } } } ] } } }所以我的建议是当你确定只需要按键取值这个操作时用 Get凡是涉及业务条件筛选的全部交给 Search。这样设计出来的代码逻辑更清晰别人接手也不会拿错 API。4. 更新文档覆盖与局部更新背后的版本机制ES 的改其实是全文替换 局部更新两条路线新手最容易在这里犯迷糊我明明只想改一个age字段为什么执行一次更新后其他字段全变空了问题通常出在你用了整体覆盖的方式。4.1 整体覆盖最简单的改法但代价很昂贵把完整的文档重新 PUT 一遍就是整体覆盖PUT /student/_doc/1 { name: 张三, age: 23 }执行完之后ID 为 1 的文档中major字段就消失了——因为 ES 把旧文档直接替换成了新文档。这个方式虽然简单但在业务场景里非常危险如果你先查出来一个文档只改其中两个字段然后把整个文档原样传回去等于用一次读加一次索引操作完成了更新磁盘 IO 是原来的一倍不止。所以整体覆盖更适合整份数据完全同步的场景比如每天从外部数据源全量更新商品信息。局部更新才是日常业务里最常写的。4.2 局部更新Update API 让字段原地修改ES 提供的局部更新 API 是POST /student/_update/1 { doc: { age: 23 } }注意这里用的是 POST不是 PUT端点也不是_doc而是_update。请求体里有个doc对象意思是把文档中这些字段更新为对应值。这个操作发生后直播间里的场景是这样的ES 先根据 ID 找到旧文档把传入的doc字段与旧文档做合并生成一个新文档再把旧文档标记为删除最后写入新文档。整个过程对调用方是原子的你不用担心读到中间状态。4.3 用 Painless 脚本做更复杂的更新如果只是字段赋值用doc就够了。但有些更新逻辑需要在原值基础上做计算比如库存减一、访问量加一、数组追加元素。这些场景得靠脚本。比如给学生的年龄加 1 岁POST /student/_update/1 { script: { source: ctx._source.age 1, lang: painless } }再比如给文档追加一个标签POST /student/_update/1 { script: { source: ctx._source.tags.add(优秀学员) } }Painless 是目前 ES 唯一支持的脚本语言语法接近 Java但没那么复杂。你只需要记住一个核心变量ctx._source它代表当前文档的 JSON 数据你能像操作 Java Map 一样修改它。4.4 版本冲突与重试机制并发更新的必备知识Update API 有一个隐藏得很深的特性它在执行前会先读取旧文档然后基于读到的内容做合并。这就引入了一个并发问题——两个请求同时读到同一个旧文档都准备更新第二个提交时就会碰上 ES 的版本校验。ES 的做法是如果并发冲突更新请求直接返回 409。为了解决这个问题官方提供了重试参数POST /student/_update/1?retry_on_conflict3 { doc: { age: 23 } }这个参数的含义是如果发生冲突ES 会自动重新读取最新文档再尝试更新总共最多重试 3 次。对于用户体验要求不高的场景加上这个参数能够大幅减少 409 报错但要注意它并不能保证最终一致性——只是让普遍环境下的并发更新更顺畅而已。4.5 一个必须提前知道的隐藏条件_source要存在上面说的 Update API 能完成合并前提是文档里保存了完整的原始字段数据。如果当初写数据的时候关闭了_source存储mapping 里设置_source: {enabled: false}Update API 是没法做局部更新的——因为它根本读不到旧数据来合并。所以建索引的 mapping 时要慎重考虑_source的开关。关闭它确实能节省存储但一旦需要局部更新、重建索引或者做数据管道分析麻烦就大了。我见过一些团队为了省磁盘空间把_source关掉后面做 reindex 时找不到原数据只能从数据仓库重新回放费了很大劲。5. 删除文档Delete 操作与误删后的恢复思路删除在实际工作中很常见也很危险。ES 的删除设计得比 MongoDB 复杂和 MySQL 完全不同所以需要单独篇幅讲清楚。5.1 按 ID 删除简单但不可逆删除一个文档的 API 长这样DELETE /student/_doc/1返回结果里result字段是deleted表示删除成功。如果文档不存在返回的result是not_found但注意 HTTP 状态码依然是 200——和 Get 一样ES 倾向于用响应体语义而不是状态码来表达删除未命中。这个操作不可逆至少在 ES 层面是这样的。你说的不可逆是指恢复步骤复杂而不是技术上绝对办不到这一点在后文会展开。5.2 批量删除与按条件删除_bulk和delete_by_query实际业务中很少逐条删除——要么批量直接删一堆 ID要么按条件删除一类数据。批量删除用_bulk端点一次发多个删除动作POST /_bulk { delete: { _index: student, _id: 1 } } { delete: { _index: student, _id: 2 } } { delete: { _index: student, _id: 3 } }注意这里每行都是一个完整的 JSON且必须逐行换行不能把多个动作放一个 JSON 数组里——这是 Bulk API 的固定格式很多人第一次写会在这上面卡住。按条件删除用delete_by_queryPOST /student/_delete_by_query { query: { term: { major: 历史学 } } }执行前 ES 会先按查询条件把命中的文档遍历出来再逐条删除。这个方法返回的 JSON 里有deleted字段告诉你实际删了多少条。执行前如果担心删错可以先把 query 部分放到_search里查一遍数量确认无误后再执行删除。5.3 删除索引与删除文档的本质区别我必须强调一个经常被混淆的概念删除文档Delete a document和删除索引Delete an index是完全不同的操作。删除文档只是把索引里的某些数据标记为删除删除索引则是把整个索引的空间全部释放DELETE /student执行完这个student索引就没了——包括 mapping、数据、配置文件全部消失。这个操作没有撤销快捷键一旦误删只能走数据恢复流程。所以我的建议是生产环境里删除索引之前先做一个索引别名alias检查并确认是不是真的没有需要保留的 snapshot 快照。什么都没有的话才动手。5.4 误删后的恢复思路快照、reindex 与软删除Elasticsearch 恢复数据这个话题的热度一直居高不下就是因为误删场景太常见了。要判断能不能恢复取决于你有没有提前准备下面的任意一种机制第一种快照恢复。ES 支持把索引快照备份到仓库比如本地磁盘或对象存储一旦误删只要仓库里还有快照就能还原到某个时间点POST /_snapshot/my_backup/snapshot_1/_restore { indices: student }创建快照的动作是这个PUT /_snapshot/my_backup/snapshot_1第二种reindex 恢复。如果没有快照但误删的是索引而文档在别处比如 MySQL、HDFS、日志采集系统还保留着原数据那就需要从源头把数据重新导入 ES。这个过程中有一个很实用的技巧从老的备份索引比如每天定时 clone 的别名索引用_reindex同步过来POST /_reindex { source: { index: student_backup }, dest: { index: student } }第三种软删除设计。这也是我在生产环境强烈建议做的——在文档里加一个status字段删除时不是调 DELETE API而是做一次 Update把status改为deleted。这样数据并没有真正消失排查问题时甚至可以随时捞回来。等到确认超过保留期再用delete_by_query做定时清理。6. 真正的查Query DSL 结构化查询从这里入门把增、改、删除的基本功练熟之后日常工作中占比重最大的还是查询。ES 的查询语言叫 Query DSL本质上是一套基于 JSON 的结构化查询语言。如果对数据库增删改查有基础你会发现 DSL 里很多概念和 SQL 都能对应上但表达方式完全不一样。6.1 全量查询与分页从match_all开始最简单的查询是取回一个索引下的全部文档GET /student/_search { query: { match_all: {} } }ES 默认只返回前 10 条而不是像 MySQL 那样全部吐出来。控制分页靠from和sizeGET /student/_search { from: 20, size: 10, query: { match_all: {} } }from是从第几条开始默认 0size是取多少条。分页逻辑在数据量小的时候没问题但超过万级慎用深分页因为 ES 要先把所有结果都排序再截取性能会很差。生产环境一般用search_after代替翻页。6.2 全文检索match与倒排索引match是 ES 全文检索的核心查询。它的行为很接近搜索引擎把查询的文本分词然后去倒排索引里找每一个词再算相关度分数GET /student/_search { query: { match: { major: 计算机 科学 } } }这里如果查询词是计算机科学ES 会先按分词器把它拆成计算机科学再分别去匹配。所以它的结果往往比 SQL 的LIKE %计算机科学%更宽泛也可能更精准取决于分词器。如果你是第一次接触全文检索记住一句话match是按词查不是按整串查。6.3 精确匹配term与keyword字段的配合和match相反term是做精确匹配的GET /student/_search { query: { term: { major: 计算机科学 } } }这个查询要求major字段的值完全等于计算机科学不分词整体匹配。但有个前提容易踩坑如果 mapping 里major是text类型默认会分词term查询通常是查不到完整内容的反而要用major.keyword这种子字段。所以写term前一定要确认目标字段是keyword类型或者.keyword子字段存在。这一句话能救回你不少调试时间。6.4 组合查询bool把多个条件拼起来真实业务几乎不会只有一个条件。比如查年龄大于 20 岁、专业是计算机、姓名包含张的同学就得用bool查询来组装。bool内部有四个子句分别代表不同的逻辑关系must必须匹配相当于 ANDshould应该匹配相当于 OR提升相关度must_not必须不匹配相当于 NOTfilter过滤条件不参与打分只做筛选性能好一个实际例子GET /student/_search { query: { bool: { must: [ { match: { name: 张 } } ], filter: [ { term: { major: 计算机科学 } }, { range: { age: { gt: 20 } } } ] } } }这个查询的意思是姓名包含张专业精确等于计算机科学年龄大于 20。filter条件只过滤不参与相关度计算所以在大量过滤场景下它的性能明显优于must——这也是我在设计查询时经常做的优化点。6.5 搜索结果里的hits结构怎么看执行完任意_search返回的顶层 JSON 里有几个固定字段。hits.total是符合条件的总数注意它是一个对象值在hits.total.value里hits.hits数组才是真正返回的文档列表数组里每个元素又包含_index、_id、_score和_source。_score是相关度分数match_all时所有分数都相同bool查询里用到must或should时分数才有意义。刚开始调查询时我建议先盯着hits.total.value而不是一上来就解析hits.hits这样能快速判断过滤条件是否生效少写很多调试日志。等确认命中的数量符合预期了再处理具体文档内容。我自己平时用得最多的组合是先用bool filter做精确过滤再用一个multi_match做全文搜索字段的匹配最后用sort排序。这套组合能覆盖 80% 的日常业务查询需求而且性能不容易翻车。工具上Kibana 的 Dev Tools 始终是我的首选调试环境。先在 Dev Tools 里把 DSL 调通再搬到项目代码里能省掉大量试错时间。另外提醒一句ES 的查询结果默认返回的所有字段都在_source如果想减少网络传输可以用_source参数只取需要的字段GET /student/_search { _source: [name, age], query: { match_all: {} } }这一点在字段多、文档大的业务场景里效果特别明显——查 1000 条文档只返回 name 和 age和返回全部字段的响应体大小能差好几倍。养成写_source白名单的习惯给 ES 减负也给自己少传点无关数据。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门