Guide de l’algorithme HyperLogLog

1. Vue d’ensemble

La structure de données HyperLogLog (HLL) est une structure de données probabiliste utilisée pour estimer la cardinalité d’un ensemble de données .

Supposons que nous avons des millions d’utilisateurs et que nous voulons calculer le nombre de visites distinctes sur notre page Web. Une implémentation naïve consisterait à stocker chaque identifiant d’utilisateur unique dans un ensemble, puis la taille de l’ensemble serait notre cardinalité.

Lorsque nous traitons de très gros volumes de données, compter la cardinalité de cette façon sera très inefficace, car le jeu de données occupera beaucoup de mémoire.

Mais si nous sommes d’accord avec une estimation de quelques pour cent et que nous n’avons pas besoin du nombre exact de visites uniques, nous pouvons utiliser le HLL , car il a été conçu pour un tel cas d’utilisation - estimer le nombre de millions voire même Des milliards de valeurs distinctes .

2. Dépendance Maven

Pour commencer, nous devons ajouter la dépendance Maven pour https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22net.agkn%22%20AND%20a%3A%22hll % 22[ hll ]bibliothèque:

<dependency>
    <groupId>net.agkn</groupId>
    <artifactId>hll</artifactId>
    <version>1.6.0</version>
</dependency>

3. Estimation de la cardinalité à l’aide de HLL

Sautant bien - le constructeur HLL a deux arguments que nous pouvons ajuster en fonction de nos besoins:

  • log2m (log base 2) – il s’agit du nombre de registres utilisés en interne

par HLL (note: nous spécifions le m ) ** regwidth – c’est le nombre de bits utilisés par registre

Si nous voulons une plus grande précision, nous devons définir ces valeurs sur des valeurs plus élevées.

Une telle configuration aura une surcharge supplémentaire car notre HLL occupera plus de mémoire. Si nous sommes bien avec une précision moindre, nous pouvons réduire ces paramètres et notre HLL occupera moins de mémoire.

Créons un HLL pour compter des valeurs distinctes pour un ensemble de données contenant 100 millions d’entrées. Nous allons définir le paramètre log2m égal à 14 et regwidth égal à 5 - des valeurs raisonnables pour un ensemble de données de cette taille.

  • Lorsque chaque nouvel élément est inséré dans HLL , il doit être préalablement haché. ** Nous utiliserons Hashing.murmur3 128 () depuis la bibliothèque Guava (incluse dans la dépendance hll__) car elle est à la fois précise et rapide.

HashFunction hashFunction = Hashing.murmur3__128();
long numberOfElements = 100__000__000;
long toleratedDifference = 1__000__000;
HLL hll = new HLL(14, 5);

Le choix de ces paramètres devrait nous donner un taux d’erreur inférieur à 1% (1 000 000 d’éléments). Nous allons tester cela dans un instant.

Ensuite, insérons les 100 millions d’éléments:

LongStream.range(0, numberOfElements).forEach(element -> {
    long hashedValue = hashFunction.newHasher().putLong(element).hash().asLong();
    hll.addRaw(hashedValue);
  }
);

Enfin, nous pouvons tester que la cardinalité renvoyée par le HLL se situe dans les limites du seuil d’erreur souhaité :

long cardinality = hll.cardinality();
assertThat(cardinality)
  .isCloseTo(numberOfElements, Offset.offset(toleratedDifference));

4. Taille de la mémoire de HLL

Nous pouvons calculer la quantité de mémoire que votre ALL utilisera dans la section précédente en utilisant la formule suivante: nombre De its = 2 ^ log2m ** regwidth__.

Dans notre exemple, il s’agira de 2 ^ 14 ** 5 bits (environ 81000 bits ou 8100 octets). Ainsi, l’estimation de la cardinalité d’un ensemble de 100 millions de membres à l’aide de HLL n’occupe que 8100 octets de mémoire.

Comparons cela avec une implémentation d’ensemble naïve. Dans une telle implémentation, nous avons besoin d’un ensemble de 100 millions de valeurs longues qui occuperait 100 000 000 ** 8 octets = 800 000 000 octets __ .

Nous pouvons voir que la différence est étonnamment élevée. Avec HLL , nous n’avons besoin que de 8100 octets, alors que pour l’implémentation naïve Set , il nous faudrait environ 800 mégaoctets.

Lorsque nous considérons des ensembles de données plus volumineux, la différence entre HLL et la mise en œuvre naïve Set devient encore plus grande.

5. Union de deux __HLLs

HLL a une propriété bénéfique lors de l’union _. Lorsque nous prenons l’union de deux HLLs créées à partir d’ensembles de données distincts et mesurons sa cardinalité , nous obtenons le même seuil d’erreur pour l’union que nous aurions si nous avions utilisé un seul HLL_ et calculé les valeurs de hachage pour tous les éléments des deux ensembles de données depuis le début .

Notez que lorsque nous unissons deux HLL, les deux paramètres log2m et regwidth doivent être identiques pour produire des résultats corrects.

Testons cette propriété en créant deux HLLs – l’une contient des valeurs comprises entre 0 et 100 millions, et la seconde entre 100 et 200 millions:

HashFunction hashFunction = Hashing.murmur3__128();
long numberOfElements = 100__000__000;
long toleratedDifference = 1__000__000;
HLL firstHll = new HLL(15, 5);
HLL secondHLL = new HLL(15, 5);

LongStream.range(0, numberOfElements).forEach(element -> {
    long hashedValue = hashFunction.newHasher()
      .putLong(element)
      .hash()
      .asLong();
    firstHll.addRaw(hashedValue);
    }
);

LongStream.range(numberOfElements, numberOfElements **  2).forEach(element -> {
    long hashedValue = hashFunction.newHasher()
      .putLong(element)
      .hash()
      .asLong();
    secondHLL.addRaw(hashedValue);
    }
);

Veuillez noter que nous avons ajusté les paramètres de configuration de HLLs , augmentant ainsi le paramètre log2m de 14, comme indiqué dans la section précédente, à 15 pour cet exemple, car l’union HLL résultante contiendra deux fois plus d’éléments.

Ensuite, lions firstHll et secondHll en utilisant la méthode union () . Comme vous pouvez le constater, la cardinalité estimée se situe dans les limites d’un seuil d’erreur, comme si nous avions pris la cardinalité d’un HLL avec 200 millions d’éléments:

firstHll.union(secondHLL);
long cardinality = firstHll.cardinality();
assertThat(cardinality)
  .isCloseTo(numberOfElements **  2, Offset.offset(toleratedDifference **  2));

6. Conclusion

Dans ce tutoriel, nous avons examiné l’algorithme HyperLogLog .

Nous avons vu comment utiliser le HLL pour estimer la cardinalité d’un ensemble. Nous avons également vu que HLL est très peu encombrant par rapport à la solution naïve. Et nous avons effectué l’opération d’union sur deux HLLs et vérifié que l’union se comporte de la même manière qu’un seul HLL .

Vous trouverez la mise en œuvre de tous ces exemples et extraits de code dans le projet GitHub ; C’est un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.