elasticsearch去重:collapse、cardinality、terms+top_hits实现总结

2024-07-01 09:29:42 浏览数 (1)

一 、collapse折叠去重

elasticsearch中的collapse功能允许用户对搜索结果进行分组,这在某些情况下可以看作是一种去重操作。它的主要目的是在搜索大量文档时,只显示每个分组的一个代表文档,而不是显示所有匹配的文档。

原理

collapse功能基于一个或多个字段的值对搜索结果进行分组。当你指定了collapse参数后,Elasticsearch会在后台对匹配的文档进行分组,并且每个分组只会返回一个代表文档。这个代表文档通常是分组中的第一个文档,但也可以通过其他参数进行定制。

用法

以下是如何在Elasticsearch查询中使用collapse的基本示例:

代码语言:javascript复制
{
  "query": {
    "match": {
      "field": "value"
    }
  },
  "collapse": {
    "field": "group_field"
  }
}

在这个示例中:

  • query部分定义了搜索的基本条件。在这个例子中,我们搜索字段field值为value的文档。
  • collapse部分指定了用于分组的字段,即group_field。所有在这个字段上具有相同值的文档将被分组在一起,并且只返回一个代表文档。

你还可以通过添加inner_hits参数来定制返回的分组代表文档。例如,如果你想在每个分组中返回评分最高的文档,你可以这样做:

代码语言:javascript复制
{
  "query": {
    "match": {
      "field": "value"
    }
  },
  "collapse": {
    "field": "group_field",
    "inner_hits": {
      "name": "most_relevant",
      "size": 1,
      "sort": [
        {
          "_score": {
            "order": "desc"
          }
        }
      ]
    }
  }
}
  • inner_hits部分定义了如何为每个分组选择代表文档。这里,我们命名了inner_hits的结果为most_relevant
  • size: 1表示每个分组只返回一个文档。
  • sort部分指定了如何对分组内的文档进行排序。在这里,我们根据文档的评分(_score)进行降序排序,因此每个分组的代表文档将是该分组中评分最高的文档。
注意事项
  1. 性能开销:在大数据集上应用collapse功能可能会带来额外的性能开销,因为需要对结果进行分组和排序操作。
  2. 分页复杂性:当与分页功能结合使用时,需要注意Elasticsearch的分页是基于索引顺序,而不是折叠后的顺序,这可能导致深度分页时的性能问题或结果不一致。
  3. 不能与scrollrescoresearch_after结合使用: 由于collapse需要对结果进行分组和排序以确定每个组的最佳匹配文档,这个过程可能会与scrollrescoresearch_after的某些功能冲突。
  4. 性能表现:虽然collapse通常比完全的分组和聚合操作更高效,因为它只返回每个组的最佳文档,但处理大量数据时仍可能产生性能开销。
  5. 字段类型collapse参数所使用的字段必须是keywordnumber类型,因为这些类型的字段值精确,适用于分组和排序。使用text类型字段可能导致不准确的结果。

在使用collapse时,请务必考虑这些限制和注意事项,以确保查询的准确性和性能。通过合理规划和优化查询,可以充分利用collapse的分组功能,同时避免潜在的性能瓶颈。

二、字段聚合(terms) top_hits聚合 去重

结合使用字段聚合(terms)和top_hits聚合可以实现去重功能。

原理
  1. 字段聚合(terms):此聚合类型用于显示某个字段中的唯一值及其对应的文档数量。通过字段聚合,我们可以将数据按照指定字段的不同值进行分组。
  2. top_hits聚合:此聚合类型用于在每个分组(bucket)内部返回最匹配的文档。当与terms聚合结合使用时,它可以在每个分组中返回指定数量的文档,通常用于返回每个分组的代表性文档。

结合这两种聚合,我们可以先按照某个字段进行分组(实现初步的“去重”效果,即每个分组代表一个唯一的字段值),然后在每个分组中使用top_hits聚合返回代表性的文档,从而实现更精细的去重功能。

用法
  1. 构建基础查询:首先,你需要构建一个基础的Elasticsearch查询,用于筛选出需要进行去重处理的文档集合。
  2. 添加terms聚合:在查询的聚合部分,添加一个terms聚合,并指定需要按其进行分组的字段。这样,Elasticsearch会将所有文档按照该字段的唯一值进行分组。
  3. 嵌套top_hits聚合:在terms聚合的每个分组中,嵌套一个top_hits聚合。这样,在每个分组内部,你可以指定返回最匹配的文档数量(通常是1,以实现去重效果)。
  4. (可选)定制top_hits:你可以进一步定制top_hits聚合,例如通过指定排序方式来控制返回的代表性文档。

有一个包含商品信息的索引,并且你想按照“品牌”字段对商品进行去重,以便每个品牌只显示一个代表性商品。查询可能如下所示:

代码语言:javascript复制
GET /products/_search
{
  "size": 0, // 不返回具体的匹配文档,只返回聚合结果
  "aggs": {
    "brands": { // terms聚合,按品牌分组
      "terms": {
        "field": "brand",
        "size": 10 // 假设我们想要获取前10个品牌的商品
      },
      "aggs": {
        "top_product": { // 在每个品牌分组内,使用top_hits聚合返回代表性商品
          "top_hits": {
            "size": 1, // 每个品牌只返回一个代表性商品
            "sort": [ // 可以根据需要指定排序方式,例如按评分或价格排序
              {
                "_score": { // 按评分排序
                  "order": "desc"
                }
              }
            ]
          }
        }
      }
    }
  }
}

我们首先使用terms聚合按照“品牌”字段对商品进行分组,然后在每个分组中使用top_hits聚合返回一个代表性商品(评分最高的商品)。这样,我们就实现了按照品牌对商品进行去重的功能。

三、两种方法的比较

字段聚合(terms) top_hits聚合
  • 原理:这种方法首先使用terms聚合按某个字段的值进行分组,然后在每个分组内部使用top_hits聚合来获取每个分组的顶部文档。
  • 灵活性:非常高。你可以自定义terms聚合的字段,以及top_hits聚合返回的文档数量和排序方式。
  • 性能:依赖于聚合字段的基数(即不同值的数量)。如果基数很大,性能可能会受到影响,因为需要为每个不同的值进行聚合。
  • 结果:返回的是每个分组的一个或多个代表文档,以及每个分组的大小等信息。
  • 用途:适用于需要对数据进行多维分析和统计的场景。
使用collapse功能
  • 原理collapse功能通过指定一个字段来对搜索结果进行分组,并且每组只返回一个最佳匹配的文档(通常是基于排序字段的最高或最低值)。
  • 灵活性:相对较低。你只能基于一个字段进行分组,并且每组只返回一个文档。
  • 性能:通常比字段聚合更高效,因为它不需要计算每个分组的统计信息,只需要找到每个分组的最佳匹配文档。
  • 结果:返回的是每个分组的最佳匹配文档。
  • 用途:适用于只需要获取每个分组的代表文档,而不需要详细统计信息的场景。
对比总结
  • 灵活性:字段聚合 top_hits提供了更多的自定义选项,可以按多个字段进行分组,并控制返回的文档数量和排序。而collapse则更简单直接,只基于一个字段进行分组。
  • 性能:对于大数据集,collapse可能更高效,因为它避免了复杂的聚合计算。然而,实际性能还取决于具体的使用场景和数据分布。
  • 结果丰富性:字段聚合 top_hits可以返回更丰富的信息,包括分组大小和多个代表文档。而collapse只返回每个分组的最佳文档。

在选择使用哪种方法时,应根据具体需求、数据量和性能要求来权衡。如果你需要详细的分组统计信息和多个代表文档,字段聚合 top_hits可能是更好的选择。如果你只需要快速获取每个分组的最佳文档,并且关注性能,那么collapse可能更适合你。

四、cardinality 统计去重后的数量

cardinality聚合是一种用于统计某个字段中不同值的数量基数(即去重后的数量)的功能。

原理
  1. 基于HyperLogLog 算法:cardinality聚合是基于HyperLogLog (HLL)算法的近似算法。HLL会先对输入作哈希运算,然后根据哈希运算的结果中的bits做概率估算,从而得到基数值,即不同值的数量。
  2. 近似结果:需要注意的是,由于使用了HLL算法,cardinality聚合提供的是一个近似结果,而不是精确值。但在大多数情况下,这个近似值已经足够准确,可以满足业务需求。
  3. 性能优化:为了提升性能,Elasticsearch在处理大数据集时会使用一定的优化策略,比如使用分桶和并行处理等技术来加速计算过程。
用法
  1. 基础用法:要使用cardinality聚合,你需要在Elasticsearch的查询请求中指定一个cardinality聚合,并设置要统计的字段。例如,如果你想统计一个索引中“color”字段的不同值的数量,你可以发送一个包含cardinality聚合的查询请求。
  2. 嵌套在其他聚合中:cardinality聚合还可以嵌套在其他聚合中,比如date_histogram聚合。这样,你可以按时间间隔(如每月、每天等)来统计不同值的数量。这对于分析时间序列数据中的唯一值数量非常有用。
  3. 调整精度:虽然cardinality聚合提供的是近似结果,但你可以通过调整相关参数来权衡精度和性能。Elasticsearch允许你设置精度阈值,以便在可接受的误差范围内获得更快的计算结果。

假设你有一个包含商品销售数据的Elasticsearch索引,你想统计“color”字段中有多少种不同的颜色。你可以使用以下查询来实现:

代码语言:javascript复制
GET /sales/_search  
{  
  "query": {  
    "range": {  
      "date": {  
        "gte": "2024-01-01",  
        "lte": "2024-06-30"  
      }  
    }  
  },  
  "size": 0,  
  "aggs": {  
    "distinct_colors_in_period": {  
      "cardinality": {  
        "field": "color"  ,
         "precision_threshold": 1000  

      }  
    }  
  }  
}

这个查询会返回一个聚合结果,其中包含“color”字段中不同颜色的数量。

precision_threshold 参数说明

cardinality 度量是一个近似算法。 它是基于 HyperLogLog (HLL)算法的。 HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。

我们不需要理解技术细节, 但我们最好应该关注一下这个算法的 特性 :

  • 可配置的精度,用来控制内存的使用(更精确 = 更多内存)。
  • 小的数据集精度是非常高的。
  • 我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。

要配置精度,我们必须指定 precision_threshold 参数的值。 这个阈值定义了在何种基数水平下我们希望得到一个近乎精确的结果.

  • recision_threshold 接受 0–40,000 之间的数字,更大的值还是会被当作 40,000 来处理。
  • 以上示例会确保当字段唯一值在 1000 以内时会得到非常准确的结果。尽管算法是无法保证这点的,但如果基数在阈值以下,几乎总是 100% 正确的。高于阈值的基数会开始节省内存而牺牲准确度,同时也会对度量结果带入误差。
  • 对于指定的阈值,HLL 的数据结构会大概使用 precision_threshold * 8 字节的内存,所以就必须在牺牲内存和获得额外的准确度间做平衡。
  • 在实际应用中, 100 的阈值可以在唯一值为百万的情况下仍然将误差维持 5% 以内。

五、collapse cardinality 实现去重统计和查询

代码语言:javascript复制
{
    "query": {
        "bool": {
            "filter": [
                {
                    "range": {
                        "personid": {
                            "lt": "1000000000"
                        }
                    }
                },
                {
                    "term": {
                        "isDeleted": "0"
                    }
                }
            ]
        }
    },
    "collapse": {
        "field": "course_id"
    },
    "from": 0,
    "size": 10,
    "track_total_hits": true,
    "aggs": {
        "courseAgg": {
            "cardinality": {
                "field": "course_id"
            }
        }
    }
}

返回结果

代码语言:javascript复制
{
    "took": 140,
    "timed_out": false,
    "_shards": {
        "total": 6,
        "successful": 6,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2111,
            "relation": "eq"
        },
        "max_score": 0.0,
        "hits": [...]
    },
    "aggregations": {
        "courseAgg": {
            "value": 1070
        }
    }
}

解释说明

  1. hits中的total字段显示的总条数,实际上是查询结果在去重之前的总数量,也就是原始数据的条数。这个数值在分页功能中通常不会被直接使用。而hits数组的大小与aggregations中的courseAgg聚合值相等,表示数组中展示的是去重后的数据。
  2. aggregations中的courseAgg条数,代表去重后的实际数据条数,这也是进行分页时所使用的关键数值,它指示了去重后可用于展示的总条数。
  3. from参数表示查询的起始位置,即从哪里开始检索数据,它相当于查询的偏移量。
  4. size参数定义了每次查询返回的数据条数,即一次检索并展示多少条记录。

0 人点赞