如何比较两个 Elasticsearch 索引并找出缺失的文档

张开发
2026/6/19 21:20:14 15 分钟阅读
如何比较两个 Elasticsearch 索引并找出缺失的文档
作者来自 Elastic David Pilato刚接触 Elasticsearch加入我们的 Elasticsearch 入门网路会议。你也可以开始免费云试用或现在就在你的本地机器上试用 Elastic。在管理 Elasticsearch 索引时你可能需要验证一个索引中的所有文档是否也存在于另一个索引中例如在 reindex 操作、迁移或数据管道之后。Elasticsearch 并未提供内置的“diff”命令来完成这一点但正确的方法取决于一个关键问题两个索引之间的文档 ID 是否稳定更多阅读Elasticsearch消除 Elasticsearch 中的重复数据Logstash如何在 Elasticsearch 中查找和删除重复文档使用 Elasticsearch 进行日志重复数据删除问题假设你有两个索引index-a源和 index-b目标你想找出所有存在于 index-a 但缺失于 index-b 的文档。一种简单的方法是同时查询两个索引并在内存中比较结果但这种方法无法扩展。Elasticsearch 旨在处理数百万文档一次性加载所有数据并不现实。有两种场景ID 稳定两个索引对同一文档使用相同的 _id例如将 emp_no 作为文档 ID。这是最简单的情况。ID 生成文档通过不同的数据管道写入并分配了随机或顺序 ID。你无法通过 _id 比较需要基于内容进行匹配。接下来我们分别讲解这两种情况。步骤 0 —— 一个更轻量的 Elasticsearch CLI本文中的所有示例都使用 escli这是一个用 Rust 编写的小型命令行接口CLI对 Elasticsearch REST API 进行了封装。它从环境变量中读取你的集群 URL 和凭据因此你无需在每个命令中重复认证 headers。为了说明这一点的重要性下面是一个使用原始 curl 的典型 _search 调用curl -X GET \ -H Authorization: ApiKey $ELASTIC_API_KEY \ -H Content-Type: application/json \ -d {query:{term:{user.id:kimchy}}} \ $ELASTICSEARCH_URL/my-index-000001/_search使用 escli相同的请求变为./escli search --index my-index-000001 {query:{term:{user.id:kimchy}}}凭据存储在.env文件中escli 会自动加载 —— 无需在每次调用时使用-H Authorization: ...也降低了在 shell 历史中泄露敏感信息的风险。请求体通过 stdin传入这使得可以轻松地通过 jq 动态构建并传递多行 JSON。步骤 1 —— 统计两个索引中的文档数量在进行完整扫描之前先快速统计每个索引的文档数量。如果数量相同两个索引很可能已经同步就不需要再进行扫描。./escli count --index index-a ./escli count --index index-b_count API 返回{ count: 1000000 }如果计数不同则继续进行完整比较。步骤 2 —— 当 ID 有意义时使用 op_typecreate如果两个索引对同一文档使用相同的 _id例如因为你使用像 emp_no 这样的业务主键而不是生成的 UUID 来索引文档你可以通过一次 _reindex 调用来查找并修复缺失的文档。为什么使用有意义的 ID当数据具有自然主键时使用有意义的字段作为 _id而不是随机 UUID是一种最佳实践。这意味着同一文档始终具有相同的 _id无论由哪个数据管道写入。你可以通过 ID 轻松更新或删除文档。你可以使用 op_typecreate 来跳过目标中已存在的文档。无需在客户端进行扫描或比较。op_typecreate 技巧使用 _reindex 并设置 op_typecreate会尝试将源索引中的每个文档创建到目标索引中。如果目标中已存在相同 _id 的文档Elasticsearch 会将其报告为 version_conflict 并继续处理而不会覆盖已有文档。设置 conflictsproceed 可以让 API 在遇到冲突时继续执行而不是在第一个冲突时中止。./escli reindex { source: { index: index-a }, dest: { index: index-b, op_type: create }, conflicts: proceed }响应会准确地告诉你发生了什么{ total: 1000000, created: 49594, version_conflicts: 950406, failures: [] }created从 index-b 中缺失并已被添加的文档。version_conflicts在 index-b 中已存在且未被修改的文档。无需扫描无需客户端比较无需中间文件。所有操作都在服务端完成在一个包含 100 万文档的数据集中大约只需 6 秒。步骤 3 —— 当 ID 不稳定时基于业务键的比较有时你无法依赖 _id。一个在写入时生成 ID 的数据管道每次处理同一条记录都会分配不同的 _id。如果 index-a 和 index-b 由这样的两个管道生成同一个员工记录在一个索引中可能是 _id: abc123而在另一个索引中是 _id: xyz789即使底层数据完全相同。在这种情况下你需要基于内容而不是 ID 来匹配文档。关键是识别一组字段它们组合在一起构成一个唯一的业务键。对于员工数据集一个合理的业务键是first_name、last_name、birth_date。如果在 index-b 中不存在具有这三个字段相同组合的文档则 index-a 中的该文档就被视为 “缺失”。3a —— 使用 PIT search_after 扫描源索引在源索引上打开一个时间点 (PIT)以获取一致的快照然后分页遍历仅获取业务键字段./escli open_point_in_time index-a 5m # → { id: 46ToAwMDaWR... }./escli search { size: 10000, _source: [first_name, last_name, birth_date], pit: { id: 46ToAwMDaWR..., keep_alive: 5m }, sort: [{ _shard_doc: asc }] }排序键 _shard_doc 是全索引分页最有效的排序方式它使用内部 Lucene 文档顺序无额外开销。使用 search_after 重复直到响应中没有命中。完成后务必关闭 PIT./escli close_point_in_time {id: 46ToAwMDaWR...}对于源文档的每一页通过 _msearch 检查目标索引。为每页源文档构建一个 _msearch 请求每个文档一个子查询。每个子查询在三个业务键字段上使用 bool/must 并设置 size: 0我们只需要知道是否存在匹配不需要检索文档本身。./escli msearch EOF {index: index-b} {size:0,query:{bool:{must:[{term:{first_name.keyword:Alice1}},{term:{last_name.keyword:Smith}},{term:{birth_date:1985-03-12}}]}}} {index: index-b} {size:0,query:{bool:{must:[{term:{first_name.keyword:Bob2}},{term:{last_name.keyword:Jones}},{term:{birth_date:1990-07-24}}]}}} EOF响应包含每个子查询对应的一条记录顺序与子查询相同{ responses: [ { hits: { total: { value: 1 } } }, { hits: { total: { value: 0 } } } ] }total.value 0 表示 index-b 中没有文档匹配该业务键该文档缺失。从源页收集对应的 _id。关于 .keyword 子字段term 查询要求精确keyword匹配。first_name 和 last_name 字段在索引映射中必须有 .keyword 子字段。演示的 mapping.json 包含了这一点。3c — 使用按日期分片加速如果业务键包含日期字段你可以将源数据按日期分片并将每个分片作为独立任务运行。每个分片打开自己的 PIT并在 birth_date 上使用范围过滤器运行自己的 msearch 循环并将结果写入单独文件。父脚本并行启动所有分片并在所有任务完成后汇总结果。但根据你的用例你也可能希望按其他字段分片例如如果有 team 字段可以为每个团队运行一个分片。关键是找到一个字段使数据可以被分割成合理均匀的块从而可以并行处理。[compare] Launching 5 slices in parallel... → Slice 1: 1960-01-01 → 1969-12-31 ✅ — 244408 checked, 12207 missing → Slice 2: 1970-01-01 → 1979-12-31 ✅ — 243624 checked, 12212 missing → Slice 3: 1980-01-01 → 1989-12-31 ✅ — 243551 checked, 11921 missing → Slice 4: 1990-01-01 → 1999-12-31 ✅ — 243895 checked, 11991 missing → Slice 5: 2000-01-01 → 2009-12-31 ✅ — 24522 checked, 1263 missing1M 数据集上的性能为了验证这些方法演示在 index-a 中生成 1,000,000 条文档并故意在 index-b 中跳过约 5%49,594 条缺失文档然后运行完整的 compare → reindex 循环。MacBook M3 Pro 上的结果Comparison (compare-indices.sh):策略比较重建索引总计工作原理op_type6s6s全量 _reindex 服务端执行跳过已存在文档business-key1m 38s4s1m 42sPIT 扫描 按业务键 _msearchsplit-by-date32s4s36s同 business-key相同逻辑5 个切片并行执行op_typecreate 方法最快因为所有操作都在服务端执行无需客户端扫描。split-by-date 策略通过并行处理将 business-key 耗时从 1m 38s 缩短到 36s对于两个 1M 文档索引之间的比较效果不错。决策树Are _id values stable between both indices? ├── Yes → _reindex with op_typecreate (6s, server-side) └── No → Do you have a reliable business key? ├── Yes, simple scan is fast enough → business-key (1m 42s) └── Yes, and you need more speed → split-by-date (36s, parallel)结论Elasticsearch 不提供原生的索引差异命令但正确的策略取决于你的数据模型尽可能使用功能性 _id例如 emp_no 这样的自然业务键。它能解锁最简单、最快的方式使用 _reindex 并设置 op_typecreate通过一次服务端调用即可查找并填补缺失文档。当 ID 不稳定时使用 PIT _msearch 按业务键匹配。按某个字段分片并并行处理以恢复大部分性能。如果你经常需要这样操作可以考虑在摄取时计算业务键字段的哈希并用作 _id。这样你既有稳定的 ID又有高效的查询。完整示例包括数据集生成、比较脚本和重建索引脚本可在 https://github.com/dadoonet/blog-compare-indices/ 获取。原文https://www.elastic.co/search-labs/blog/elasticsearch-index-comparison

更多文章