{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE Trustworthy #-}
-- | A bloom filter for the Redis in-memory store.
module Data.RedisBloom
(
-- * Bloom filter configuration
-- ** Fundamental types
module Data.RedisBloom.Internal,
-- ** Static bloom filter configuration
Bloom(..),
-- * Bloom filter operations
createBF, createIfNewBF, addBF, queryBF, querySafeBF
) where
#if !MIN_VERSION_base(4,8,0)
import Prelude hiding (mapM)
import Data.Traversable (Traversable(..))
import Data.Foldable (foldMap)
#endif
import Data.Monoid (All(..))
import Data.ByteString.Char8 (pack)
import Database.Redis
import Data.RedisBloom.Hash
import Data.RedisBloom.Internal
-- | Bloom filter static configuration.
-- To use suggested values based on the desired
-- false-positive rate and capacity, use 'Data.RedisBloom.Suggestions.suggestCreate'.
data Bloom a = Bloom {
-- | The key to store the bloom filter under.
key :: !Key,
-- | Bloom filter capacity, i.e. the number of bits used.
capacity :: !Capacity,
-- | The hash family associated with the bloom filter.
-- See 'Data.RedisBloom.Hash.hashFamilyFNV' and 'Data.RedisBloom.Hash.hashFamilySimple'
hf :: HashFamily a
}
-- | Create a new bloom filter with the specified configuration.
createBF :: (RedisCtx m (Either Reply)) => Bloom a -> m (Either Reply Status)
createBF bf = set (key bf) empty
where
empty = pack ""
-- | Create a new bloom filter with the specified configuration if the specified key does not yet exist.
createIfNewBF :: (RedisCtx m (Either Reply)) => Bloom a -> m (Either Reply Bool)
createIfNewBF bf = setnx (key bf) empty
where
empty = pack ""
-- | Add an element to an existing bloom filter.
addBF :: (RedisCtx m f) => Bloom a -> a -> m ()
addBF bf = mapM_ (flip (setbit (key bf)) one) . fmap (toInteger . (`mod` cap) . fromIntegral) . hf bf
where
(Capacity cap) = capacity bf
one = pack "1"
getBit :: forall m f a. (Functor f, MonadRedis m, RedisCtx m f) => Bloom a -> Integer -> m (f Bool)
getBit bf i = do
r <- getbit (key bf) i
return . fmap (>= 1) $ (r :: f Integer)
-- | Query whether an element exists in the bloom filter.
querySafeBF :: (Applicative f, MonadRedis m, RedisCtx m f) => Bloom a -> a -> m (f Bool)
querySafeBF bf = query (capacity bf) (getBit bf) (hf bf)
-- | Query whether an element exists in the bloom filter.
--
-- Gracefully fails upon failure by returning 'False'.
queryBF :: (MonadRedis m, RedisCtx m (Either Reply)) => Bloom a -> a -> m Bool
queryBF bf = fmap (either (const False) id) . query (capacity bf) (getBit bf) (hf bf)
query :: (Applicative f, Monad m) => Capacity -> (Integer -> m (f Bool)) -> HashFamily a -> a -> m (f Bool)
query (Capacity c) q hashf x = do
let hashes = fmap (toInteger . (`mod` c) . fromIntegral) . hashf $ x
lookupMany q hashes
lookupMany :: (Applicative f, Traversable t, Monad m) => (a -> m (f Bool)) -> t a -> m (f Bool)
lookupMany lookupBit hashes = do
bools <- mapM lookupBit hashes
let b' = fmap (getAll . foldMap All) . sequenceA $ bools
return b'