【问题标题】:Cardinality over Date Histogram日期直方图的基数
【发布时间】:2019-08-01 18:23:27
【问题描述】:

查询 Elasticsearch 以实现表示唯一访问者总数指标的日期直方图的最佳方法是什么?

考虑以下数据:

PUT /events
{
"mappings" : {
        "_doc" : {
            "properties" : {
                "userId" : { "type" : "keyword" },
                "eventDate" : { "type" : "date" }
            }
        }
    }
}

POST /events/_bulk
{ "index" : { "_index" : "events", "_type" : "_doc", "_id" : "1" } }
{"userId": "1","eventDate": "2019-03-04T13:40:18.514Z"}
{ "index" : { "_index" : "events", "_type" : "_doc", "_id" : "2" } }
{"userId": "2","eventDate": "2019-03-04T13:46:18.514Z"}
{ "index" : { "_index" : "events", "_type" : "_doc", "_id" : "3" } }
{"userId": "3","eventDate": "2019-03-04T13:50:18.514Z"}
{ "index" : { "_index" : "events", "_type" : "_doc", "_id" : "4" } }
{"userId": "1","eventDate": "2019-03-05T13:46:18.514Z"}
{ "index" : { "_index" : "events", "_type" : "_doc", "_id" : "5" } }
{"userId": "4","eventDate": "2019-03-05T13:46:18.514Z"}

现在,如果我查询 userId 字段的基数,我会得到 4 个不同的访问者。

POST /events/_search
{
    "size": 0,
    "aggs": {
        "visitors": {
            "cardinality": {
                "field": "userId"
            }
        }
    }
}

但是,将文档分布在日期直方图上,我得到的总和为 5,因为两个存储桶中都有重复的 userId。

POST /events/_search
{
    "size": 0,
    "aggs": {
        "visits_over_time": {
            "date_histogram": {
                "field": "eventDate",
                "interval": "1d"
            },
            "aggs": {
                "visitors": {
                    "cardinality": {
                        "field": "userId"
                    }
                }
            }
        }
    }
}

有没有办法过滤掉那些重复的值?实现这一目标的最佳方法是什么?

【问题讨论】:

  • 嗨,我不确定我是否理解了这个问题:如果您按天对事件进行分组,那么 userId 1 在两个存储桶中是正确的
  • 是的,但我需要每个 userId 在所有存储桶中只出现一次,即保留第一个 userId 出现。
  • 你有没有免费的 X-path 包(有观察者)?
  • @LeBigCat 是的,这是一个选项。
  • 已编辑答案以进行澄清,但我认为如果您只关心有多少而不关心单个用户,您需要一个不同的查询

标签: elasticsearch date-histogram


【解决方案1】:

用户 ID 是重复的,但它们出现在不同的日期,因此按天分配它们会使其出现多次,除非您查看的是特定日期。即使这样,如果相同的 id 在同一天多次出现,您可能仍然有重复的 id,具体取决于您查看的时间范围的精确程度。由于您正在查看一天的间隔,因此它返回 5 条记录是正确的,并且应该说在 4 日,有 3 个 id,其中一个是重复的,并且在第二天显示了两条具有两个不同 id 的记录,其中一个是重复的。如果您将间隔增加到一周或一个月,这些重复项将计为一个。

我确定您已经遇到过这个问题,但请再看一下,因为它正在解释您的确切用例。 Link

基本上,它会返回给定日期的所有唯一身份访问者。如果您不关心单个用户,而只想知道有多少,那么您需要一种不同的方法。可能是查询分组

【讨论】:

  • 没错,这就是重点,我需要用户 ID 在所有选定范围内都是唯一的,不仅在单个存储桶上(我在 7 中也有一天和一周长的存储桶, 30 天和 90 天的范围)。问题上提供的查询与您发布的链接相同,但类似地,该查询没有完成我需要的查询,每种颜色每个月计算一次,但它在所有存储桶中并不是唯一的。您能否提供有关如何通过您提到的查询组解决此问题的任何示例?
  • 您的用例到底是什么?我假设您想在一段时间内显示唯一身份访问者。如果是这样,你已经工作了。上述查询每天为您提供唯一访问者。如果您希望在 7 天的时间间隔内拥有唯一身份访问者,您可以将 1d 更改为 1w。然后,这将每周为您提供独特的访问者。也许您需要更多地解释您的用例。
  • 专注于示例案例,忘记我提到的存储桶大小。想象一下索引上的所有文档都来自同一个月,我需要的很简单:一个日期直方图,间隔为 1 天(以天为单位)的整个月的唯一访问者,只计算第一个事件。
【解决方案2】:

我们在代码中遇到了同样的问题,我们的解决方案是在 UserId 字段上使用术语聚合,在 datetime 字段上使用嵌套的 Min 聚合。这为您提供了一个包含第一次访问的 Bucket 的每个 userId 的存储桶。 我们在日期直方图之外进行此聚合,然后手动对其进行映射。

"aggs": {
    "UniqueUsers": {
      "terms": {
        "field": "userId",
        "size": 1000,
      }, "aggs": {
        "FirstSeen": {
          "min": {
            "field": "date"
          }
        }
      }
    }
  }

这对我们有用,但我相信应该有更好的实现。

【讨论】:

  • 你的意思是先获取所有的用户ID/日期,然后在客户端的内存中将它们聚合成一个直方图吗?
  • 有点,这将为您提供每个用户的存储桶,其中包含第一次读数的度量存储桶,您只需将读数映射到直方图中。
【解决方案3】:

即使我想避免使用脚本,Scripted Metric Aggregation 似乎是完成请求的唯一方法:

{
    "size": 0,
    "aggs": {
        "visitors": {
            "scripted_metric": {
                "init_script": "params._agg.dateMap = new HashMap();",
                "map_script": "params._agg.dateMap.merge(doc.userId[0].toString(), doc.eventDate.value, (e1, e2) -> e1.isBefore(e2) ? e1 : e2);",
                "combine_script": "return params._agg.dateMap;",
                "reduce_script": "def dateMap = new HashMap(); for (map in params._aggs) { if (map == null) continue; for (entry in map.entrySet()) dateMap.merge(entry.key, entry.value, (e1, e2) -> e1.isBefore(e2) ? e1 : e2); } def hist = new TreeMap(); for (entry in dateMap.entrySet()) hist.merge(entry.value.toString(), 1, (a, b) -> a + 1); return hist;"
            }
        }
    }
}

Init 只是创建一个空的 HashMap,Map 用 userId 作为键填充该映射,并将最旧的 eventDate 设置为值,Combine 只是解包要传递给 Reduce 的映射:

def dateMap = new HashMap();
for (map in params._aggs) {
    if (map == null) continue;
    for (entry in map.entrySet())
        dateMap.merge(entry.key, entry.value, (e1, e2) -> e1.isBefore(e2) ? e1 : e2);
}

def hist = new TreeMap();
for (entry in dateMap.entrySet())
    hist.merge(entry.value.toString(), 1, (a, b) -> a + 1);
return hist;

在为每个集群节点执行组合代码之前,Reduce 将所有映射合并为一个(即 dateMap),并保留每个 userId 最旧的 eventDate。然后计算每个 eventDate 的出现次数。

结果是:

"aggregations": {
    "visitors": {
        "value": {
            "2019-03-04T13:40:18.514Z": 1,
            "2019-03-04T13:46:18.514Z": 1,
            "2019-03-04T13:50:18.514Z": 1,
            "2019-03-05T13:46:18.514Z": 1
        }
    }
}

唯一缺少的部分是必须将这些值分组到应用程序代码的直方图中。

注意¹:使用风险自负,我不知道内存消耗是否会因为这些哈希映射而增加很多,或者它在大型数据集上的表现如何。

注意²:从 Elasticsearch 6.4 开始,应使用statestates 而不是params._aggparams._aggs

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-04
    • 1970-01-01
    • 2020-04-27
    • 2014-02-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多