Tuesday, 24 February 2009

Bloom Filters

Bloom Filters are an efficient data structure for determining whether an item is a member of a set. It's a probabilistic set, it's guaranteed to never return a false negative BUT can sometimes falsely report that an item is in the set.

The Bloom filter doesn't store the data in the structure, instead it uses a bit array. There are two operations on a Bloom filter:

  1. Add - as the name suggests adds a new element to the filter.
  2. Query - returns whether an item is in the set or not


A series of hash functions (ideally independent) are used to calculate a number of indices within the bit-array. When we add something we set the various bit indices, and when we query we check whether all these bit indices are set.


(defstruct bloom-filter :hashfns :value)

(defn make-bloom-filter
([n] (struct bloom-filter md5-hashes (bit-array n)))
([n fns] (struct bloom-filter fns (bit-array n))))

(defn add!
[bloom n]
(let [hashes (map (fn [x] (x n)) (bloom :hashfns))]
(doseq [x hashes] (set-bit! (bloom :value) x 1))
bloom))

(defn query
[bloom n]
(let [hashes (map (fn [x] (x n)) (bloom :hashfns))]
(reduce bit-and (map (fn [z] (get-bit (bloom :value) z)) hashes))))


The gotcha for me was remembering to use doseq for side effects. If instead I'd used map I'd have (and was) in trouble because it wasn't evaluated. doseq forces the evaluation.

One simple choice for the hashes is to use MD5 hash values and split it. MessageDigest allows you to calculate various hash functions.


(ns bloom
(:use bitarray)
(:use clojure.contrib.test-is)
(:import (java.security MessageDigest)))

(defn pad [n s]
(let [padding (- n (count s))]
(apply str (concat (apply str (repeat padding "0")) s))))

(defn md5-hash [s]
(let [m (MessageDigest/getInstance "MD5")]
(.update m (.getBytes (str s)) 0 (count s))
(let [x (.toString (BigInteger. 1 (.digest m)) 16)]
(pad 32 x))))


So how well does this work?


(deftest test-bloom
(let [teststrs (map (fn [x] (str x)) (range 0 1000))
bloom (make-bloom-filter 0xFFFF)]
(doseq [x teststrs]
(is (= 0 (query bloom x)))
(add! bloom x)
(is (= 0 (query bloom (str "not" x))))
(is (query bloom x)))))


In this example, I've used hash functions which break MD5 down into 4 lots of 4 hex characters which gives a range of 65536.

Running this test gives


bloom> (run-tests 'bloom)
Ran 1 tests containing 3000 assertions.
0 failures, 0 errors.


Awesome, no false positives. Taking it down to 0xFFF gives 82 false positives which is inline(ish!) with the figures here Bloom filter error table.

The primary use case is caching - check it's in some storage mechanism before doing something expensive (BigTable and Squid Cache both use bloom filters.).

Rapleaf write about how using a Bloom filter saved some serious time (see here).

Monday, 23 February 2009

Bit Fields using Clojure

A bit array is a way of getting a very compact array of Boolean values with each value being represented as a single bit. Bit arrays are usually associated with low(er)-level like C, but you can do them in Clojure too.

Clojure provides array functions through aget and aset (slightly strange in that it mutates a data structure in place). Using Java arrays isn't very functional, but in my opinion this is about providing the balance between strictness (e.g. Haskell) and leniency (e.g. C).

We define a bit-array as a structure consisting of some array data and the width of each field.


(defstruct bit-field :element-width :array-data)

(defn bit-array
[n]
(struct bit-field 31 (int-array (inc (int (/ n 31))))))


Where 31 is the range of an integer in Java (it doesn't support unsigned) (yeah, my thinking is fuzzy here, but I think that's right... Certainly 32 fails all my tests.).

A bit-array only consists of a couple of operations, get-bit and set-bit!. The bang (!) notation at the end of the set-bit function name is an informal way of indicating that the function mutates its data.


(defn set-bit!
[bitfield bit val]
(let [r (mod bit (bitfield :element-width))
n (int (/ bit (bitfield :element-width)))
x (aget (bitfield :array-data) n)]
(if (not (zero? val))
(aset (bitfield :array-data) n (bit-or x (bit-shift-left 1 r)))
(aset (bitfield :array-data) n (bit-xor x (bit-shift-left 1 r))))
bitfield))

(defn get-bit
[bitfield bit]
(let [r (mod bit (bitfield :element-width))
x (aget (bitfield :array-data) (int (/ bit (bitfield :element-width))))]
(if (= 0 (bit-and x (bit-shift-left 1 r))) 0 1)))


We work out the index in the array to change by dividing the bit to set by the width of each element and use bit-shift-left to identify the bit in question to twiddle.

How can we be sure this works? Well, we can never be 100% sure without a proof, but we can at least run it against a reasonable set of data! Clojure Contrib has a very simple library for testing. You define tests using the deftest macro, each test consists of a number of is functions that verify assertions.



(deftest test-bits
(let [n 3000
f (bit-array n)]
(is (= 31 (f :element-width)))
(doseq [x (range 0 n)]
(is (= 0 (get-bit f x))))
(doseq [x (range 0 n)]
(set-bit! f x 1)
(is (= 1 (get-bit f x))))
(doseq [x (range 0 n)]
(set-bit! f x 0)


Tests are run using run-tests. For example:


bitarray> (run-tests 'bitarray)

Testing bitarray

Ran 1 tests containing 9001 assertions.
0 failures, 0 errors.
nil


So it passes the tests therefore it at least vaguely works!

Friday, 20 February 2009

Lazy Clojure, Slime and Emacs

The latest version of Swank Clojure is now compatible with the recent lazy changes to Clojure.

The most visible changes:

  • Use lazy-seq to construct things, lazy-cons is gone for example:

    user> (lazy-seq (cons 1 (cons 2 (cons 3 ()))))
    (1 2 3)

  • Nil punning is gone (mentioned before)