Below is a tiny Bloom filter built from a dictionary of ten common words. It uses three independent hash functions and a 32-bit array. Type any word and press Check to see whether it passes, gets flagged, or triggers a false positive.
<p class="hint">A Bloom filter stores the dictionary as a 32-bit array using 3 hash functions. Type a word and press Check to see which bits are probed. A word is <b>flagged</b> only if at least one probed bit is OFF — that is a definite miss. If all bits are ON the word is <b>probably correct</b>, but it might be a false positive.</p>
<div class="dict-display" id="dict-display"></div>
<div class="bits-row">
<span class="bits-label">Filter bits (32):</span>
<div class="bits" id="bits"></div>
</div>
<div class="input-row">
<input id="word-input" type="text" placeholder="Type a word…" autocomplete="off" spellcheck="false" />
<button id="check-btn" type="button">Check</button>
</div>
<div class="status" id="status"></div>
<div class="hash-row" id="hash-row"></div>
<button id="reset-btn" type="button" class="ghost">Reset</button>
/* General layout styles. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.dict-display { font-size: .8rem; color: #555; margin-bottom: .55rem; }
.dict-display b { color: #1d3557; }
/* Bit array display styles. */
.bits-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; flex-wrap: wrap; }
.bits-label { font-size: .78rem; color: #555; white-space: nowrap; }
.bits { display: flex; gap: 3px; flex-wrap: wrap; }
.bit { width: 18px; height: 18px; border-radius: 4px; background: #d0d5dc; font-size: .65rem;
display: flex; align-items: center; justify-content: center; color: #888;
transition: background .15s; }
.bit.on { background: #1d3557; color: #fff; }
.bit.hit { background: #e63946; color: #fff; }
/* Input row and button styles. */
.input-row { display: flex; gap: .5rem; margin-bottom: .55rem; }
input { flex: 1; padding: .42rem .7rem; border: 1px solid #ccc; border-radius: 8px;
font: 400 15px system-ui; outline: none; }
input:focus { border-color: #1d3557; }
button { font: 600 14px system-ui; padding: .42rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; font-size: .85rem; padding: .32rem .7rem; }
/* Status message styles. */
.status { font-size: .98rem; font-weight: 600; min-height: 1.4em; margin-bottom: .35rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #b06e00; }
/* Hash detail row styles. */
.hash-row { font-size: .78rem; color: #666; min-height: 1.2em; margin-bottom: .5rem; }
/* Bloom filter spellchecker: encodes a dictionary into a compact bit array. */
/* No false negatives: if a bit is OFF the word is definitely absent. */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* The fixed dictionary of known words. */
const DICT = ['apple','banana','cherry','dragon','example','filter','grape','honey','island','jungle'];
/* Filter parameters. */
const M = 32; // number of bits in the filter
const K = 3; // number of independent hash functions
/* Deterministic hash using a seed; maps a word to one of M bit positions. */
function hashFn(word, seed) {
let h = seed;
for (let i = 0; i < word.length; i++) {
h = Math.imul(31, h) ^ word.charCodeAt(i);
h |= 0;
}
return ((h >>> 0) % M);
}
function bloomBits(word) {
/* Returns the K bit positions this word maps to. */
const positions = [];
for (let s = 1; s <= K; s++) positions.push(hashFn(word, s * 0x9e3779b9 | 0));
return positions;
}
/* Build the filter by setting bits for every dictionary word. */
const filter = new Uint8Array(M);
DICT.forEach(w => { bloomBits(w).forEach(p => { filter[p] = 1; }); });
function check(word) {
/* Returns bit positions, whether all are set, and whether the word is truly in the dict. */
const positions = bloomBits(word);
const allSet = positions.every(p => filter[p] === 1);
const inDict = DICT.includes(word.toLowerCase());
return { positions, allSet, inDict };
}
/* Render the dictionary label. */
function renderDict() {
const el = document.getElementById('dict-display');
el.innerHTML = 'Dictionary (10 words): ' + DICT.map(w => `<b>${w}</b>`).join(', ');
}
function renderBits(highlighted) {
/* Render the bit array; highlight probed positions in red. */
const el = document.getElementById('bits');
el.innerHTML = '';
for (let i = 0; i < M; i++) {
const d = document.createElement('div');
d.className = 'bit' + (highlighted && highlighted.includes(i) ? ' hit' : (filter[i] ? ' on' : ''));
d.title = i;
el.appendChild(d);
}
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text; s.className = 'status ' + cls;
}
function setHashRow(text) {
document.getElementById('hash-row').textContent = text;
}
function doCheck() {
const raw = document.getElementById('word-input').value.trim();
if (!raw) return;
const word = raw.toLowerCase();
const { positions, allSet, inDict } = check(word);
renderBits(positions);
const posStr = positions.join(', ');
setHashRow(i18nFmt('%k% hash positions probed: %positions%', { k: K, positions: posStr }));
if (!allSet) {
/* At least one bit is off — definite miss. */
setStatus(i18nFmt('✗ '%word%' is NOT in the dictionary — at least one bit was off.', { word: raw }), 'bad');
} else if (inDict) {
/* All bits on and word is truly in the dictionary. */
setStatus(i18nFmt('✓ '%word%' is in the dictionary — all bits are on.', { word: raw }), 'ok');
} else {
/* All bits on but word is NOT in dictionary — false positive. */
setStatus(i18nFmt('⚠ '%word%' is NOT in the dictionary — but all bits are on. This is a false positive!', { word: raw }), 'warn');
}
}
document.getElementById('check-btn').addEventListener('click', doCheck);
document.getElementById('word-input').addEventListener('keydown', e => {
if (e.key === 'Enter') doCheck();
});
document.getElementById('reset-btn').addEventListener('click', () => {
document.getElementById('word-input').value = '';
renderBits(null);
setStatus('', '');
setHashRow('');
});
renderDict();
renderBits(null);
<p class="hint">A Bloom filter stores the dictionary as a 32-bit array using 3 hash functions. Type a word and press Check to see which bits are probed. A word is <b>flagged</b> only if at least one probed bit is OFF — that is a definite miss. If all bits are ON the word is <b>probably correct</b>, but it might be a false positive.</p>
<div class="dict-display" id="dict-display"></div>
<div class="bits-row">
<span class="bits-label">Filter bits (32):</span>
<div class="bits" id="bits"></div>
</div>
<div class="input-row">
<input id="word-input" type="text" placeholder="Type a word…" autocomplete="off" spellcheck="false" />
<button id="check-btn" type="button">Check</button>
</div>
<div class="status" id="status"></div>
<div class="hash-row" id="hash-row"></div>
<button id="reset-btn" type="button" class="ghost">Reset</button>
/* General layout styles. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.dict-display { font-size: .8rem; color: #555; margin-bottom: .55rem; }
.dict-display b { color: #1d3557; }
/* Bit array display styles. */
.bits-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; flex-wrap: wrap; }
.bits-label { font-size: .78rem; color: #555; white-space: nowrap; }
.bits { display: flex; gap: 3px; flex-wrap: wrap; }
.bit { width: 18px; height: 18px; border-radius: 4px; background: #d0d5dc; font-size: .65rem;
display: flex; align-items: center; justify-content: center; color: #888;
transition: background .15s; }
.bit.on { background: #1d3557; color: #fff; }
.bit.hit { background: #e63946; color: #fff; }
/* Input row and button styles. */
.input-row { display: flex; gap: .5rem; margin-bottom: .55rem; }
input { flex: 1; padding: .42rem .7rem; border: 1px solid #ccc; border-radius: 8px;
font: 400 15px system-ui; outline: none; }
input:focus { border-color: #1d3557; }
button { font: 600 14px system-ui; padding: .42rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; font-size: .85rem; padding: .32rem .7rem; }
/* Status message styles. */
.status { font-size: .98rem; font-weight: 600; min-height: 1.4em; margin-bottom: .35rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #b06e00; }
/* Hash detail row styles. */
.hash-row { font-size: .78rem; color: #666; min-height: 1.2em; margin-bottom: .5rem; }
/* Bloom filter spellchecker: encodes a dictionary into a compact bit array. */
/* No false negatives: if a bit is OFF the word is definitely absent. */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* The fixed dictionary of known words. */
const DICT = ['apple','banana','cherry','dragon','example','filter','grape','honey','island','jungle'];
/* Filter parameters. */
const M = 32; // number of bits in the filter
const K = 3; // number of independent hash functions
/* Deterministic hash using a seed; maps a word to one of M bit positions. */
function hashFn(word, seed) {
let h = seed;
for (let i = 0; i < word.length; i++) {
h = Math.imul(31, h) ^ word.charCodeAt(i);
h |= 0;
}
return ((h >>> 0) % M);
}
function bloomBits(word) {
/* Returns the K bit positions this word maps to. */
const positions = [];
for (let s = 1; s <= K; s++) positions.push(hashFn(word, s * 0x9e3779b9 | 0));
return positions;
}
/* Build the filter by setting bits for every dictionary word. */
const filter = new Uint8Array(M);
DICT.forEach(w => { bloomBits(w).forEach(p => { filter[p] = 1; }); });
function check(word) {
/* Returns bit positions, whether all are set, and whether the word is truly in the dict. */
const positions = bloomBits(word);
const allSet = positions.every(p => filter[p] === 1);
const inDict = DICT.includes(word.toLowerCase());
return { positions, allSet, inDict };
}
/* Render the dictionary label. */
function renderDict() {
const el = document.getElementById('dict-display');
el.innerHTML = 'Dictionary (10 words): ' + DICT.map(w => `<b>${w}</b>`).join(', ');
}
function renderBits(highlighted) {
/* Render the bit array; highlight probed positions in red. */
const el = document.getElementById('bits');
el.innerHTML = '';
for (let i = 0; i < M; i++) {
const d = document.createElement('div');
d.className = 'bit' + (highlighted && highlighted.includes(i) ? ' hit' : (filter[i] ? ' on' : ''));
d.title = i;
el.appendChild(d);
}
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text; s.className = 'status ' + cls;
}
function setHashRow(text) {
document.getElementById('hash-row').textContent = text;
}
function doCheck() {
const raw = document.getElementById('word-input').value.trim();
if (!raw) return;
const word = raw.toLowerCase();
const { positions, allSet, inDict } = check(word);
renderBits(positions);
const posStr = positions.join(', ');
setHashRow(i18nFmt('%k% hash positions probed: %positions%', { k: K, positions: posStr }));
if (!allSet) {
/* At least one bit is off — definite miss. */
setStatus(i18nFmt('✗ '%word%' is NOT in the dictionary — at least one bit was off.', { word: raw }), 'bad');
} else if (inDict) {
/* All bits on and word is truly in the dictionary. */
setStatus(i18nFmt('✓ '%word%' is in the dictionary — all bits are on.', { word: raw }), 'ok');
} else {
/* All bits on but word is NOT in dictionary — false positive. */
setStatus(i18nFmt('⚠ '%word%' is NOT in the dictionary — but all bits are on. This is a false positive!', { word: raw }), 'warn');
}
}
document.getElementById('check-btn').addEventListener('click', doCheck);
document.getElementById('word-input').addEventListener('keydown', e => {
if (e.key === 'Enter') doCheck();
});
document.getElementById('reset-btn').addEventListener('click', () => {
document.getElementById('word-input').value = '';
renderBits(null);
setStatus('', '');
setHashRow('');
});
renderDict();
renderBits(null);
Notice what happens when you type a word that is not in the dictionary but whose bits all happen to be set anyway — the filter reports it as probably correct. That is a false positive: the filter is wrong, but only in the safe direction. It will never tell you a real dictionary word is a typo.
Comments
Loading comments...