查询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"
}
}
}
}
}
}
有没有办法过滤掉那些重复的值?最好的方法是什么?
答案 0 :(得分:4)
我们在代码中遇到了相同的问题,我们的解决方案是在UserId字段上使用术语聚合,在datetime字段上使用嵌套的最小聚合。这为您提供了包含第一次访问的存储桶的每个userId的存储桶。 我们在日期直方图之外进行此汇总,然后手动进行映射。
"aggs": {
"UniqueUsers": {
"terms": {
"field": "userId",
"size": 1000,
}, "aggs": {
"FirstSeen": {
"min": {
"field": "date"
}
}
}
}
}
这对我们有用,但是我相信应该有更好的实现方式。
答案 1 :(得分:1)
用户ID是重复的,但它们在不同的日期出现,因此,除非您在特定的日期查看,否则按天分配它们将使它多次出现。即使这样,如果同一ID在同一天多次出现,则您仍然可能有重复的ID,具体取决于您查看的时间范围的精确度。由于您查看的时间间隔是正确的,因此它返回5条记录是正确的,应该说在4日有3个ID,其中一个是重复的,第二天显示两个具有两个ID的记录,其中一个是重复项。如果您将间隔增加到一周或一个月,则这些重复项将被计为一个。
我确定您已经遇到了这个问题,但是请在解释您的确切用例时再看一遍。 Link
基本上,它会返回给定日期的所有唯一身份访问者。如果您不关心单个用户,而只想知道有多少用户,则需要使用其他方法。也许是按查询分组
答案 2 :(得分:0)
即使我想避免使用脚本,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开始,应使用state
和states
而不是params._agg
和params._aggs
。