Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/archive/query/search-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ CREATE INDEX IF NOT EXISTS idx_search_sessions_prune
ON search_sessions(accessed_at DESC, created_at DESC, session_id);
`;

const SEARCH_RANKING_VERSION = 5;
const SEARCH_RANKING_VERSION = 6;
const SEARCH_SESSION_MAX_COUNT = 500;
const SEARCH_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const SEARCH_TOP_SCORE_COUNT = 10;
Expand Down
5 changes: 4 additions & 1 deletion src/archive/search-index/search-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface SearchIndexQueryResult {

const SEARCH_INDEX_VERSION = "3";
export const SEARCH_INDEX_FTS_HIT_LIMIT = 32_000;
const FTS5_RANK_SCORE_SCALE = 1_000_000;
const TIER_WEIGHTS = [1, 0.45, 0.08] as const;

export interface ArchiveIndexSettings {
Expand Down Expand Up @@ -748,7 +749,9 @@ function createTextKindFilter(
}

function rankToScore(rank: number): number {
return 1 / (1 + Math.max(0, Math.abs(rank)));
const relevance = Math.max(0, -rank) * FTS5_RANK_SCORE_SCALE;

return relevance / (1 + relevance);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function createObjectHitKey(hit: SearchIndexObjectHit): string {
Expand Down
111 changes: 111 additions & 0 deletions test/archive/query/archive-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,117 @@ describe("archive/query/archive-view", () => {
});
});

it("orders text search hits by FTS relevance within the text bucket", async () => {
await withTempDir("spinedigest-archive-view-", async (path) => {
const document = await DirectoryDocument.open(`${path}/document`);

try {
await document.openSession(async (openedDocument) => {
await openedDocument.createSerial();
const draft = await openedDocument
.getSerialFragments(1)
.createDraft();

draft.addSentence("alpha appears in a weak candidate.", 6);
draft.addSentence("alpha beta alpha beta is the strongest match.", 8);
await draft.commit();
await openedDocument.writeBookMeta({
authors: [],
description: null,
identifier: null,
language: "en",
publishedAt: null,
publisher: null,
sourceFormat: "markdown",
title: "Search Ranking Fixture",
version: 1,
});
await openedDocument.writeToc({
items: [
{
children: [],
serialId: 1,
title: "Ranking",
},
],
version: 1,
});
});
await rebuildArchiveSearchIndex(document);

const result = await findArchiveObjects(document, "alpha beta", {
limit: 10,
types: ["source"],
});

expect(result.items.map((item) => item.id)).toStrictEqual([
"wikg://chapter/1/source#1",
"wikg://chapter/1/source#0",
]);
expect(result.items[0]?.score).toBeGreaterThan(
result.items[1]?.score ?? 0,
);
} finally {
await document.release();
}
});
});

it("orders chapter search hits by FTS relevance within the object property bucket", async () => {
await withTempDir("spinedigest-archive-view-", async (path) => {
const document = await DirectoryDocument.open(`${path}/document`);

try {
await document.openSession(async (openedDocument) => {
await openedDocument.createSerial();
await openedDocument.createSerial();
await openedDocument.writeBookMeta({
authors: [],
description: null,
identifier: null,
language: "en",
publishedAt: null,
publisher: null,
sourceFormat: "markdown",
title: "Chapter Ranking Fixture",
version: 1,
});
await openedDocument.writeToc({
items: [
{
children: [],
serialId: 1,
title: "alpha appears in a weak chapter",
},
{
children: [],
serialId: 2,
title: "alpha beta alpha beta strongest chapter",
},
],
version: 1,
});
});
await rebuildArchiveSearchIndex(document);

const result = await findArchiveObjects(document, "alpha beta", {
limit: 10,
types: ["chapter"],
});

expect(result.items.map((item) => item.id)).toStrictEqual([
"chapter:2",
"chapter:1",
]);
expect(result.items[0]?.score).toBeGreaterThan(
result.items[1]?.score ?? 0,
);
} finally {
await document.release();
}
});
});

it("limits FTS candidates before search cache hydration", async () => {
await withTempDir("spinedigest-archive-view-", async (path) => {
const document = await DirectoryDocument.open(`${path}/document`);
Expand Down
Loading