<div class="lsm-wrap">
<div class="insert-row">
<input id="key-in" type="text" placeholder="key (e.g. apple)" maxlength="12" />
<input id="val-in" type="text" placeholder="value" maxlength="16" />
<button id="btn-insert" type="button">Insert</button>
</div>
<div class="query-row">
<input id="q-in" type="text" placeholder="query key" maxlength="12" />
<button id="btn-query" type="button">Query</button>
<button id="btn-compact" type="button" class="ghost">Compact L0 → L1</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="msg" class="msg"></div>
<div class="levels">
<div class="level-block">
<div class="level-label">Memtable <span class="cap" id="mem-cap"></span></div>
<div class="run-list" id="mem-list"></div>
</div>
<div class="level-block">
<div class="level-label">Level 0 <span class="note">(unsorted runs)</span></div>
<div class="run-list" id="l0-list"></div>
</div>
<div class="level-block">
<div class="level-label">Level 1 <span class="note">(single sorted run)</span></div>
<div class="run-list" id="l1-list"></div>
</div>
</div>
<div class="legend">
<span class="chip chip-mem">memtable</span>
<span class="chip chip-l0">L0 run</span>
<span class="chip chip-l1">L1 (merged)</span>
<span class="chip chip-hit">query hit</span>
<span class="chip chip-bloom">Bloom miss</span>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1a2430; margin: 0; font-size: 14px; }
.lsm-wrap { display: flex; flex-direction: column; gap: .65rem; }
.insert-row, .query-row { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
input { flex: 1 1 90px; min-width: 70px; padding: .38rem .55rem; border: 1px solid #b0bec5; border-radius: 6px;
font: inherit; font-size: 13px; }
button { padding: .38rem .8rem; border-radius: 6px; border: 1px solid #1d3557; background: #1d3557;
color: #fff; font: 600 13px system-ui, sans-serif; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
.msg { min-height: 1.3em; font-weight: 600; font-size: .88rem; }
.msg.ok { color: #0a7d33; }
.msg.miss { color: #c62828; }
.msg.bloom { color: #e65100; }
.msg.info { color: #1565c0; }
.levels { display: flex; flex-direction: column; gap: .5rem; }
.level-block { border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .6rem; background: #f5f8fb; }
.level-label { font-weight: 700; font-size: .8rem; color: #3d5a80; margin-bottom: .3rem; }
.level-label .cap { font-weight: 400; color: #888; }
.level-label .note { font-weight: 400; color: #888; }
.run-list { display: flex; flex-wrap: wrap; gap: .35rem; min-height: 28px; }
.entry { display: inline-flex; align-items: center; gap: 3px; padding: .2rem .5rem;
border-radius: 5px; font: 600 12px ui-monospace, monospace; border: 1px solid transparent; }
.entry-mem { background: #dbeafe; border-color: #93c5fd; color: #1e3a5f; }
.entry-l0 { background: #ede9fe; border-color: #a78bfa; color: #2d1b69; }
.entry-l1 { background: #d1fae5; border-color: #6ee7b7; color: #064e3b; }
.entry.hit { outline: 2px solid #f59e0b; background: #fef3c7; }
.bloom-badge { font-size: 10px; color: #9a3412; background: #ffedd5;
border: 1px solid #fed7aa; border-radius: 4px; padding: 1px 4px; margin-left: 2px; }
.run-sep { border-left: 2px dashed #c4b5fd; height: 22px; margin: 0 .1rem; }
.legend { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .2rem; }
.chip { font-size: 11px; padding: 2px 7px; border-radius: 4px; }
.chip-mem { background: #dbeafe; color: #1e3a5f; }
.chip-l0 { background: #ede9fe; color: #2d1b69; }
.chip-l1 { background: #d1fae5; color: #064e3b; }
.chip-hit { background: #fef3c7; border: 1px solid #f59e0b; color: #78350f; }
.chip-bloom { background: #ffedd5; color: #9a3412; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ── LSM-tree simulator ────────────────────────────────────────────────────
// Memtable capacity: 4 entries. On flush, entries are sorted and written as
// an immutable SSTable run at Level 0. Compaction merges all L0 runs into a single sorted Level 1 run (oldest version wins on duplicate keys).
// Bloom filter: each level stores the set of keys it contains; a query first checks the Bloom set — if absent it skips that level (simulated miss).
const MEM_CAP = 4;
let memtable = []; // [{key,val}] — insertion order
let l0 = []; // array of runs; each run = [{key,val}] sorted
let l1 = []; // single sorted run [{key,val}]
let bloomL0 = []; // one Set per L0 run
let bloomL1 = new Set();
function setMsg(text, cls) {
const el = document.getElementById('msg');
el.textContent = text;
el.className = 'msg ' + (cls || '');
}
function renderEntry(key, val, level, highlight) {
const d = document.createElement('span');
d.className = 'entry entry-' + level + (highlight ? ' hit' : '');
d.textContent = key + ':' + val;
return d;
}
function renderBloomBadge() {
const b = document.createElement('span');
b.className = 'bloom-badge';
b.textContent = 'Bloom ✗';
return b;
}
function render(highlight) {
// highlight = { key, found } or null
const memList = document.getElementById('mem-list');
const l0List = document.getElementById('l0-list');
const l1List = document.getElementById('l1-list');
const capEl = document.getElementById('mem-cap');
memList.innerHTML = ''; l0List.innerHTML = ''; l1List.innerHTML = '';
capEl.textContent = '(' + memtable.length + '/' + MEM_CAP + ')';
// Memtable
memtable.forEach(({key, val}) => {
const isHit = highlight && highlight.key === key && highlight.level === 'mem';
memList.appendChild(renderEntry(key, val, 'mem', isHit));
});
// L0: newest run first
for (let i = l0.length - 1; i >= 0; i--) {
if (i < l0.length - 1) {
const sep = document.createElement('div'); sep.className = 'run-sep'; l0List.appendChild(sep);
}
l0[i].forEach(({key, val}) => {
const isHit = highlight && highlight.key === key && highlight.level === 'l0' && highlight.runIdx === i;
const e = renderEntry(key, val, 'l0', isHit);
l0List.appendChild(e);
});
}
// L1
l1.forEach(({key, val}) => {
const isHit = highlight && highlight.key === key && highlight.level === 'l1';
l1List.appendChild(renderEntry(key, val, 'l1', isHit));
});
}
function flush() {
const sorted = [...memtable].sort((a,b) => a.key < b.key ? -1 : 1);
l0.push(sorted);
bloomL0.push(new Set(sorted.map(e => e.key)));
memtable = [];
setMsg(fmt('Memtable full — flushed %length% entries to L0 as a sorted run.', { length: sorted.length }), 'info');
}
function insert() {
const k = document.getElementById('key-in').value.trim();
const v = document.getElementById('val-in').value.trim();
if (!k || !v) { setMsg('Enter both a key and a value.', 'miss'); return; }
// Always append to memtable (latest write wins; older versions filtered at read/compaction)
memtable.push({key: k, val: v});
document.getElementById('key-in').value = '';
document.getElementById('val-in').value = '';
setMsg(fmt('Inserted "%k%" into memtable.', { k: k }), 'info');
if (memtable.length >= MEM_CAP) flush();
render(null);
}
function compact() {
if (l0.length === 0) { setMsg('No L0 runs to compact.', 'info'); return; }
// Merge all L0 runs + existing L1 into one sorted run.
// Latest version of each key wins (scan L0 newest-first, then L0 older, then L1).
const seen = new Set();
const merged = [];
// Process: memtable writes > newest L0 run > older L0 runs > L1
// We want newest-first: iterate l0 newest-first, then l1
const allRuns = [...l0].reverse().concat([l1]);
for (const run of allRuns) {
for (const {key, val} of run) {
if (!seen.has(key)) { seen.add(key); merged.push({key, val}); }
}
}
merged.sort((a,b) => a.key < b.key ? -1 : 1);
l1 = merged;
bloomL1 = new Set(merged.map(e => e.key));
l0 = []; bloomL0 = [];
setMsg(fmt('Compacted %length% run(s) into a single sorted L1 (%length2% unique keys).', { length: allRuns.length, length2: merged.length }), 'info');
render(null);
}
function query() {
const k = document.getElementById('q-in').value.trim();
if (!k) { setMsg('Enter a key to query.', 'miss'); return; }
// Search: memtable first (newest), then L0 newest-first, then L1
// 1. Memtable scan (no Bloom filter needed)
for (let i = memtable.length - 1; i >= 0; i--) {
if (memtable[i].key === k) {
setMsg(fmt('Found "%k%" = "%val%" in memtable.', { k: k, val: memtable[i].val }), 'ok');
render({key: k, level: 'mem'});
document.getElementById('q-in').value = '';
return;
}
}
// 2. L0 newest-first; Bloom filter checked per run
let bloomMisses = 0;
for (let i = l0.length - 1; i >= 0; i--) {
if (!bloomL0[i].has(k)) { bloomMisses++; continue; } // Bloom says absent
const e = l0[i].find(x => x.key === k);
if (e) {
setMsg(fmt('Found "%k%" = "%val%" in L0 run %i% (%bloomMisses% Bloom miss(es) skipped).', { k: k, val: e.val, i: i, bloomMisses: bloomMisses }), 'ok');
render({key: k, level: 'l0', runIdx: i});
document.getElementById('q-in').value = '';
return;
}
}
// 3. L1 Bloom filter
if (!bloomL1.has(k)) {
setMsg(fmt('"%k%" not found — Bloom filter ruled out memtable, all L0 runs, and L1 instantly.', { k: k }), 'bloom');
render(null);
document.getElementById('q-in').value = '';
return;
}
const e1 = l1.find(x => x.key === k);
if (e1) {
setMsg(fmt('Found "%k%" = "%val%" in L1 (%bloomMisses% Bloom miss(es) in L0).', { k: k, val: e1.val, bloomMisses: bloomMisses }), 'ok');
render({key: k, level: 'l1'});
document.getElementById('q-in').value = '';
return;
}
setMsg(fmt('"%k%" not found in any level.', { k: k }), 'miss');
render(null);
document.getElementById('q-in').value = '';
}
function reset() {
memtable = []; l0 = []; l1 = []; bloomL0 = []; bloomL1 = new Set();
setMsg('Reset — all levels cleared.', 'info');
render(null);
}
document.getElementById('btn-insert').addEventListener('click', insert);
document.getElementById('btn-query').addEventListener('click', query);
document.getElementById('btn-compact').addEventListener('click', compact);
document.getElementById('btn-reset').addEventListener('click', reset);
// Allow Enter key in inputs
['key-in','val-in'].forEach(id =>
document.getElementById(id).addEventListener('keydown', e => { if(e.key==='Enter') insert(); }));
document.getElementById('q-in').addEventListener('keydown', e => { if(e.key==='Enter') query(); });
// Seed a few entries so the demo is non-empty on load
const seed = [['apple','red'],['cherry','dark'],['banana','yellow'],['date','sweet'],
['elderberry','purple'],['fig','brown']];
seed.forEach(([k,v]) => {
memtable.push({key:k,val:v});
if(memtable.length>=MEM_CAP) flush();
});
render(null);
<div class="lsm-wrap">
<div class="insert-row">
<input id="key-in" type="text" placeholder="key (e.g. apple)" maxlength="12" />
<input id="val-in" type="text" placeholder="value" maxlength="16" />
<button id="btn-insert" type="button">Insert</button>
</div>
<div class="query-row">
<input id="q-in" type="text" placeholder="query key" maxlength="12" />
<button id="btn-query" type="button">Query</button>
<button id="btn-compact" type="button" class="ghost">Compact L0 → L1</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="msg" class="msg"></div>
<div class="levels">
<div class="level-block">
<div class="level-label">Memtable <span class="cap" id="mem-cap"></span></div>
<div class="run-list" id="mem-list"></div>
</div>
<div class="level-block">
<div class="level-label">Level 0 <span class="note">(unsorted runs)</span></div>
<div class="run-list" id="l0-list"></div>
</div>
<div class="level-block">
<div class="level-label">Level 1 <span class="note">(single sorted run)</span></div>
<div class="run-list" id="l1-list"></div>
</div>
</div>
<div class="legend">
<span class="chip chip-mem">memtable</span>
<span class="chip chip-l0">L0 run</span>
<span class="chip chip-l1">L1 (merged)</span>
<span class="chip chip-hit">query hit</span>
<span class="chip chip-bloom">Bloom miss</span>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1a2430; margin: 0; font-size: 14px; }
.lsm-wrap { display: flex; flex-direction: column; gap: .65rem; }
.insert-row, .query-row { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
input { flex: 1 1 90px; min-width: 70px; padding: .38rem .55rem; border: 1px solid #b0bec5; border-radius: 6px;
font: inherit; font-size: 13px; }
button { padding: .38rem .8rem; border-radius: 6px; border: 1px solid #1d3557; background: #1d3557;
color: #fff; font: 600 13px system-ui, sans-serif; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
.msg { min-height: 1.3em; font-weight: 600; font-size: .88rem; }
.msg.ok { color: #0a7d33; }
.msg.miss { color: #c62828; }
.msg.bloom { color: #e65100; }
.msg.info { color: #1565c0; }
.levels { display: flex; flex-direction: column; gap: .5rem; }
.level-block { border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .6rem; background: #f5f8fb; }
.level-label { font-weight: 700; font-size: .8rem; color: #3d5a80; margin-bottom: .3rem; }
.level-label .cap { font-weight: 400; color: #888; }
.level-label .note { font-weight: 400; color: #888; }
.run-list { display: flex; flex-wrap: wrap; gap: .35rem; min-height: 28px; }
.entry { display: inline-flex; align-items: center; gap: 3px; padding: .2rem .5rem;
border-radius: 5px; font: 600 12px ui-monospace, monospace; border: 1px solid transparent; }
.entry-mem { background: #dbeafe; border-color: #93c5fd; color: #1e3a5f; }
.entry-l0 { background: #ede9fe; border-color: #a78bfa; color: #2d1b69; }
.entry-l1 { background: #d1fae5; border-color: #6ee7b7; color: #064e3b; }
.entry.hit { outline: 2px solid #f59e0b; background: #fef3c7; }
.bloom-badge { font-size: 10px; color: #9a3412; background: #ffedd5;
border: 1px solid #fed7aa; border-radius: 4px; padding: 1px 4px; margin-left: 2px; }
.run-sep { border-left: 2px dashed #c4b5fd; height: 22px; margin: 0 .1rem; }
.legend { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .2rem; }
.chip { font-size: 11px; padding: 2px 7px; border-radius: 4px; }
.chip-mem { background: #dbeafe; color: #1e3a5f; }
.chip-l0 { background: #ede9fe; color: #2d1b69; }
.chip-l1 { background: #d1fae5; color: #064e3b; }
.chip-hit { background: #fef3c7; border: 1px solid #f59e0b; color: #78350f; }
.chip-bloom { background: #ffedd5; color: #9a3412; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ── LSM-tree simulator ────────────────────────────────────────────────────
// Memtable capacity: 4 entries. On flush, entries are sorted and written as
// an immutable SSTable run at Level 0. Compaction merges all L0 runs into a single sorted Level 1 run (oldest version wins on duplicate keys).
// Bloom filter: each level stores the set of keys it contains; a query first checks the Bloom set — if absent it skips that level (simulated miss).
const MEM_CAP = 4;
let memtable = []; // [{key,val}] — insertion order
let l0 = []; // array of runs; each run = [{key,val}] sorted
let l1 = []; // single sorted run [{key,val}]
let bloomL0 = []; // one Set per L0 run
let bloomL1 = new Set();
function setMsg(text, cls) {
const el = document.getElementById('msg');
el.textContent = text;
el.className = 'msg ' + (cls || '');
}
function renderEntry(key, val, level, highlight) {
const d = document.createElement('span');
d.className = 'entry entry-' + level + (highlight ? ' hit' : '');
d.textContent = key + ':' + val;
return d;
}
function renderBloomBadge() {
const b = document.createElement('span');
b.className = 'bloom-badge';
b.textContent = 'Bloom ✗';
return b;
}
function render(highlight) {
// highlight = { key, found } or null
const memList = document.getElementById('mem-list');
const l0List = document.getElementById('l0-list');
const l1List = document.getElementById('l1-list');
const capEl = document.getElementById('mem-cap');
memList.innerHTML = ''; l0List.innerHTML = ''; l1List.innerHTML = '';
capEl.textContent = '(' + memtable.length + '/' + MEM_CAP + ')';
// Memtable
memtable.forEach(({key, val}) => {
const isHit = highlight && highlight.key === key && highlight.level === 'mem';
memList.appendChild(renderEntry(key, val, 'mem', isHit));
});
// L0: newest run first
for (let i = l0.length - 1; i >= 0; i--) {
if (i < l0.length - 1) {
const sep = document.createElement('div'); sep.className = 'run-sep'; l0List.appendChild(sep);
}
l0[i].forEach(({key, val}) => {
const isHit = highlight && highlight.key === key && highlight.level === 'l0' && highlight.runIdx === i;
const e = renderEntry(key, val, 'l0', isHit);
l0List.appendChild(e);
});
}
// L1
l1.forEach(({key, val}) => {
const isHit = highlight && highlight.key === key && highlight.level === 'l1';
l1List.appendChild(renderEntry(key, val, 'l1', isHit));
});
}
function flush() {
const sorted = [...memtable].sort((a,b) => a.key < b.key ? -1 : 1);
l0.push(sorted);
bloomL0.push(new Set(sorted.map(e => e.key)));
memtable = [];
setMsg(fmt('Memtable full — flushed %length% entries to L0 as a sorted run.', { length: sorted.length }), 'info');
}
function insert() {
const k = document.getElementById('key-in').value.trim();
const v = document.getElementById('val-in').value.trim();
if (!k || !v) { setMsg('Enter both a key and a value.', 'miss'); return; }
// Always append to memtable (latest write wins; older versions filtered at read/compaction)
memtable.push({key: k, val: v});
document.getElementById('key-in').value = '';
document.getElementById('val-in').value = '';
setMsg(fmt('Inserted "%k%" into memtable.', { k: k }), 'info');
if (memtable.length >= MEM_CAP) flush();
render(null);
}
function compact() {
if (l0.length === 0) { setMsg('No L0 runs to compact.', 'info'); return; }
// Merge all L0 runs + existing L1 into one sorted run.
// Latest version of each key wins (scan L0 newest-first, then L0 older, then L1).
const seen = new Set();
const merged = [];
// Process: memtable writes > newest L0 run > older L0 runs > L1
// We want newest-first: iterate l0 newest-first, then l1
const allRuns = [...l0].reverse().concat([l1]);
for (const run of allRuns) {
for (const {key, val} of run) {
if (!seen.has(key)) { seen.add(key); merged.push({key, val}); }
}
}
merged.sort((a,b) => a.key < b.key ? -1 : 1);
l1 = merged;
bloomL1 = new Set(merged.map(e => e.key));
l0 = []; bloomL0 = [];
setMsg(fmt('Compacted %length% run(s) into a single sorted L1 (%length2% unique keys).', { length: allRuns.length, length2: merged.length }), 'info');
render(null);
}
function query() {
const k = document.getElementById('q-in').value.trim();
if (!k) { setMsg('Enter a key to query.', 'miss'); return; }
// Search: memtable first (newest), then L0 newest-first, then L1
// 1. Memtable scan (no Bloom filter needed)
for (let i = memtable.length - 1; i >= 0; i--) {
if (memtable[i].key === k) {
setMsg(fmt('Found "%k%" = "%val%" in memtable.', { k: k, val: memtable[i].val }), 'ok');
render({key: k, level: 'mem'});
document.getElementById('q-in').value = '';
return;
}
}
// 2. L0 newest-first; Bloom filter checked per run
let bloomMisses = 0;
for (let i = l0.length - 1; i >= 0; i--) {
if (!bloomL0[i].has(k)) { bloomMisses++; continue; } // Bloom says absent
const e = l0[i].find(x => x.key === k);
if (e) {
setMsg(fmt('Found "%k%" = "%val%" in L0 run %i% (%bloomMisses% Bloom miss(es) skipped).', { k: k, val: e.val, i: i, bloomMisses: bloomMisses }), 'ok');
render({key: k, level: 'l0', runIdx: i});
document.getElementById('q-in').value = '';
return;
}
}
// 3. L1 Bloom filter
if (!bloomL1.has(k)) {
setMsg(fmt('"%k%" not found — Bloom filter ruled out memtable, all L0 runs, and L1 instantly.', { k: k }), 'bloom');
render(null);
document.getElementById('q-in').value = '';
return;
}
const e1 = l1.find(x => x.key === k);
if (e1) {
setMsg(fmt('Found "%k%" = "%val%" in L1 (%bloomMisses% Bloom miss(es) in L0).', { k: k, val: e1.val, bloomMisses: bloomMisses }), 'ok');
render({key: k, level: 'l1'});
document.getElementById('q-in').value = '';
return;
}
setMsg(fmt('"%k%" not found in any level.', { k: k }), 'miss');
render(null);
document.getElementById('q-in').value = '';
}
function reset() {
memtable = []; l0 = []; l1 = []; bloomL0 = []; bloomL1 = new Set();
setMsg('Reset — all levels cleared.', 'info');
render(null);
}
document.getElementById('btn-insert').addEventListener('click', insert);
document.getElementById('btn-query').addEventListener('click', query);
document.getElementById('btn-compact').addEventListener('click', compact);
document.getElementById('btn-reset').addEventListener('click', reset);
// Allow Enter key in inputs
['key-in','val-in'].forEach(id =>
document.getElementById(id).addEventListener('keydown', e => { if(e.key==='Enter') insert(); }));
document.getElementById('q-in').addEventListener('keydown', e => { if(e.key==='Enter') query(); });
// Seed a few entries so the demo is non-empty on load
const seed = [['apple','red'],['cherry','dark'],['banana','yellow'],['date','sweet'],
['elderberry','purple'],['fig','brown']];
seed.forEach(([k,v]) => {
memtable.push({key:k,val:v});
if(memtable.length>=MEM_CAP) flush();
});
render(null);
Notice how every insert is a fast sequential append, while a query has to probe the Bloom filter for each level before reading the data. The more levels that exist, the higher the read amplification — the price paid for write speed.
Comments
Loading comments...