Elasticsearch:消除 Elasticsearch 中的重复数据

Elastic 专栏收录该内容
494 篇文章 87 订阅

重复数据在数据分析和搜索中会造成错误。在我们的实际使用中,我们应该避免重复导入的数据。重复数据有各种原因会造成。比如我们重复导入同样的数据。当我们写入文档时使用自动生成的 ID,那么同样的文档被导入两次,这样会造成同样的两个一样的文档会保存于 Elasticsearch 中尽管它们的 ID 会有不同。在我之前的文章 “Beats:如何避免重复的导入数据”,我详细描述了如果使用 Beats 导入数据时,避免重复数据。

避免在 Elasticsearch 索引中重复始终是一件好事。 但是,通过消除重复项,你可以获得其他好处:节省磁盘空间,提高搜索准确性,提高硬件资源管理效率。 也许最重要的是,你减少了搜索的获取时间。令人惊讶的是,有关该主题的文档很少,因此我们提供了本教程,为你提供识别和管理索引中重复项的适当技术。

 

示例数据

这里有四个简单的文档,其中一个是另一个的重复数据。 我们建立一个叫做 employeeid 的索引。

POST employeeid/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "John", "organisation": "Apple", "employeeID": "23141A"}
{ "index" : { "_id" : "2" } }
{ "name" : "Sam", "organisation": "Tesla", "employeeID": "TE9829"}
{ "index" : { "_id" : "3" } }
{ "name" : "Sarah", "organisation": "Microsoft", "employeeID": "M54667"}
{ "index" : { "_id" : "4" } }
{ "name" : "John", "organisation": "Apple", "employeeID": "23141A"}

从上面的命令中,我们可以看得出来 ID 为 1 和 4 的两个文档完全是一样的,尽管它们的 ID  是不同的。

 

数据导入过程中避免重复文档

在考虑如何在 Elasticsearch 中执行重复检查之前,让我们花点时间考虑一下不同类型的索引方案。

一种情况是在索引编制之前我们可以访问源文档。 在这种情况下,检查数据并查找一个或多个包含唯一值的字段相对容易。 也就是说,该字段的每个不同值仅出现在一个文档中。 在这种情况下,我们可以将该特定字段设置为 Elasticsearch 索引的文档 ID。 由于任何重复的源文档也将具有相同的文档 ID,因此 Elasticsearch 将确保这些重复文档不会成为索引的一部分。

你可以参考我之前的文章 “Beats:如何避免重复的导入数据”。

 

Upsert

另一种情况是一个或多个文档具有相同的标识符但内容不同。 当用户编辑文档并想使用相同的文档 ID 重新索引该文档时,通常会发生这种情况。 问题在于,当用户尝试重新索引时,Elasticsearch 不允许这样做,因为它的文档 ID 必须是唯一的。

解决方法是使用 Upsert API。 Upsert 检查特定文档的存在,如果存在,Upsert 将使用 Upsert 的内容更新该文档。 如果文档不存在,Upsert 将创建具有相同内容的文档。 无论哪种方式,用户都将在相同的文档 ID 下获得内容更新。

在第三种情况下,在创建索引之前无法访问数据集。 在这些情况下,我们将需要搜索索引并检查重复项。 这就是我们在以下各节中演示的内容。

在写入数据时,我们可以使用 Upsert 来进行。如果该文档尚不存在,则将 Upsert 元素的内容作为新文档插入。 如果文档存在,则执行更新。比如:

POST test/_update/1
{
  "script": {
    "source": "ctx._source.counter += params.count",
    "lang": "painless",
    "params": {
      "count": 4
    }
  },
  "upsert": {
    "counter": 1
  }
}

在上面,如果 ID 为 1 的文档已经存在没那么将执行脚本,并把 count 的值加上 4。否则创建一个新的文档,并把 count 字段的值设置为 1。又比如:

POST sessions/_update/dh3sgudg8gsrgl
{
  "scripted_upsert": true,
  "script": {
    "id": "my_web_session_summariser",
    "params": {
      "pageViewEvent": {
        "url": "foo.com/bar",
        "response": 404,
        "time": "2014-01-01 12:32"
      }
    }
  },
  "upsert": {}
}

在上面,我们设置 scrpted_upsert 为 true。无论 ID 为 dh3sgudg8gsrgl 已经存在与否,id 为 my_web_session_summariser 的脚本将被执行,并把相应的参数传入。

我们也可以直接使用 _update 对文档直接更新,比如:

POST test/_update/1
{
  "doc": {
    "name": "new_name"
  },
  "doc_as_upsert": true
}

在上面,如果 ID 为 1 的文档已经存在,那么它的字段 name 值将被更新,否则创建一个新的 ID 为 1 的文档,并设置它的字段 name 值为 new_name。

在使用 Upsert 命令时,我们需要注意的是:我们必须提供一个 ID,另外它有两个操作:检查是否存在,并更新或者写入。Upsert 的速度会比正常的自动生成 ID 的导入速度慢。

当我们使用 Logstash 进行导入时,我们也可以指定 Upsert

 

检查重复项的基本技巧

在上面的每个示例文档中,我们看到三个字段:name,organisation 及 employeeID,并且如果我们假设 name 字段是唯一的,则可以将该字段指定为检查重复项的标识符。 如果多个文档的 name 字段具有相同的值,则该文档确实是重复的。

遵循此基本原理,我们可以执行简单的术语聚合,以获取 name 字段每个值的文档计数。 但是,这种简单的聚合只会返回该字段每个值下的文档计数。 这种方法在检查重复项时没有用,因为我们要检查文档中该字段的一个或多个值的重复项。 为此,我们还需要应用 top_hits 聚合 - 一个子聚合器,其中每个存储桶中均会聚合最匹配的文档。

这是我们建议针对上面给出的示例文档索引的查询:

GET employeeid/_search
{
  "size": 0,
  "aggs": {
    "duplicateCount": {
      "terms": {
        "field": "name.keyword",
        "min_doc_count": 2
      },
      "aggs": {
        "duplicateDocuments": {
          "top_hits": {}
        }
      }
    }
  }
}

在这里,我们定义参数  min_doc_count。通过将此参数设置为 2,只有 doc_count 为 2 或更大的聚合桶将出现在聚合中(如以下结果所示)。

上面的命令运行的结果是:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "duplicateCount" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "John",
          "doc_count" : 2,
          "duplicateDocuments" : {
            "hits" : {
              "total" : {
                "value" : 2,
                "relation" : "eq"
              },
              "max_score" : 1.0,
              "hits" : [
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "1",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "John",
                    "organisation" : "Apple",
                    "employeeID" : "23141A"
                  }
                },
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "4",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "John",
                    "organisation" : "Apple",
                    "employeeID" : "23141A"
                  }
                }
              ]
            }
          }
        }
      ]
    }
  }
}

请务必注意,我们必须将 min_doc_count 的值设置为2。 否则,其他结果将出现在聚合中,并且我们将找不到可能存在的任何重复项。

 

对多个字段中的值进行重复数据删除

我们上面所做的是一个非常基本的示例,该示例根据单个字段中的值来标识重复文档。这不是很有趣。还是有用的。在大多数情况下,检查重复项需要检查多个字段。我们不能可靠地假设员工文档之间存在重复项,而这些重复项仅在名称字段中包含多次出现的 “Bill” 值。在许多实际情况下,有必要检查许多不同字段之间的重复项。考虑到上面的示例数据集,我们需要检查所有字段中的重复项。

我们可以从上一节中扩展我们的方法,并执行多字段术语聚合和 top-hits 聚合。我们可以对索引文档中的所有三个字段进行术语聚合。我们将再次指定 min_doc_count 参数,以仅获取 doc_count 大于或等于 2 的存储桶。我们还应用 top_hits 聚合以获取正确的结果。为了容纳多个字段,我们使用脚本来帮助我们追加字段值以在聚合中显示:

GET employeeid/_search
{
  "size": 0,
  "aggs": {
    "duplicateCount": {
      "terms": {
        "script": "doc['name.keyword'].value + doc['employeeID.keyword'].value + doc['organisation.keyword'].value",
        "min_doc_count": 2
      },
      "aggs": {
        "duplicateDocuments": {
          "top_hits": {}
        }
      }
    }
  }
}

如下所示,运行此查询的结果将显示一个重复计数聚合。聚合 duplicateDocuments 包含在其中找到重复值的文档。 我们可以对这些文档进行交叉检查和验证。

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "duplicateCount" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "John23141AApple",
          "doc_count" : 2,
          "duplicateDocuments" : {
            "hits" : {
              "total" : {
                "value" : 2,
                "relation" : "eq"
              },
              "max_score" : 1.0,
              "hits" : [
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "1",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "John",
                    "organisation" : "Apple",
                    "employeeID" : "23141A"
                  }
                },
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "4",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "John",
                    "organisation" : "Apple",
                    "employeeID" : "23141A"
                  }
                }
              ]
            }
          }
        }
      ]
    }
  }
}

从上面,我们可以看出来有一个重复的文档。

假如我们把导入的文档修改为:

POST employeeid/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "John", "organisation": "Apple", "employeeID": "23141A"}
{ "index" : { "_id" : "2" } }
{ "name" : "Sam", "organisation": "Tesla", "employeeID": "TE9829"}
{ "index" : { "_id" : "3" } }
{ "name" : "Sarah", "organisation": "Microsoft", "employeeID": "M54667"}
{ "index" : { "_id" : "4" } }
{ "name" : "John", "organisation": "Apple", "employeeID": "23141A"}
{ "index" : { "_id" : "5" } }
{ "name" : "Sarah", "organisation": "Microsoft", "employeeID": "M54667"}

在上面,ID 为 1 和 4 为重复文档。3 和 5 的文档为重复文档。重新运行上面的查询,我们可以看到:

  "aggregations" : {
    "duplicateCount" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "John23141AApple",
          "doc_count" : 2,
          "duplicateDocuments" : {
            "hits" : {
              "total" : {
                "value" : 2,
                "relation" : "eq"
              },
              "max_score" : 1.0,
              "hits" : [
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "1",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "John",
                    "organisation" : "Apple",
                    "employeeID" : "23141A"
                  }
                },
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "4",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "John",
                    "organisation" : "Apple",
                    "employeeID" : "23141A"
                  }
                }
              ]
            }
          }
        },
        {
          "key" : "SarahM54667Microsoft",
          "doc_count" : 2,
          "duplicateDocuments" : {
            "hits" : {
              "total" : {
                "value" : 2,
                "relation" : "eq"
              },
              "max_score" : 1.0,
              "hits" : [
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "3",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "Sarah",
                    "organisation" : "Microsoft",
                    "employeeID" : "M54667"
                  }
                },
                {
                  "_index" : "employeeid",
                  "_type" : "_doc",
                  "_id" : "5",
                  "_score" : 1.0,
                  "_source" : {
                    "name" : "Sarah",
                    "organisation" : "Microsoft",
                    "employeeID" : "M54667"
                  }
                }
              ]
            }
          }
        }
      ]
    }
  }

如上所示,重复的文档都被显示出来了。

  • 4
    点赞
  • 4
    评论
  • 11
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值