diff --git a/apps/server/src/orchestration-v2/ThreadSearch.test.ts b/apps/server/src/orchestration-v2/ThreadSearch.test.ts index 5d816133c89c7..540cfb8645a02 100644 --- a/apps/server/src/orchestration-v2/ThreadSearch.test.ts +++ b/apps/server/src/orchestration-v2/ThreadSearch.test.ts @@ -791,6 +791,35 @@ it.layer(layerTest)("ThreadSearch across scripts and without the index", (it) => ); }); +it.layer(layerTest)("ThreadSearch without the index", (it) => { + it.effect("finds a curly apostrophe for a straight one and back", () => + Effect.gen(function* () { + const projections = yield* ProjectionStore.ProjectionStoreV2; + const search = yield* ThreadSearch.ThreadSearch; + const sql = yield* SqlClient.SqlClient; + const project = ProjectId.make("project:scan-apostrophes"); + const curly = ThreadId.make("thread:scan-apostrophe-curly"); + const straight = ThreadId.make("thread:scan-apostrophe-straight"); + yield* createProject(project); + yield* Effect.forEach( + [ + thread(curly, project), + message(curly, "scan-apostrophe-curly", "user", "the gannet’s nest"), + thread(straight, project), + message(straight, "scan-apostrophe-straight", "user", "a kittiwake's nest"), + ], + projections.apply, + { discard: true }, + ); + yield* sql`DROP TABLE orchestration_v2_message_search`; + const threadIds = (query: string) => + search.search({ query }).pipe(Effect.map(({ matches }) => matches.map((m) => m.threadId))); + assert.deepEqual(yield* threadIds("gannet's"), [curly]); + assert.deepEqual(yield* threadIds("kittiwake’s"), [straight]); + }), + ); +}); + it.layer(layerTest)("ThreadSearch.searchOlderSettled", (it) => { it.effect("returns older settled title matches in scope, then older message hits", () => Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/ThreadSearch.ts b/apps/server/src/orchestration-v2/ThreadSearch.ts index 62c7f4eed45ec..99934b7f47627 100644 --- a/apps/server/src/orchestration-v2/ThreadSearch.ts +++ b/apps/server/src/orchestration-v2/ThreadSearch.ts @@ -33,7 +33,11 @@ import { type ThreadSearchSubject, } from "@t3tools/shared/paletteSearch"; import { threadSearchPullRequests } from "@t3tools/shared/threadPullRequests"; -import { indexedMessageCondition, nonPlaceholderMessageCondition } from "./messageSearchIndex.ts"; +import { + indexedMessageCondition, + indexedMessageText, + nonPlaceholderMessageCondition, +} from "./messageSearchIndex.ts"; import { loadOlderSettledThreadsPage } from "./ShellStream.ts"; // When the user last wrote in the thread aliased `threads`, as the clients @@ -222,7 +226,7 @@ export const messageHitsQuery = ( AND messages.message_id NOT LIKE 'async-answer:%' AND NOT EXISTS ( SELECT 1 FROM json_each(${substrings}) AS token - WHERE instr(lower(json_extract(messages.payload_json, '$.text')), token.value) = 0 + WHERE instr(lower(${indexedMessageText(sql, "messages.payload_json")}), token.value) = 0 ) -- The index's own rowid, so FTS5 returns hits newest first and stops at -- the limit; ordering by the messages table's rowid sorts every hit.