Skip to content

Commit 3365055

Browse files
Make search relevance smoke tests use a synthetic public corpus
Closes #224
1 parent 883c3f6 commit 3365055

1 file changed

Lines changed: 32 additions & 46 deletions

File tree

backend/tests/docs-search.test.ts

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, before, after } from 'node:test';
22
import assert from 'node:assert';
33
import { mkdtempSync, rmSync } from 'node:fs';
44
import { tmpdir } from 'node:os';
5-
import { join, resolve } from 'node:path';
5+
import { join } from 'node:path';
66

77
import {
88
createSearchIndex,
@@ -13,10 +13,8 @@ import {
1313
type SearchDocument,
1414
type SearchResult,
1515
} from '../src/docs/searchIndex';
16-
import { extractDoc, iterContentDocs } from '../src/docs/search/extract';
17-
import { findContentRoot } from './helpers/content';
16+
import { extractDoc } from '../src/docs/search/extract';
1817

19-
const CONTENT_DIR = findContentRoot();
2018
const paths = (results: SearchResult[]): string[] => results.map((r) => String(r.path));
2119

2220
describe('docs search - field config (parity with docs_index.py)', () => {
@@ -144,51 +142,39 @@ describe('docs search - save/load round-trip (json-1)', () => {
144142
});
145143
});
146144

147-
// Smoke fixtures updated for TF-IDF (minsearch) -> BM25-lite (zerosearch).
148-
// Captured against a minsearch top-k baseline: top-1 is identical for every
149-
// query below; ordering shifts inside the relevant set, recall stays on par
150-
// (see issue #85 notes). Each fixture asserts the BM25-lite top-1 plus a doc
151-
// that must remain in the top-5 (recall guard).
152-
const SMOKE: { query: string; top1: string; recall: string }[] = [
153-
{
154-
query: 'podcast intake',
155-
top1: 'content/media/podcast/templates/podcast-share-the-podcast-page-template.md',
156-
recall: 'content/tasks/templates/podcast.md',
157-
},
158-
{
159-
query: 'newsletter sponsor',
160-
top1: 'content/overview/reference/newsletter.md',
161-
recall: 'content/newsletter/sponsorship/sops/creating-a-document-for-sponsored-content-for-a-newsletter.md',
162-
},
163-
{
164-
query: 'course certificate',
165-
top1: 'content/tasks/templates/course.md',
166-
recall: 'content/courses/reference/course-guide.md',
167-
},
168-
{
169-
query: 'youtube upload',
170-
top1: 'content/media/open-source-spotlight/reference/for-update-download-open-source-spotlight-video-from-zoom-and-upload-it-to-youtube.md',
171-
recall: 'content/media/video-youtube/sops/downloading-and-uploading-videos-from-loom-to-youtube.md',
172-
},
145+
// Public synthetic relevance fixtures: title/summary matches should outrank
146+
// incidental single-word matches, while related body text remains discoverable.
147+
const SMOKE = [
148+
{ query: 'podcast intake', title: 'Podcast intake checklist', related: 'Recording preparation', distractor: 'Podcast audio settings' },
149+
{ query: 'newsletter sponsor', title: 'Newsletter sponsor overview', related: 'Publication planning', distractor: 'Newsletter typography' },
150+
{ query: 'course certificate', title: 'Course certificate guide', related: 'Completion records', distractor: 'Course exercises' },
151+
{ query: 'youtube upload', title: 'YouTube upload checklist', related: 'Video preparation', distractor: 'YouTube analytics' },
173152
];
174153

175-
describe('docs search - smoke query relevance over the document corpus (BM25-lite)', () => {
176-
// The corpus lives in the private knowledge repository, so these rank real
177-
// documents only when it is checked out.
178-
let index: ReturnType<typeof createSearchIndex>;
179-
before(() => {
180-
if (!CONTENT_DIR) return;
181-
const docs = iterContentDocs(CONTENT_DIR);
182-
index = createSearchIndex().fit(docs);
183-
});
184-
185-
for (const { query, top1, recall } of SMOKE) {
186-
it(`ranks the expected doc first and keeps recall for "${query}"`, (t) => {
187-
if (!CONTENT_DIR) return t.skip('knowledge repository not checked out');
154+
describe('docs search - smoke query relevance over a public synthetic corpus (BM25-lite)', () => {
155+
const corpus: SearchDocument[] = SMOKE.flatMap(({ query, title, related, distractor }, index) => [
156+
{
157+
path: `synthetic/topic-${index}/overview.md`, id: `overview-${index}`,
158+
title, summary: `A synthetic overview of ${query}.`,
159+
body: 'Reference material for a test topic.',
160+
},
161+
{
162+
path: `synthetic/topic-${index}/related.md`, id: `related-${index}`,
163+
title: related, body: `This synthetic reference also discusses ${query}.`,
164+
},
165+
{
166+
path: `synthetic/topic-${index}/incidental.md`, id: `incidental-${index}`,
167+
title: distractor, body: 'A separate topic with only an incidental keyword match.',
168+
},
169+
]);
170+
const index = createSearchIndex().fit(corpus);
171+
172+
for (const [topic, { query }] of SMOKE.entries()) {
173+
it(`ranks the expected doc first and keeps recall for "${query}"`, () => {
188174
const results = index.search(query, { numResults: 5 });
189-
assert.ok(results.length > 0, 'expected at least one result');
190-
assert.strictEqual(results[0].path, top1, `top-1 for "${query}"`);
191-
assert.ok(paths(results).includes(recall), `"${recall}" should be in top-5 for "${query}"`);
175+
assert.strictEqual(results[0]?.path, `synthetic/topic-${topic}/overview.md`, `top-1 for "${query}"`);
176+
assert.ok(paths(results).includes(`synthetic/topic-${topic}/related.md`), 'related body-text match remains in top-5');
177+
assert.ok(paths(results).includes(`synthetic/topic-${topic}/incidental.md`), 'a competing single-term document was also searched');
192178
});
193179
}
194180
});

0 commit comments

Comments
 (0)