bloomfilter.gno
5.07 Kb · 163 lines
1// Package bloomfilter ports the classic Bloom filter data structure to
2// gno.land: a fixed-size bit array plus k independent hash functions that
3// let you test set membership with zero false negatives and a small, bounded
4// false-positive rate — without ever storing the actual items.
5//
6// Membership is checked via double hashing (Kirsch–Mitzenmacher): two base
7// hashes h1, h2 are combined as h1 + i*h2 to derive k bit positions per item,
8// avoiding the cost of k independent hash functions.
9//
10// Add is a crossing function per the gno 0.9 interrealm convention (it takes
11// `cur realm` as its first parameter); MightContain and Stats are read-only.
12package bloomfilter
13
14import (
15 "chain"
16 "strconv"
17 "strings"
18)
19
20const (
21 numBits = 2048 // m: total bits in the filter
22 numBytes = numBits / 8
23 numHashes = 5 // k: hash functions per item (double-hashed from 2 bases)
24 maxHistory = 20 // most recent additions kept for Render display only
25)
26
27var (
28 bits [numBytes]byte // the bit array itself
29 setBits int // running count of bits currently set to 1
30 added int // total Add calls (may double-count re-adds)
31 history []string // last few added items, for the Render view
32)
33
34// fnv1a is a minimal FNV-1a 32-bit hash over a string, parameterized by an
35// offset basis so two calls with different seeds behave as independent
36// hash functions for the double-hashing scheme below.
37func fnv1a(s string, seed uint32) uint32 {
38 h := seed
39 for i := 0; i < len(s); i++ {
40 h ^= uint32(s[i])
41 h *= 16777619 // FNV prime
42 }
43 return h
44}
45
46// positions returns the k bit positions an item hashes to.
47func positions(item string) [numHashes]uint32 {
48 h1 := fnv1a(item, 2166136261)
49 h2 := fnv1a(item, 84696351)
50 if h2 == 0 {
51 h2 = 1 // keep the second hash non-degenerate
52 }
53 var pos [numHashes]uint32
54 for i := 0; i < numHashes; i++ {
55 pos[i] = (h1 + uint32(i)*h2) % numBits
56 }
57 return pos
58}
59
60// setBit sets bit `pos` and reports whether it was previously unset.
61func setBit(pos uint32) bool {
62 byteIdx := pos / 8
63 mask := byte(1 << (pos % 8))
64 if bits[byteIdx]&mask != 0 {
65 return false
66 }
67 bits[byteIdx] |= mask
68 return true
69}
70
71// testBit reports whether bit `pos` is set.
72func testBit(pos uint32) bool {
73 byteIdx := pos / 8
74 mask := byte(1 << (pos % 8))
75 return bits[byteIdx]&mask != 0
76}
77
78// Add inserts `item` into the filter. Crossing function: any caller (user or
79// realm) may add, matching this demo's open-membership model.
80func Add(cur realm, item string) {
81 if item == "" {
82 panic("bloomfilter: empty item")
83 }
84 for _, pos := range positions(item) {
85 if setBit(pos) {
86 setBits++
87 }
88 }
89 added++
90 history = append(history, item)
91 if len(history) > maxHistory {
92 history = history[len(history)-maxHistory:]
93 }
94 chain.Emit("Add", "item", item, "totalAdded", strconv.Itoa(added))
95}
96
97// MightContain reports whether `item` was possibly added before. A false
98// (definitely-not-a-member) answer is always correct; a true answer can
99// occasionally be a false positive, never a false negative.
100func MightContain(item string) bool {
101 if item == "" {
102 return false
103 }
104 for _, pos := range positions(item) {
105 if !testBit(pos) {
106 return false
107 }
108 }
109 return true
110}
111
112// FalsePositiveRatePercent estimates the current false-positive rate, in
113// percent, as (bitsSet/m)^k — the standard Bloom filter approximation once
114// bits are randomly distributed. Computed with plain integer/float math to
115// avoid depending on math.Exp/Pow availability.
116func FalsePositiveRatePercent() float64 {
117 fillRatio := float64(setBits) / float64(numBits)
118 rate := 1.0
119 for i := 0; i < numHashes; i++ {
120 rate *= fillRatio
121 }
122 return rate * 100
123}
124
125// Stats returns the raw counters backing the Render view and
126// FalsePositiveRatePercent: (itemsAdded, bitsSet, totalBits, hashCount).
127func Stats() (int, int, int, int) {
128 return added, setBits, numBits, numHashes
129}
130
131// Render draws the filter's current stats and recently-added items as
132// Markdown for gnoweb.
133func Render(path string) string {
134 var b strings.Builder
135
136 b.WriteString("# Bloom Filter\n\n")
137 b.WriteString("A probabilistic set membership test: `Add` an item, then " +
138 "`MightContain` it back. False positives are possible; false " +
139 "negatives never happen. The filter never stores the items " +
140 "themselves — only " + strconv.Itoa(numBits) + " bits.\n\n")
141
142 fillRatio := float64(setBits) / float64(numBits) * 100
143
144 b.WriteString("## Stats\n\n")
145 b.WriteString("- **Bits (m):** " + strconv.Itoa(numBits) + "\n")
146 b.WriteString("- **Hash functions (k):** " + strconv.Itoa(numHashes) + "\n")
147 b.WriteString("- **Items added:** " + strconv.Itoa(added) + "\n")
148 b.WriteString("- **Bits set:** " + strconv.Itoa(setBits) + " (" +
149 strconv.FormatFloat(fillRatio, 'f', 1, 64) + "% full)\n")
150 b.WriteString("- **Estimated false-positive rate:** " +
151 strconv.FormatFloat(FalsePositiveRatePercent(), 'f', 3, 64) + "%\n\n")
152
153 b.WriteString("## Recently added\n\n")
154 if len(history) == 0 {
155 b.WriteString("_Nothing added yet — call `Add` with an item string._\n")
156 return b.String()
157 }
158 for i := len(history) - 1; i >= 0; i-- {
159 b.WriteString("- `" + history[i] + "`\n")
160 }
161
162 return b.String()
163}