Anleitung zum HyperLogLog-Algorithmus

1. Überblick

Die Datenstruktur HyperLogLog (HLL) ist eine Wahrscheinlichkeitsdatenstruktur, mit der die Kardinalität eines Datensatzes geschätzt wird ** .

Nehmen wir an, wir haben Millionen von Benutzern und möchten die Anzahl der Besuche auf unserer Website berechnen. Eine naive Implementierung besteht darin, jede eindeutige Benutzer-ID in einem Satz zu speichern, und dann wäre die Größe des Satzes unsere Kardinalität.

Bei sehr großen Datenmengen ist das Kardinalitätszählen auf diese Weise sehr ineffizient, da der Datensatz viel Speicher beansprucht.

Wenn wir jedoch mit einer Schätzung von wenigen Prozent zufrieden sind und nicht die genaue Anzahl der eindeutigen Besuche benötigen, können wir die HLL verwenden, da sie für genau einen solchen Anwendungsfall entwickelt wurde - Schätzen der Anzahl von Millionen oder sogar der Zahl Milliarden verschiedener Werte .

2. Maven-Abhängigkeit

Um zu beginnen, müssen wir die Maven-Abhängigkeit für https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22net.agkn%22%20AND%20a%3A%22hll hinzufügen % 22[ hll ]Bibliothek:

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

3. Kardinalität mit HLL schätzen

Direkt einspringen - Der HLL -Konstruktor hat zwei Argumente, die wir je nach Bedarf anpassen können:

  • log2m (Protokollbasis 2) – Dies ist die Anzahl der intern verwendeten Register

von HLL (Hinweis: Wir geben den m an) ** regwidth – Dies ist die Anzahl der pro Register verwendeten Bits

Wenn wir eine höhere Genauigkeit wünschen, müssen wir diese auf höhere Werte setzen.

Eine solche Konfiguration hat zusätzlichen Aufwand, da unser HLL mehr Speicherplatz beansprucht. Wenn die Genauigkeit geringer ist, können wir diese Parameter verringern, und unser HLL belegt weniger Speicher.

Erstellen Sie eine HLL , um unterschiedliche Werte für einen Datensatz mit 100 Millionen Einträgen zu zählen. Wir setzen den log2m -Parameter gleich 14 und regwidth gleich 5 - angemessene Werte für einen Datensatz dieser Größe.

Wenn jedes neue Element in die HLL eingefügt wird, muss es zuvor gehasht werden.

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

Die Wahl dieser Parameter sollte eine Fehlerrate von unter einem Prozent (1.000.000 Elemente) ergeben. Wir werden das gleich testen.

Als Nächstes fügen wir die 100 Millionen Elemente ein:

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

Schließlich können wir testen, dass die von der HLL zurückgegebene Kardinalität innerhalb unserer gewünschten Fehlerschwelle liegt :

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

4. Speichergröße von HLL

Wir können berechnen, wie viel Speicher unser HLL aus dem vorherigen Abschnitt benötigen wird, indem Sie die folgende Formel verwenden: numberOfBits = 2 ^ log2m ** regwidth .

In unserem Beispiel sind dies 2 ^ 14 ** 5 Bits (ungefähr 81000 Bits oder 8100 Bytes). Die Schätzung der Kardinalität eines 100-Millionen-Mitgliedssatzes unter Verwendung von HLL belegte nur 8100 Byte Speicher.

Vergleichen wir das mit einer naiven Implementierung. In einer solchen Implementierung müssen wir einen Set von 100 Millionen Long -Werten haben, die 100.000.000 ** 8 Byte = 800.000.000 Byte _. _ belegen würden

Wir können sehen, dass der Unterschied erstaunlich groß ist. Mit HLL benötigen wir nur 8100 Bytes, während wir mit der naiven Set -Implementierung ungefähr 800 Megabyte benötigen.

Wenn wir größere Datensätze betrachten, wird der Unterschied zwischen HLL und der naiven Set -Implementierung noch größer.

5. Union of Two HLLs

HLL hat eine vorteilhafte Eigenschaft bei der Durchführung von Vereinigungen _. Wenn wir die Vereinigung zweier aus unterschiedlichen Datensätzen erstellter HLLs nehmen und deren Kardinalität messen, erhalten wir die gleiche Fehlerschwelle für die Vereinigung, die wir erhalten würden, wenn wir eine einzige HLL_ verwendet hätten. und berechnete die Hashwerte für alle Elemente beider Datensätze von Anfang an .

Wenn Sie zwei HLLs zusammenführen, sollten beide die gleichen log2m - und regwidth -Parameter haben, um korrekte Ergebnisse zu erzielen.

Lassen Sie uns diese Eigenschaft testen, indem Sie zwei HLLs – erstellen, wobei eine mit Werten von 0 bis 100 Millionen und die zweite mit Werten von 100 bis 200 Millionen gefüllt wird:

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);
    }
);

Bitte beachten Sie, dass wir die Konfigurationsparameter der HLLs angepasst haben, wobei der log2m -Parameter von 14, wie im vorherigen Abschnitt gezeigt, für dieses Beispiel auf 15 erhöht wurde, da die resultierende HLL -Vereinigung doppelt so viele Elemente enthält.

Lassen Sie uns als Nächstes die firstHll und die secondHll mit der union () -Methode verbinden. Wie Sie sehen können, liegt die geschätzte Kardinalität innerhalb einer Fehlerschwelle, als hätten wir die Kardinalität von einer HLL mit 200 Millionen Elementen genommen:

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

6. Fazit

In diesem Tutorial haben wir uns den HyperLogLog -Algorithmus angesehen.

Wir haben gesehen, wie man mit der HLL die Kardinalität einer Menge abschätzen kann. Wir haben auch gesehen, dass HLL im Vergleich zu der naiven Lösung sehr platzsparend ist. Und wir führten die Union-Operation an zwei HLLs aus und verifizierten, dass sich die Union wie eine einzelne HLL__ verhält.

Die Implementierung all dieser Beispiele und Codeausschnitte finden Sie im GitHub-Projekt . Dies ist ein Maven-Projekt, daher sollte es einfach sein, den aktuellen Status zu importieren und auszuführen.