Руководство по алгоритму HyperLogLog

1. Обзор

Структура данных HyperLogLog (HLL) является вероятностной структурой данных, используемой для оценки мощности набора данных .

Предположим, что у нас миллионы пользователей, и мы хотим рассчитать количество отдельных посещений нашей веб-страницы. Наивной реализацией было бы сохранение каждого уникального идентификатора пользователя в наборе, и тогда размер набора был бы нашим количеством элементов.

Когда мы имеем дело с очень большими объемами данных, подсчет кардинальности таким образом будет очень неэффективным, поскольку набор данных будет занимать много памяти.

Но если мы согласны с оценкой в ​​несколько процентов и не нуждаемся в точном количестве уникальных посещений, тогда мы можем использовать HLL , так как он был разработан именно для такого варианта использования - подсчет количества миллионов или даже миллиарды различных ценностей .

2. Maven Dependency

Для начала нам нужно добавить зависимость Maven для https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22net.agkn%22%20AND%20a%3A%22hll Библиотека% 22[ hll ]:

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

3. Оценка мощности с помощью HLL

Прямо сейчас - конструктор HLL имеет два аргумента, которые мы можем настроить в соответствии с нашими потребностями

  • log2m (log base 2) – это количество регистров, используемых внутри

HLL (примечание: мы указываем m ) ** regwidth – это количество бит, используемых в регистре

Если мы хотим более высокую точность, нам нужно установить для них более высокие значения.

Такая конфигурация будет иметь дополнительные издержки, потому что наш HLL будет занимать больше памяти. Если у нас все хорошо с более низкой точностью, мы можем уменьшить эти параметры, и наш HLL будет занимать меньше памяти.

Давайте создадим HLL для подсчета различных значений для набора данных со 100 миллионами записей. Мы установим параметр log2m равным 14 и regwidth равным 5 - разумные значения для набора данных такого размера.

  • Когда каждый новый элемент вставляется в HLL , его нужно предварительно хэшировать. ** Мы будем использовать Hashing.murmur3 128 () из библиотеки Guava (входит в зависимость hll__), потому что он точен и быстр.

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

Выбор этих параметров должен дать нам коэффициент ошибок ниже одного процента (1 000 000 элементов). Мы будем проверять это через минуту.

Далее, давайте вставим 100 миллионов элементов:

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

Наконец, мы можем проверить, что количество элементов, возвращаемое HLL , находится в пределах нашего желаемого порога ошибки :

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

4. Объем памяти HLL

Мы можем рассчитать, сколько памяти займет наш HLL из предыдущего раздела, используя следующую формулу: numberOfBits = 2 ^ log2m ** regwidth

В нашем примере это будет 2 ^ 14 ** 5 бит (примерно 81000 бит или 8100 байт). Таким образом, оценка мощности 100-миллионного набора элементов с использованием HLL заняла только 8100 байт памяти.

Давайте сравним это с реализацией наивного набора. В такой реализации нам нужно иметь Set из 100 миллионов Long значений, которые будут занимать 100 000 000 ** 8 байт = 800 000 000 байт _. _

Мы видим, что разница удивительно высока. При использовании HLL нам нужно всего 8100 байт, тогда как при использовании простой реализации Set нам потребуется примерно 800 мегабайт.

Когда мы рассматриваем большие наборы данных, разница между HLL и наивной реализацией Set становится еще больше.

5. Союз двух HLLs

HLL имеет одно полезное свойство при выполнении объединений _. Когда мы возьмем объединение двух HLLs , созданных из разных наборов данных, и измерим его мощность, мы получим тот же порог ошибки для объединения, который мы получили бы, если бы использовали один HLL_ и вычислил значения хеш-функции для всех элементов обоих наборов данных с начала .

Обратите внимание, что когда мы объединяем два HLL, оба должны иметь одинаковые параметры log2m и regwidth для получения правильных результатов.

Давайте проверим это свойство, создав два HLL - , один из которых заполняется значениями от 0 до 100 миллионов, а второй заполняется значениями от 100 миллионов до 200 миллионов:

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

Обратите внимание, что мы настроили параметры конфигурации HLLs , увеличив параметр log2m с 14, как показано в предыдущем разделе, до 15 для этого примера, поскольку полученное объединение HLL будет содержать вдвое больше элементов.

Далее, давайте объединим firstHll и secondHll с помощью метода union () . Как видите, расчетное количество элементов находится в пределах порога ошибки, как если бы мы взяли количество элементов от одного HLL с 200 миллионами элементов:

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

6. Заключение

В этом уроке мы рассмотрели алгоритм HyperLogLog .

Мы увидели, как использовать HLL для оценки мощности множества. Мы также увидели, что HLL очень компактен по сравнению с наивным решением. И мы выполнили операцию объединения для двух HLLs и убедились, что объединение ведет себя так же, как и один HLL

Реализация всех этих примеров и фрагментов кода можно найти в проекте GitHub ; это проект Maven, поэтому его легко импортировать и запускать как есть.