Current section
Files
Jump to
Current section
Files
src/hyper.erl
%% @doc: Implementation of HyperLogLog with bias correction as
%% described in the Google paper,
%% http://static.googleusercontent.com/external_content/untrusted_dlcp/
%% research.google.com/en//pubs/archive/40671.pdf
-module(hyper).
%%-compile(native).
-export([new/1, new/2, insert/2, insert_many/2]).
-export([union/1, union/2]).
-export([card/1, intersect_card/2]).
-export([to_json/1, from_json/1, from_json/2, precision/1, bytes/1, is_hyper/1]).
-export([compact/1, reduce_precision/2]).
-type precision() :: 4..16.
-type registers() :: any().
-record(hyper, {p :: precision(), registers :: {module(), registers()}}).
-type value() :: binary().
-type filter() :: #hyper{}.
-export_type([filter/0, precision/0, registers/0]).
%% Exported for testing
-export([run_of_zeroes/1]).
-define(DEFAULT_BACKEND, hyper_binary).
%%
%% API
%%
-spec new(precision()) -> filter().
new(P) ->
new(P, ?DEFAULT_BACKEND).
-spec new(precision(), module()) -> filter().
new(P, Mod) when 4 =< P andalso P =< 16 andalso is_atom(Mod) ->
#hyper{p = P, registers = {Mod, Mod:new(P)}}.
-spec is_hyper(filter()) -> boolean().
is_hyper(#hyper{}) ->
true;
is_hyper(_) ->
false.
-spec insert(value(), filter()) -> filter().
insert(Value, #hyper{registers = {Mod, Registers}, p = P} = Hyper)
when is_binary(Value) ->
Hash = crypto:hash(sha, Value),
<<Index:P, RegisterValue:(64 - P)/bitstring, _/bitstring>> = Hash,
ZeroCount = run_of_zeroes(RegisterValue) + 1,
%% Registers are only allowed to increase, implement by backend
Hyper#hyper{registers = {Mod, Mod:set(Index, ZeroCount, Registers)}};
insert(_Value, _Hyper) ->
error(badarg).
-spec insert_many([value()], filter()) -> filter().
insert_many(L, Hyper) ->
lists:foldl(fun insert/2, Hyper, L).
-spec union([filter()]) -> filter().
union(Filters) when is_list(Filters) ->
case lists:usort(lists:map(fun (#hyper{p = P, registers = {Mod, _}}) ->
{P, Mod}
end,
Filters))
of
%% same P and backend
[{_P, Mod}] ->
Registers = lists:map(fun (#hyper{registers = {_, R}}) ->
R
end,
Filters),
[First | _] = Filters,
First#hyper{registers = {Mod, Mod:max_merge(Registers)}};
%% mixed P, but still must have same backend
[{MinP, Mod} | _] ->
FoldedFilters = lists:map(fun (#hyper{registers = {M, _}} = F) when M =:= Mod ->
hyper:reduce_precision(MinP, F)
end,
Filters),
union(FoldedFilters)
end.
union(Small, Big) ->
union([Small, Big]).
%% NOTE: use with caution, no guarantees on accuracy.
-spec intersect_card(filter(), filter()) -> float().
intersect_card(Left, Right) when Left#hyper.p =:= Right#hyper.p ->
max(0.0, card(Left) + card(Right) - card(union(Left, Right))).
-spec card(filter()) -> float().
card(#hyper{registers = {Mod, Registers0}, p = P}) ->
M = trunc(pow(2, P)),
Registers = Mod:compact(Registers0),
RegisterSum = Mod:register_sum(Registers),
E = alpha(M) * pow(M, 2) / RegisterSum,
Ep = case E =< 5 * M of
true ->
E - estimate_bias(E, P);
false ->
E
end,
V = Mod:zero_count(Registers),
H = case V of
0 ->
Ep;
_ ->
M * math:log(M / V)
end,
case H =< hyper_const:threshold(P) of
true ->
H;
false ->
Ep
end.
precision(#hyper{p = Precision}) ->
Precision.
bytes(#hyper{registers = {Mod, Registers}}) ->
Mod:bytes(Registers).
compact(#hyper{registers = {Mod, Registers}} = Hyper) ->
Hyper#hyper{registers = {Mod, Mod:compact(Registers)}}.
reduce_precision(P, #hyper{p = OldP, registers = {Mod, Registers}} = Hyper)
when P < OldP ->
Hyper#hyper{p = P, registers = {Mod, Mod:reduce_precision(P, Registers)}};
reduce_precision(P, #hyper{p = P} = Filter) ->
Filter.
%%
%% SERIALIZATION
%%
-spec to_json(filter()) -> any().
to_json(#hyper{p = P, registers = {Mod, Registers}}) ->
Compact = Mod:compact(Registers),
{[{<<"p">>, P},
{<<"registers">>, base64:encode(zlib:gzip(Mod:encode_registers(Compact)))}]}.
-spec from_json(any()) -> filter().
from_json(Struct) ->
from_json(Struct, ?DEFAULT_BACKEND).
-spec from_json(any(), module()) -> filter().
from_json({Struct}, Mod) ->
P = proplists:get_value(<<"p">>, Struct),
Bytes = zlib:gunzip(base64:decode(proplists:get_value(<<"registers">>, Struct))),
Registers = Mod:decode_registers(Bytes, P),
#hyper{p = P, registers = {Mod, Registers}}.
%%
%% HELPERS
%%
alpha(16) ->
0.673;
alpha(32) ->
0.697;
alpha(64) ->
0.709;
alpha(M) ->
0.7213 / (1 + 1.079 / M).
pow(X, Y) ->
math:pow(X, Y).
run_of_zeroes(B) ->
run_of_zeroes(1, B).
run_of_zeroes(I, B) ->
case B of
<<0:I, _/bitstring>> ->
run_of_zeroes(I + 1, B);
_ ->
I - 1
end.
estimate_bias(E, P) ->
BiasVector = hyper_const:bias_data(P),
EstimateVector = hyper_const:estimate_data(P),
NearestNeighbours = nearest_neighbours(E, EstimateVector),
lists:sum([element(Index, BiasVector) || Index <- NearestNeighbours]) /
length(NearestNeighbours).
nearest_neighbours(E, Vector) ->
Distances = lists:map(fun (Index) ->
V = element(Index, Vector),
{pow(E - V, 2), Index}
end,
lists:seq(1, size(Vector))),
SortedDistances = lists:keysort(1, Distances),
{_, Indexes} = lists:unzip(lists:sublist(SortedDistances, 6)),
Indexes.