Type some text below. We'll split it across a few mappers, each emitting a (word, 1) pair for every word it sees. Then the shuffle groups identical words together, and the reducers add up the ones — giving the final count.
<p class="hint">MapReduce in three stages. <b>Map</b>: each mapper reads its own chunk of text and emits a <code>(word, 1)</code> pair per word. <b>Shuffle</b>: pairs for the same word are grouped. <b>Reduce</b>: the ones are summed into a final count.</p>
<textarea id="text" rows="3">the quick brown fox the lazy dog the fox runs the dog sleeps</textarea>
<div class="btns">
<button id="run" type="button">Run MapReduce</button>
<button id="reset" type="button" class="ghost">Reset text</button>
</div>
<div class="stage"><h4>1 · Map <span class="sub">(independent mappers)</span></h4><div id="map" class="cols"></div></div>
<div class="stage"><h4>2 · Shuffle <span class="sub">(group by word)</span></h4><div id="shuffle" class="pairs"></div></div>
<div class="stage"><h4>3 · Reduce <span class="sub">(sum the ones)</span></h4><div id="reduce" class="pairs"></div></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
code { background: #eef2f6; padding: 0 .25em; border-radius: 4px; font-size: .85em; }
textarea { width: 100%; font: 500 14px ui-monospace, monospace; padding: .5rem;
border: 1px solid #cdd9e3; border-radius: 8px; resize: vertical; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .6rem 0 .2rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stage { margin-top: .9rem; }
.stage h4 { margin: 0 0 .4rem; font-size: .95rem; color: #1d3557; }
.sub { font-weight: 400; color: #788; font-size: .82rem; }
.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: .5rem; }
.mapper { background: #f3f6f9; border: 1px solid #d7e0e8; border-radius: 8px; padding: .45rem; }
.mapper .mh { font: 700 11px system-ui; color: #4a6072; margin-bottom: .3rem; text-transform: uppercase; letter-spacing: .04em; }
.pair { display: inline-block; background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3;
border-radius: 6px; padding: .1rem .4rem; margin: .12rem; font: 600 12px ui-monospace, monospace; }
.pairs { line-height: 1.9; }
.group { display: inline-flex; align-items: center; gap: .25rem; background: #fff; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .15rem .4rem; margin: .15rem; }
.group .k { font: 700 12px ui-monospace, monospace; color: #1d3557; }
.group .v { font: 600 11px ui-monospace, monospace; color: #788; }
.res { background: #d8f0e0; border-color: #9fd6b3; }
.res .total { font: 700 12px ui-monospace, monospace; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Word-count MapReduce. The data text is split across MAPPERS independent chunks.
// map(chunk) -> list of (word,1). shuffle groups by word. reduce sums the ones.
// Key property: each mapper is independent — order and grouping don't change the
// final totals, which is exactly why the work parallelizes across machines.
const MAPPERS = 3;
const tokens = s => s.toLowerCase().match(/[a-z0-9]+/g) || [];
function mapPhase(words){ // split words into MAPPERS chunks, each emits (w,1)
const chunks = Array.from({length: MAPPERS}, () => []);
words.forEach((w,i) => chunks[i % MAPPERS].push(w));
return chunks.map(ch => ch.map(w => [w, 1])); // per-mapper list of pairs
}
function shufflePhase(mapped){ // group all pairs by key -> {word: [1,1,...]}
const g = {};
mapped.flat().forEach(([w,one]) => (g[w] = g[w] || []).push(one));
return g;
}
function reducePhase(grouped){ // sum the ones per key
const out = {};
for (const w in grouped) out[w] = grouped[w].reduce((a,b) => a+b, 0);
return out;
}
function esc(s){ return s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
function run(){
const words = tokens(document.getElementById('text').value);
const mapped = mapPhase(words);
const grouped = shufflePhase(mapped);
const reduced = reducePhase(grouped);
// 1 · Map
document.getElementById('map').innerHTML = mapped.map((pairs,i) =>
fmt('<div class="mapper"><div class="mh">Mapper %i%%v%</div>%v2%</div>', { i: i, v: 1, v2: pairs.length ? pairs.map(([w]) => '<span class="pair">('+esc(w)+', 1)</span>').join('') : '<span class="sub">— empty —</span>' })).join('');
// 2 · Shuffle
const keys = Object.keys(grouped).sort();
document.getElementById('shuffle').innerHTML = keys.map(w =>
'<span class="group"><span class="k">'+esc(w)+'</span><span class="v">[ '+grouped[w].join(', ')+' ]</span></span>').join('');
// 3 · Reduce
document.getElementById('reduce').innerHTML = keys
.slice().sort((a,b) => reduced[b]-reduced[a] || a.localeCompare(b))
.map(w => '<span class="group res"><span class="k">'+esc(w)+'</span><span class="total">= '+reduced[w]+'</span></span>').join('');
console.log('words:', words.length, '| | reduced:', JSON.stringify(reduced));
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
document.getElementById('text').value = 'the quick brown fox the lazy dog the fox runs the dog sleeps';
run();
});
run();
<p class="hint">MapReduce in three stages. <b>Map</b>: each mapper reads its own chunk of text and emits a <code>(word, 1)</code> pair per word. <b>Shuffle</b>: pairs for the same word are grouped. <b>Reduce</b>: the ones are summed into a final count.</p>
<textarea id="text" rows="3">the quick brown fox the lazy dog the fox runs the dog sleeps</textarea>
<div class="btns">
<button id="run" type="button">Run MapReduce</button>
<button id="reset" type="button" class="ghost">Reset text</button>
</div>
<div class="stage"><h4>1 · Map <span class="sub">(independent mappers)</span></h4><div id="map" class="cols"></div></div>
<div class="stage"><h4>2 · Shuffle <span class="sub">(group by word)</span></h4><div id="shuffle" class="pairs"></div></div>
<div class="stage"><h4>3 · Reduce <span class="sub">(sum the ones)</span></h4><div id="reduce" class="pairs"></div></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
code { background: #eef2f6; padding: 0 .25em; border-radius: 4px; font-size: .85em; }
textarea { width: 100%; font: 500 14px ui-monospace, monospace; padding: .5rem;
border: 1px solid #cdd9e3; border-radius: 8px; resize: vertical; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .6rem 0 .2rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stage { margin-top: .9rem; }
.stage h4 { margin: 0 0 .4rem; font-size: .95rem; color: #1d3557; }
.sub { font-weight: 400; color: #788; font-size: .82rem; }
.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: .5rem; }
.mapper { background: #f3f6f9; border: 1px solid #d7e0e8; border-radius: 8px; padding: .45rem; }
.mapper .mh { font: 700 11px system-ui; color: #4a6072; margin-bottom: .3rem; text-transform: uppercase; letter-spacing: .04em; }
.pair { display: inline-block; background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3;
border-radius: 6px; padding: .1rem .4rem; margin: .12rem; font: 600 12px ui-monospace, monospace; }
.pairs { line-height: 1.9; }
.group { display: inline-flex; align-items: center; gap: .25rem; background: #fff; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .15rem .4rem; margin: .15rem; }
.group .k { font: 700 12px ui-monospace, monospace; color: #1d3557; }
.group .v { font: 600 11px ui-monospace, monospace; color: #788; }
.res { background: #d8f0e0; border-color: #9fd6b3; }
.res .total { font: 700 12px ui-monospace, monospace; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Word-count MapReduce. The data text is split across MAPPERS independent chunks.
// map(chunk) -> list of (word,1). shuffle groups by word. reduce sums the ones.
// Key property: each mapper is independent — order and grouping don't change the
// final totals, which is exactly why the work parallelizes across machines.
const MAPPERS = 3;
const tokens = s => s.toLowerCase().match(/[a-z0-9]+/g) || [];
function mapPhase(words){ // split words into MAPPERS chunks, each emits (w,1)
const chunks = Array.from({length: MAPPERS}, () => []);
words.forEach((w,i) => chunks[i % MAPPERS].push(w));
return chunks.map(ch => ch.map(w => [w, 1])); // per-mapper list of pairs
}
function shufflePhase(mapped){ // group all pairs by key -> {word: [1,1,...]}
const g = {};
mapped.flat().forEach(([w,one]) => (g[w] = g[w] || []).push(one));
return g;
}
function reducePhase(grouped){ // sum the ones per key
const out = {};
for (const w in grouped) out[w] = grouped[w].reduce((a,b) => a+b, 0);
return out;
}
function esc(s){ return s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
function run(){
const words = tokens(document.getElementById('text').value);
const mapped = mapPhase(words);
const grouped = shufflePhase(mapped);
const reduced = reducePhase(grouped);
// 1 · Map
document.getElementById('map').innerHTML = mapped.map((pairs,i) =>
fmt('<div class="mapper"><div class="mh">Mapper %i%%v%</div>%v2%</div>', { i: i, v: 1, v2: pairs.length ? pairs.map(([w]) => '<span class="pair">('+esc(w)+', 1)</span>').join('') : '<span class="sub">— empty —</span>' })).join('');
// 2 · Shuffle
const keys = Object.keys(grouped).sort();
document.getElementById('shuffle').innerHTML = keys.map(w =>
'<span class="group"><span class="k">'+esc(w)+'</span><span class="v">[ '+grouped[w].join(', ')+' ]</span></span>').join('');
// 3 · Reduce
document.getElementById('reduce').innerHTML = keys
.slice().sort((a,b) => reduced[b]-reduced[a] || a.localeCompare(b))
.map(w => '<span class="group res"><span class="k">'+esc(w)+'</span><span class="total">= '+reduced[w]+'</span></span>').join('');
console.log('words:', words.length, '| | reduced:', JSON.stringify(reduced));
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
document.getElementById('text').value = 'the quick brown fox the lazy dog the fox runs the dog sleeps';
run();
});
run();
Notice that each mapper works alone, on its own chunk, with no knowledge of the others. That independence is the whole trick: you could run each mapper on a different machine and the answer would be identical. Real systems do exactly this across thousands of servers — the only thing that changes is the size of the input.
Comments
Loading comments...