Below are five short documents and a query box. The demo computes a TF-IDF score for each document against your query and ranks them. Try single words like "algorithm" or "matrix", then try rare words versus common ones like "the" and watch the scores collapse.
<!-- Interactive TF-IDF document ranker — type a query, click Rank. -->
<p class="hint">Enter one or more words and click <b>Rank</b>. Each document gets a TF-IDF score: high when the word appears <em>often in that document</em> and <em>rarely across all documents</em>. Try common words like "the" and watch scores drop to zero.</p>
<div class="query-row">
<label for="query">Query:</label>
<input id="query" type="text" placeholder="e.g. algorithm" autocomplete="off" spellcheck="false" />
<button id="btn-rank" type="button">Rank</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="results" class="results"></div>
/* Styles for the TF-IDF ranker demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.query-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .9rem; }
label { font-weight: 600; font-size: .9rem; white-space: nowrap; }
input { flex: 1 1 140px; padding: .4rem .65rem; border: 1px solid #9ab; border-radius: 7px;
font: 15px system-ui, sans-serif; min-width: 80px; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
.results { display: flex; flex-direction: column; gap: .55rem; }
.doc-card { border: 1px solid #d0d8e0; border-radius: 10px; padding: .6rem .8rem;
background: #f7f9fb; transition: border-color .15s; }
.doc-card.top { border-color: #1d6a3a; background: #f0faf4; }
.doc-header { display: flex; align-items: center; gap: .5rem; margin-bottom: .3rem; }
.doc-rank { font: 700 13px ui-monospace, monospace; color: #1d3557; min-width: 22px; }
.doc-title { font-weight: 700; font-size: .92rem; flex: 1; }
.doc-score { font: 600 13px ui-monospace, monospace; color: #1d6a3a; min-width: 60px; text-align: right; }
.doc-score.zero { color: #999; }
.bar-wrap { height: 6px; background: #dde4ea; border-radius: 3px; overflow: hidden; }
.bar { height: 6px; background: #1d3557; border-radius: 3px; transition: width .3s; }
.doc-text { font-size: .82rem; color: #555; margin-top: .35rem; line-height: 1.45; }
.doc-text mark { background: #fef08a; border-radius: 2px; padding: 0 1px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// TF-IDF: score = sum over query terms of tf(term, doc) * idf(term, corpus)
// tf(term, doc) = count(term in doc) / total tokens in doc
// idf(term) = log((N+1)/(df(term)+1)) + 1 (smoothed to avoid zero for rare terms)
// score(doc, query) = sum of tf*idf for each query term
// The five sample documents (each is a short paragraph about a CS topic).
const DOCS = [
{ id: 'doc1', title: 'Algorithms & Complexity', text: 'An algorithm is a finite sequence of well-defined instructions used to solve a problem or perform a computation. Algorithms can be analyzed for time complexity and space complexity. Sorting algorithms, graph algorithms, and dynamic programming are key topics in computer science.' },
{ id: 'doc2', title: 'Machine Learning', text: 'Machine learning algorithms allow computers to learn from data without being explicitly programmed. Gradient descent, backpropagation, and neural networks are core techniques. Training a deep learning model requires large datasets and significant computational resources.' },
{ id: 'doc3', title: 'Linear Algebra', text: 'Linear algebra studies vectors, matrices, and linear transformations. The eigenvalue decomposition of a matrix reveals its intrinsic structure. Matrix multiplication and the dot product are fundamental operations used throughout mathematics and machine learning.' },
{ id: 'doc4', title: 'Databases', text: 'A relational database stores data in tables with rows and columns. SQL is the standard query language for retrieving and manipulating data. Indexes speed up query execution by allowing the engine to locate rows without a full table scan.' },
{ id: 'doc5', title: 'Information Retrieval', text: 'Information retrieval systems rank documents by relevance to a query. TF-IDF weights each term by its frequency in the document and its rarity across the corpus. BM25 extends TF-IDF with length normalization and term saturation to improve ranking quality.' }
];
// Lowercase, strip punctuation, split on whitespace.
function tokenize(str) {
return str.toLowerCase().replace(/[^a-z\s]/g, ' ').split(/\s+/).filter(Boolean);
}
// Build per-doc token frequency map.
function buildIndex(docs) {
const index = {};
docs.forEach(doc => {
const tokens = tokenize(doc.text);
const freq = {};
tokens.forEach(t => { freq[t] = (freq[t] || 0) + 1; });
index[doc.id] = { tokens, freq, len: tokens.length };
});
return index;
}
// Count how many docs contain each term (document frequency).
function computeDF(docs, index) {
const df = {};
docs.forEach(doc => {
Object.keys(index[doc.id].freq).forEach(term => {
df[term] = (df[term] || 0) + 1;
});
});
return df;
}
// TF-IDF score for one doc against the query terms.
function scoreDoc(docId, queryTerms, index, df, N) {
const { freq, len } = index[docId];
if (!len) return 0;
let score = 0;
queryTerms.forEach(term => {
const tf = (freq[term] || 0) / len;
const idf = Math.log((N + 1) / ((df[term] || 0) + 1)) + 1; // Smoothed IDF keeps rare terms from getting infinite weight.
score += tf * idf;
});
return score;
}
// Wrap matching query terms in <mark> tags.
function highlight(text, terms) {
const re = new RegExp('\\b(' + terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\amp;')).join('|') + ')\\b', 'gi');
return text.replace(re, '<mark>$1</mark>');
}
const idx = buildIndex(DOCS);
const df = computeDF(DOCS, idx);
const N = DOCS.length;
function render(scored) {
const container = document.getElementById('results');
const maxScore = Math.max(...scored.map(d => d.score), 1e-9);
container.innerHTML = scored.map((d, i) => {
const pct = maxScore > 0 ? (d.score / maxScore) * 100 : 0;
const scoreStr = d.score.toFixed(4);
const isTop = i === 0 && d.score > 0;
const body = d.queryTerms.length ? highlight(d.doc.text, d.queryTerms) : d.doc.text;
return `<div class="doc-card${isTop ? ' top' : ''}">
<div class="doc-header">
<span class="doc-rank">#${i + 1}</span>
<span class="doc-title">${d.doc.title}</span>
<span class="doc-score${d.score === 0 ? ' zero' : ''}">${scoreStr}</span>
</div>
<div class="bar-wrap"><div class="bar" style="width:${pct.toFixed(1)}%"></div></div>
<div class="doc-text">${body}</div>
</div>`;
}).join('');
}
function rankDocs(query) {
const qTerms = tokenize(query);
const scored = DOCS.map(doc => ({
doc,
score: scoreDoc(doc.id, qTerms, idx, df, N),
queryTerms: qTerms
}));
scored.sort((a, b) => b.score - a.score);
render(scored);
}
// Show all docs in original order with score 0 at startup.
function renderDefault() {
render(DOCS.map(doc => ({ doc, score: 0, queryTerms: [] })));
}
document.getElementById('btn-rank').addEventListener('click', () => {
rankDocs(document.getElementById('query').value.trim());
});
document.getElementById('query').addEventListener('keydown', e => {
if (e.key === 'Enter') rankDocs(document.getElementById('query').value.trim());
});
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('query').value = '';
renderDefault();
});
renderDefault();
<!-- Interactive TF-IDF document ranker — type a query, click Rank. -->
<p class="hint">Enter one or more words and click <b>Rank</b>. Each document gets a TF-IDF score: high when the word appears <em>often in that document</em> and <em>rarely across all documents</em>. Try common words like "the" and watch scores drop to zero.</p>
<div class="query-row">
<label for="query">Query:</label>
<input id="query" type="text" placeholder="e.g. algorithm" autocomplete="off" spellcheck="false" />
<button id="btn-rank" type="button">Rank</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="results" class="results"></div>
/* Styles for the TF-IDF ranker demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.query-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .9rem; }
label { font-weight: 600; font-size: .9rem; white-space: nowrap; }
input { flex: 1 1 140px; padding: .4rem .65rem; border: 1px solid #9ab; border-radius: 7px;
font: 15px system-ui, sans-serif; min-width: 80px; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
.results { display: flex; flex-direction: column; gap: .55rem; }
.doc-card { border: 1px solid #d0d8e0; border-radius: 10px; padding: .6rem .8rem;
background: #f7f9fb; transition: border-color .15s; }
.doc-card.top { border-color: #1d6a3a; background: #f0faf4; }
.doc-header { display: flex; align-items: center; gap: .5rem; margin-bottom: .3rem; }
.doc-rank { font: 700 13px ui-monospace, monospace; color: #1d3557; min-width: 22px; }
.doc-title { font-weight: 700; font-size: .92rem; flex: 1; }
.doc-score { font: 600 13px ui-monospace, monospace; color: #1d6a3a; min-width: 60px; text-align: right; }
.doc-score.zero { color: #999; }
.bar-wrap { height: 6px; background: #dde4ea; border-radius: 3px; overflow: hidden; }
.bar { height: 6px; background: #1d3557; border-radius: 3px; transition: width .3s; }
.doc-text { font-size: .82rem; color: #555; margin-top: .35rem; line-height: 1.45; }
.doc-text mark { background: #fef08a; border-radius: 2px; padding: 0 1px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// TF-IDF: score = sum over query terms of tf(term, doc) * idf(term, corpus)
// tf(term, doc) = count(term in doc) / total tokens in doc
// idf(term) = log((N+1)/(df(term)+1)) + 1 (smoothed to avoid zero for rare terms)
// score(doc, query) = sum of tf*idf for each query term
// The five sample documents (each is a short paragraph about a CS topic).
const DOCS = [
{ id: 'doc1', title: 'Algorithms & Complexity', text: 'An algorithm is a finite sequence of well-defined instructions used to solve a problem or perform a computation. Algorithms can be analyzed for time complexity and space complexity. Sorting algorithms, graph algorithms, and dynamic programming are key topics in computer science.' },
{ id: 'doc2', title: 'Machine Learning', text: 'Machine learning algorithms allow computers to learn from data without being explicitly programmed. Gradient descent, backpropagation, and neural networks are core techniques. Training a deep learning model requires large datasets and significant computational resources.' },
{ id: 'doc3', title: 'Linear Algebra', text: 'Linear algebra studies vectors, matrices, and linear transformations. The eigenvalue decomposition of a matrix reveals its intrinsic structure. Matrix multiplication and the dot product are fundamental operations used throughout mathematics and machine learning.' },
{ id: 'doc4', title: 'Databases', text: 'A relational database stores data in tables with rows and columns. SQL is the standard query language for retrieving and manipulating data. Indexes speed up query execution by allowing the engine to locate rows without a full table scan.' },
{ id: 'doc5', title: 'Information Retrieval', text: 'Information retrieval systems rank documents by relevance to a query. TF-IDF weights each term by its frequency in the document and its rarity across the corpus. BM25 extends TF-IDF with length normalization and term saturation to improve ranking quality.' }
];
// Lowercase, strip punctuation, split on whitespace.
function tokenize(str) {
return str.toLowerCase().replace(/[^a-z\s]/g, ' ').split(/\s+/).filter(Boolean);
}
// Build per-doc token frequency map.
function buildIndex(docs) {
const index = {};
docs.forEach(doc => {
const tokens = tokenize(doc.text);
const freq = {};
tokens.forEach(t => { freq[t] = (freq[t] || 0) + 1; });
index[doc.id] = { tokens, freq, len: tokens.length };
});
return index;
}
// Count how many docs contain each term (document frequency).
function computeDF(docs, index) {
const df = {};
docs.forEach(doc => {
Object.keys(index[doc.id].freq).forEach(term => {
df[term] = (df[term] || 0) + 1;
});
});
return df;
}
// TF-IDF score for one doc against the query terms.
function scoreDoc(docId, queryTerms, index, df, N) {
const { freq, len } = index[docId];
if (!len) return 0;
let score = 0;
queryTerms.forEach(term => {
const tf = (freq[term] || 0) / len;
const idf = Math.log((N + 1) / ((df[term] || 0) + 1)) + 1; // Smoothed IDF keeps rare terms from getting infinite weight.
score += tf * idf;
});
return score;
}
// Wrap matching query terms in <mark> tags.
function highlight(text, terms) {
const re = new RegExp('\\b(' + terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\amp;')).join('|') + ')\\b', 'gi');
return text.replace(re, '<mark>$1</mark>');
}
const idx = buildIndex(DOCS);
const df = computeDF(DOCS, idx);
const N = DOCS.length;
function render(scored) {
const container = document.getElementById('results');
const maxScore = Math.max(...scored.map(d => d.score), 1e-9);
container.innerHTML = scored.map((d, i) => {
const pct = maxScore > 0 ? (d.score / maxScore) * 100 : 0;
const scoreStr = d.score.toFixed(4);
const isTop = i === 0 && d.score > 0;
const body = d.queryTerms.length ? highlight(d.doc.text, d.queryTerms) : d.doc.text;
return `<div class="doc-card${isTop ? ' top' : ''}">
<div class="doc-header">
<span class="doc-rank">#${i + 1}</span>
<span class="doc-title">${d.doc.title}</span>
<span class="doc-score${d.score === 0 ? ' zero' : ''}">${scoreStr}</span>
</div>
<div class="bar-wrap"><div class="bar" style="width:${pct.toFixed(1)}%"></div></div>
<div class="doc-text">${body}</div>
</div>`;
}).join('');
}
function rankDocs(query) {
const qTerms = tokenize(query);
const scored = DOCS.map(doc => ({
doc,
score: scoreDoc(doc.id, qTerms, idx, df, N),
queryTerms: qTerms
}));
scored.sort((a, b) => b.score - a.score);
render(scored);
}
// Show all docs in original order with score 0 at startup.
function renderDefault() {
render(DOCS.map(doc => ({ doc, score: 0, queryTerms: [] })));
}
document.getElementById('btn-rank').addEventListener('click', () => {
rankDocs(document.getElementById('query').value.trim());
});
document.getElementById('query').addEventListener('keydown', e => {
if (e.key === 'Enter') rankDocs(document.getElementById('query').value.trim());
});
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('query').value = '';
renderDefault();
});
renderDefault();
Notice how a word that appears in all five documents gets an IDF of zero and contributes nothing to any score. A word that appears in only one document gets a high IDF and a document that uses it repeatedly shoots to the top.
Comments
Loading comments...