Current section

Files

Jump to
cuckoo lib cuckoo.ex
Raw

lib/cuckoo.ex

defmodule Cuckoo do
@moduledoc """
This module implements a Cuckoo Filter.
## Examples
iex> cf = Cuckoo.new(1000, 16, 4)
%Cuckoo.Filter{...}
iex> {:ok, cf} = Cuckoo.insert(cf, 5)
%Cuckoo.Filter{...}
iex> Cuckoo.contains?(cf, 5)
true
iex> {:ok, cf} = Cuckoo.delete(cf, 5)
%Cuckoo.Filter{...}
iex> Cuckoo.contains?(cf, 5)
false
"""
use Bitwise
alias Cuckoo.Bucket, as: Bucket
@max_kicks 500
defmodule Filter do
defstruct [
:buckets,
:fingerprint_size,
:fingerprints_per_bucket,
:max_num_keys
]
@type t ::
%Filter{
buckets: Array.t,
fingerprint_size: pos_integer,
fingerprints_per_bucket: pos_integer,
max_num_keys: pos_integer
}
end
@doc """
Creates a new Cuckoo Filter using the given `max_num_keys`, `fingerprint_size` and
`fingerprints_per_bucket`.
The suggested values for the last two according to one of the publications should
be `16` and `4` respectively, as it allows the Cuckoo Filter to achieve a sweet spot
in space effiency and table occupancy.
"""
@spec new(pos_integer, pos_integer, pos_integer) :: Filter.t
def new(max_num_keys, fingerprint_size, fingerprints_per_bucket \\ 4) do
num_buckets = upper_power_2(max_num_keys / fingerprints_per_bucket)
frac = max_num_keys / num_buckets / fingerprints_per_bucket
%Filter{
buckets: Array.new(
[if(frac > 0.96, do: num_buckets <<< 1, else: num_buckets),
:fixed,
{:default, Bucket.new(fingerprints_per_bucket)}]
),
fingerprint_size: fingerprint_size,
fingerprints_per_bucket: fingerprints_per_bucket,
max_num_keys: max_num_keys
}
end
@doc """
Tries to insert `element` into the Cuckoo Filter.
Returns `{:ok, filter}` if successful, otherwise returns `{:err, :full}` from which
you should consider the Filter to be full.
"""
@spec insert(Filter.t, any) :: {:ok, Filter.t} | {:err, :full}
def insert(%Filter{
buckets: buckets,
fingerprint_size: bits_per_item,
fingerprints_per_bucket: fingerprints_per_bucket
} = filter, element) do
num_buckets = Array.size(buckets)
{fingerprint, i1} = fingerprint_and_index(element, num_buckets, bits_per_item)
i2 = alt_index(i1, fingerprint, num_buckets)
i1_bucket = Array.get(buckets, i1)
case Bucket.has_room?(i1_bucket) do
{:ok, index} ->
{:ok, %{filter |
buckets: Array.set(
buckets,
i1,
Bucket.set(i1_bucket, index, fingerprint)
)}}
{:err, :full} ->
i2_bucket = Array.get(buckets, i2)
case Bucket.has_room?(i2_bucket) do
{:ok, index} ->
{:ok, %{filter |
buckets: Array.set(
buckets,
i2,
Bucket.set(i2_bucket, index, fingerprint)
)}}
{:err, :full} ->
if :random.uniform(2) == 1 do
kickout(filter, i1, fingerprint, fingerprints_per_bucket)
else
kickout(filter, i2, fingerprint, fingerprints_per_bucket)
end
end
end
end
@doc """
Checks if the Cuckoo Filter contains `element`.
Returns `true` if does, otherwise returns `false`.
"""
@spec contains?(Filter.t, any) :: boolean
def contains?(%Filter{buckets: buckets, fingerprint_size: bits_per_item}, element) do
num_buckets = Array.size(buckets)
{fingerprint, i1} = fingerprint_and_index(element, num_buckets, bits_per_item)
case fingerprint in Array.get(buckets, i1) do
true -> true
false -> i2 = alt_index(i1, fingerprint, num_buckets)
fingerprint in Array.get(buckets, i2)
end
end
@doc """
Attempts to delete `element` from the Cuckoo Filter if it contains it.
Returns `{:err, :inexistent}` if the element doesn't exist in the filter, otherwise
returns `{:ok, filter}`.
"""
@spec delete(Filter.t, any) :: {:ok, Filter.t} | {:err, :inexistent}
def delete(%Filter{buckets: buckets, fingerprint_size: bits_per_item} = filter, element) do
num_buckets = Array.size(buckets)
{fingerprint, i1} = fingerprint_and_index(element, num_buckets, bits_per_item)
b1 = Array.get(buckets, i1)
case Bucket.find(b1, fingerprint) do
{:ok, index} ->
updated_bucket = Bucket.reset(b1, index)
{:ok, %{filter | buckets: Array.set(buckets, i1, updated_bucket)}}
{:err, :inexistent} ->
i2 = alt_index(i1, fingerprint, num_buckets)
b2 = Array.get(buckets, i2)
case Bucket.find(b2, fingerprint) do
{:ok, index} ->
updated_bucket = Bucket.reset(b2, index)
{:ok, %{filter | buckets: Array.set(buckets, i2, updated_bucket)}}
{:err, :inexistent} ->
{:err, :inexistent}
end
end
end
# private helper functions
@spec kickout(Filter.t, non_neg_integer, pos_integer, pos_integer, pos_integer) :: {:ok, Filter.t} | {:err, :full}
defp kickout(filter, index, fingerprint, fingerprints_per_bucket, current_kick \\ @max_kicks)
defp kickout(_, _, _, _, 0), do: {:err, :full}
defp kickout(%Filter{buckets: buckets} = filter, index, fingerprint, fingerprints_per_bucket, current_kick) do
bucket = Array.get(buckets, index)
# randomly select an entry from the bucket
rand = :random.uniform(fingerprints_per_bucket) - 1
# withdraw its fingerprint
old_fingerprint = Bucket.get(bucket, rand)
# replace it
bucket = Bucket.set(bucket, rand, fingerprint)
buckets = Array.set(buckets, index, bucket)
# find a place to put the old fingerprint
fingerprint = old_fingerprint
num_buckets = Array.size(buckets)
index = alt_index(index, fingerprint, num_buckets)
bucket = Array.get(buckets, index)
case Bucket.has_room?(bucket) do
{:ok, b_index} ->
bucket = Bucket.set(bucket, b_index, fingerprint)
buckets = Array.set(buckets, index, bucket)
{:ok, %{filter | buckets: buckets}}
{:err, :full} ->
kickout(%{filter | buckets: buckets}, index, fingerprint, fingerprints_per_bucket, current_kick - 1)
end
end
@spec index(pos_integer, pos_integer) :: non_neg_integer
defp index(hash, num_buckets) do
rem(hash, num_buckets)
end
@spec fingerprint(pos_integer, pos_integer) :: pos_integer
defp fingerprint(hash, bits_per_item) do
hash &&& ((1 <<< bits_per_item) - 1)
end
# calculates the smallest power of 2 greater than or equal to n
@spec upper_power_2(float) :: pos_integer
defp upper_power_2(n) do
:math.pow(2, Float.ceil(log2(n))) |> trunc
end
@spec log2(float) :: float
defp log2(n) do
:math.log(n) / :math.log(2)
end
@spec hash1(any) :: pos_integer
defp hash1(element), do: Murmur.hash(:x64_128, element)
@spec hash2(pos_integer) :: pos_integer
defp hash2(fingerprint), do: :erlang.phash2(fingerprint)
@spec fingerprint_and_index(any, pos_integer, pos_integer) :: {pos_integer, non_neg_integer}
defp fingerprint_and_index(element, num_buckets, bits_per_item) do
hash = hash1(element)
fingerprint = fingerprint(hash, bits_per_item)
index = index((hash >>> 32), num_buckets)
{fingerprint, index}
end
@spec alt_index(non_neg_integer, pos_integer, pos_integer) :: non_neg_integer
defp alt_index(i1, fingerprint, num_buckets) do
i1 ^^^ index(hash2(fingerprint), num_buckets)
end
end