Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions scripts/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,18 @@ check('search matches an author name', () => {
assert.ok(hits[0].matched.includes('nirodbaran'));
});

check('snippet centres on the window covering the most distinct terms', () => {
// 'silence' appears early (isolated) and again near 'integral' much later.
// A window centred on the early occurrence captures only 'silence'; a window
// centred near 'integral' captures both. The improved snippet picks the latter.
const filler = 'The way unfolds gradually. '.repeat(4);
const s = snippet(
{ body: `Silence rests here. ${filler}In integral yoga, silence is paramount.` },
['integral', 'silence'],
{ contextChars: 80 },
);
assert.ok(s.toLowerCase().includes('integral'), `expected 'integral' in: ${s}`);
assert.ok(s.toLowerCase().includes('silence'), `expected 'silence' in: ${s}`);
});

console.log(`\n${passed} checks passed.`);
47 changes: 38 additions & 9 deletions src/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,15 @@ export function tokenize(text) {
}

/**
* Build a short contextual snippet around the first matched term, for display
* beneath a search result. Looks in the summary first, then the body; if no
* term is found, falls back to the start of the text.
* Build a short contextual snippet around the best window for the matched
* terms, for display beneath a search result. Looks in the summary first, then
* the body; if no term is found, falls back to the start of the text.
*
* "Best window" means the position (among all occurrences of all terms) that
* maximises the number of distinct terms visible within the context window.
* For single-term queries this is identical to the previous behaviour; for
* multi-term queries it avoids returning a snippet that shows only one term
* when the terms cluster together elsewhere in the text.
*
* @param {{summary?: string, body?: string}} article
* @param {string[]} terms lowercased query terms (as returned in `matched`)
Expand All @@ -121,14 +127,37 @@ export function snippet(article, terms = [], { contextChars = 120 } = {}) {
for (const text of [article.summary, article.body]) {
if (!text) continue;
const lower = text.toLowerCase();
let idx = -1;

// Collect every occurrence position of every term.
const occurrences = [];
for (const t of terms) {
const at = lower.indexOf(t);
if (at !== -1 && (idx === -1 || at < idx)) idx = at;
let at = lower.indexOf(t);
while (at !== -1) {
occurrences.push({ pos: at, term: t });
at = lower.indexOf(t, at + 1);
}
}
if (!occurrences.length) continue;

// Pick the position whose window covers the most distinct terms.
// Ties go to the earlier position (natural iteration order).
let bestPos = occurrences[0].pos;
let bestCoverage = 0;
for (const { pos } of occurrences) {
const lo = pos - half;
const hi = pos + half;
const seen = new Set();
for (const o of occurrences) {
if (o.pos >= lo && o.pos <= hi) seen.add(o.term);
}
if (seen.size > bestCoverage) {
bestCoverage = seen.size;
bestPos = pos;
}
}
if (idx === -1) continue;
const start = Math.max(0, idx - half);
const end = Math.min(text.length, idx + half);

const start = Math.max(0, bestPos - half);
const end = Math.min(text.length, bestPos + half);
let s = text.slice(start, end).trim();
if (start > 0) s = `… ${s}`;
if (end < text.length) s = `${s} …`;
Expand Down
Loading