6
votes

Cardinalité sur histogramme de date

Quelle serait la meilleure façon d'interroger Elasticsearch afin de mettre en œuvre un histogramme de date représentant le nombre total de statistiques de visiteurs uniques?

En tenant compte des données suivantes:

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

Maintenant, si j'interroge la cardinalité du champ userId, j'obtiens les 4 visiteurs distincts.

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

Cependant, en distribuant les documents sur un histogramme de date, j'obtiens une somme totale de 5 parce qu'il y a un userId répété dans les deux compartiments.

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"}

Existe-t-il un moyen de filtrer ces valeurs répétées? Quelle serait la meilleure façon d'y parvenir?


5 commentaires

Bonjour, je ne suis pas sûr d'avoir compris le problème: si vous regroupez les événements par jours, alors il est correct que userId 1 soit dans les deux buckets


Oui, mais j'ai besoin que chaque userId n'apparaisse qu'une seule fois dans tous les buckets, c'est-à-dire qu'il conserve la première occurrence de userId.


N'avez-vous pas de pack Xpath gratuit (avec observateurs)?


@LeBigCat oui, c'est une option.


réponses modifiées pour quelques éclaircissements, mais je pense que vous avez besoin d'une requête différente si tout ce que vous vous souciez est de savoir combien et que vous ne vous souciez pas des utilisateurs individuels


3 Réponses :


1
votes

Les identifiants des utilisateurs sont répétés mais ils se produisent à des jours différents, donc les distribuer par jours fera que cela se produira plus d'une fois, sauf si vous regardez un jour spécifique. Même dans ce cas, si le même identifiant se produit le même jour plus d'une fois, vous pouvez toujours avoir des identifiants en double en fonction de la précision de la période que vous regardez. Puisque vous regardez à des intervalles d'un jour, il est correct qu'il renvoie 5 enregistrements et devrait dire que le 4, il y avait 3 identifiants dont l'un est le double et le jour suivant montre deux enregistrements avec deux identifiants différents dont l'un est le double. Si vous augmentez l'intervalle à une semaine ou un mois, ces doublons seront comptés pour un.

Je suis sûr que vous avez rencontré cela, mais donnez-lui un autre regard car il explique votre cas d'utilisation exact. Lien

En gros, il renvoie tous les visiteurs uniques d'un jour donné. Si vous ne vous souciez pas des utilisateurs individuels mais que vous voulez simplement savoir combien, vous avez besoin d'une approche différente. Peut-être un groupe par requête


3 commentaires

Exactement, c'est tout l'intérêt, j'ai besoin que les identifiants utilisateur soient uniques sur toute la plage sélectionnée, pas seulement sur un seul seau (j'ai également des seaux d'une journée et d'une semaine dans les plages de 7, 30 et 90 jours). La requête fournie sur la question est la même à partir du lien que vous avez publié, mais de la même manière, cette requête ne répond pas aux besoins dont j'ai besoin, chaque couleur est comptée une fois pour chaque mois, mais elle n'est pas unique dans tous les compartiments. Pouvez-vous fournir un exemple sur la façon de résoudre ce problème avec le groupe par requête que vous avez mentionné?


Quel est exactement votre cas d'utilisation? J'ai supposé que vous vouliez montrer des visiteurs uniques en fonction d'un intervalle de temps. Si tel est le cas, ce que vous avez fonctionne déjà. La requête ci-dessus vous donne des visiteurs uniques sur une base quotidienne. Si vous voulez des visiteurs uniques pendant un intervalle de 7 jours, vous devez changer le 1d en 1w . Ensuite, cela vous donnerait des visiteurs uniques sur une base hebdomadaire. Vous devez peut-être expliquer davantage votre cas d'utilisation.


Concentrez-vous sur le cas de l'échantillon, oubliez les tailles de seau que j'ai mentionnées. Imaginez que tous les documents de l'index proviennent du même mois, ce dont j'ai besoin est très simple: un histogramme de date avec un intervalle de 1 jour (seaux d'une journée) de visiteurs uniques du mois entier, seul le premier événement doit être compté.



6
votes

Nous avons rencontré le même problème dans notre code et notre solution consistait à utiliser une agrégation de termes sur le champ UserId avec une agrégation min imbriquée dans le champ datetime. Cela vous fournit un bucket pour chaque userId contenant le bucket lors de la première visite. Nous faisons cette agrégation en dehors de l'histogramme des dates et la mappons manuellement par la suite.

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

Cela fonctionne pour nous, mais je suis sûr qu'il devrait y avoir une meilleure implémentation.


2 commentaires

Voulez-vous d'abord obtenir tous les userIds / dates, puis les agréger dans la mémoire du client dans un histogramme?


un peu, cela vous fournira un compartiment pour chaque utilisateur, contenant un compartiment métrique de la première lecture, il vous suffira de mapper la lecture dans l'histogramme.



0
votes

Même si je souhaite éviter les scripts, L'agrégation de métriques par script semble être le seul moyen d'accomplir ce qui a été demandé:

"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
        }
    }
}

Init crée simplement un HashMap vide, la carte remplit cette carte avec userId comme clé et définit le plus ancien eventDate comme valeur, et Combine déballe simplement la carte à passer à 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;

Up to Combine le code a été exécuté pour chaque cluster node, Reduce fusionne toutes les cartes en une seule (c'est-à-dire dateMap) en préservant le plus ancien eventDate par userId. Ensuite, il compte les occurrences de chaque eventDate.

Le résultat est:

{
    "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;"
            }
        }
    }
}

La seule partie manquante est que ces valeurs doivent être regroupées dans un histogramme sur le code de l'application.

Remarque¹: Utilisez à vos risques et périls , je ne sais pas si la consommation de mémoire augmente beaucoup à cause de ces cartes de hachage ou de ses performances sur de grands ensembles de données.

Remarque²: à partir d'Elasticsearch 6.4, state et states doivent être utilisés à la place des paramètres params._agg et ._aggs .


0 commentaires