feat(search): add match-centred snippet extraction
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
926e241c7b
commit
df9f093b5d
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Match-centred excerpt extraction for search results.
|
||||
*/
|
||||
|
||||
const DEFAULT_MAX_LENGTH = 160;
|
||||
const CONTEXT_BEFORE = 40;
|
||||
|
||||
/**
|
||||
* Extracts an excerpt of `text` centred on the earliest match offset.
|
||||
*
|
||||
* `matchedOffsets` are offsets into the ORIGINAL text, as produced by
|
||||
* `scoreField`. When no offsets are supplied the excerpt falls back to a
|
||||
* head truncation.
|
||||
*/
|
||||
export function extractSnippet(
|
||||
text: string,
|
||||
matchedOffsets: number[],
|
||||
maxLength: number = DEFAULT_MAX_LENGTH
|
||||
): string {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (text.length <= maxLength) {
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
let start = 0;
|
||||
if (matchedOffsets.length > 0) {
|
||||
const earliest = Math.min(...matchedOffsets);
|
||||
start = Math.max(0, Math.min(earliest - CONTEXT_BEFORE, text.length - maxLength));
|
||||
}
|
||||
|
||||
const end = Math.min(text.length, start + maxLength);
|
||||
let snippet = text.slice(start, end).trim();
|
||||
|
||||
if (start > 0) {
|
||||
snippet = `...${snippet}`;
|
||||
}
|
||||
if (end < text.length) {
|
||||
snippet = `${snippet}...`;
|
||||
}
|
||||
|
||||
return snippet;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { extractSnippet } from "../../src/search/snippet.ts";
|
||||
|
||||
const LONG_TEXT =
|
||||
"The team met on Tuesday to review the quarterly plan. " +
|
||||
"After a long discussion about staffing, we reached a decision on the hiring freeze. " +
|
||||
"The remaining agenda items were deferred to the following week.";
|
||||
|
||||
describe("extractSnippet", () => {
|
||||
test("centres the window on the match rather than the start of the text", () => {
|
||||
const offset = LONG_TEXT.indexOf("hiring");
|
||||
const snippet = extractSnippet(LONG_TEXT, [offset]);
|
||||
expect(snippet).toContain("hiring");
|
||||
expect(snippet.startsWith("...")).toBe(true);
|
||||
});
|
||||
|
||||
test("adds a leading ellipsis only when the window is not at the start", () => {
|
||||
const snippet = extractSnippet(LONG_TEXT, [0]);
|
||||
expect(snippet.startsWith("...")).toBe(false);
|
||||
});
|
||||
|
||||
test("adds a trailing ellipsis when the text continues past the window", () => {
|
||||
const snippet = extractSnippet(LONG_TEXT, [0]);
|
||||
expect(snippet.endsWith("...")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns short text whole with no ellipsis", () => {
|
||||
expect(extractSnippet("Short note", [0])).toBe("Short note");
|
||||
});
|
||||
|
||||
test("falls back to a head truncation when there are no offsets", () => {
|
||||
const snippet = extractSnippet(LONG_TEXT, []);
|
||||
expect(snippet.startsWith("The team met")).toBe(true);
|
||||
expect(snippet.endsWith("...")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns an empty string for empty text", () => {
|
||||
expect(extractSnippet("", [5])).toBe("");
|
||||
});
|
||||
|
||||
test("uses the earliest offset when several are supplied", () => {
|
||||
const first = LONG_TEXT.indexOf("staffing");
|
||||
const later = LONG_TEXT.indexOf("deferred");
|
||||
const snippet = extractSnippet(LONG_TEXT, [later, first]);
|
||||
expect(snippet).toContain("staffing");
|
||||
});
|
||||
|
||||
test("respects a custom maxLength", () => {
|
||||
const snippet = extractSnippet(LONG_TEXT, [0], 20);
|
||||
expect(snippet.replace(/\.\.\.$/, "").length).toBeLessThanOrEqual(20);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user