feat(hll): add hyperloglog cardinality estimator
This commit is contained in:
1 parent
c501aba216
commit
c6244eb0ba
1 file changed
+105
@@ -0,0 +1,105 @@
|
|||||||
|
defmodule Excache.HyperLogLog do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
import Bitwise
|
||||||
|
|
||||||
|
@precision 14
|
||||||
|
@registerCount 1 <<< @precision
|
||||||
|
@tailBits 64 - @precision
|
||||||
|
@magic "EXHLL1"
|
||||||
|
def registerCount, do: @registerCount
|
||||||
|
|
||||||
|
def empty do
|
||||||
|
@magic <> :binary.copy(<<0>>, @registerCount)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?(<<@magic, rest::binary>>) when byte_size(rest) == @registerCount, do: true
|
||||||
|
def valid?(_), do: false
|
||||||
|
|
||||||
|
def add(binary, elements) do
|
||||||
|
registers = registersOf(binary)
|
||||||
|
|
||||||
|
{updated, changed} =
|
||||||
|
Enum.reduce(elements, {registers, false}, fn element, {acc, touched} ->
|
||||||
|
{index, rank} = placement(element)
|
||||||
|
current = :binary.at(acc, index)
|
||||||
|
|
||||||
|
if rank > current do
|
||||||
|
{replaceByte(acc, index, rank), true}
|
||||||
|
else
|
||||||
|
{acc, touched}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
{@magic <> updated, changed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def count(binary), do: estimate(registersOf(binary))
|
||||||
|
|
||||||
|
def merge(binaries) do
|
||||||
|
merged =
|
||||||
|
binaries
|
||||||
|
|> Enum.map(®istersOf/1)
|
||||||
|
|> Enum.reduce(:binary.copy(<<0>>, @registerCount), fn registers, acc ->
|
||||||
|
maxRegisters(acc, registers)
|
||||||
|
end)
|
||||||
|
|
||||||
|
@magic <> merged
|
||||||
|
end
|
||||||
|
|
||||||
|
def countMerged(binaries) do
|
||||||
|
binaries
|
||||||
|
|> merge()
|
||||||
|
|> count()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp registersOf(<<@magic, registers::binary-size(@registerCount)>>), do: registers
|
||||||
|
|
||||||
|
defp maxRegisters(left, right) do
|
||||||
|
for index <- 0..(@registerCount - 1), into: <<>> do
|
||||||
|
<<max(:binary.at(left, index), :binary.at(right, index))>>
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp placement(element) do
|
||||||
|
<<hash::unsigned-integer-size(64), _::binary>> = :crypto.hash(:sha256, element)
|
||||||
|
index = hash >>> @tailBits
|
||||||
|
tail = hash &&& (1 <<< @tailBits) - 1
|
||||||
|
{index, rank(tail) + 1}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rank(tail) when tail == 0, do: @tailBits
|
||||||
|
defp rank(tail), do: @tailBits - bitLength(tail)
|
||||||
|
|
||||||
|
defp bitLength(0), do: 0
|
||||||
|
defp bitLength(value), do: bitLength(value >>> 1) + 1
|
||||||
|
|
||||||
|
defp replaceByte(binary, index, byte) do
|
||||||
|
<<head::binary-size(index), _::8, tail::binary>> = binary
|
||||||
|
<<head::binary, byte::8, tail::binary>>
|
||||||
|
end
|
||||||
|
|
||||||
|
defp estimate(registers) do
|
||||||
|
{harmonic, zeros} =
|
||||||
|
Enum.reduce(0..(@registerCount - 1), {0.0, 0}, fn index, {sum, empties} ->
|
||||||
|
value = :binary.at(registers, index)
|
||||||
|
empties = if value == 0, do: empties + 1, else: empties
|
||||||
|
{sum + :math.pow(2, -value), empties}
|
||||||
|
end)
|
||||||
|
|
||||||
|
raw = alpha() * @registerCount * @registerCount / harmonic
|
||||||
|
|
||||||
|
corrected =
|
||||||
|
cond do
|
||||||
|
raw <= 2.5 * @registerCount and zeros != 0 ->
|
||||||
|
@registerCount * :math.log(@registerCount / zeros)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
raw
|
||||||
|
end
|
||||||
|
|
||||||
|
round(corrected)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp alpha, do: 0.7213 / (1 + 1.079 / @registerCount)
|
||||||
|
end
|
||||||
Reference in new issue
Block a user