diff --git a/apps/server/src/orchestration-v2/ThreadSearch.test.ts b/apps/server/src/orchestration-v2/ThreadSearch.test.ts index 421e79f5a0904..a853f69cc6434 100644 --- a/apps/server/src/orchestration-v2/ThreadSearch.test.ts +++ b/apps/server/src/orchestration-v2/ThreadSearch.test.ts @@ -988,6 +988,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 d3c96a40ce2b8..e0783812e90f2 100644 --- a/apps/server/src/orchestration-v2/ThreadSearch.ts +++ b/apps/server/src/orchestration-v2/ThreadSearch.ts @@ -35,6 +35,7 @@ import { import { threadSearchPullRequests } from "@t3tools/shared/threadPullRequests"; import { indexedMessageCondition, + foldedMessageSearchText, messageSearchText, nonPlaceholderMessageCondition, } from "./messageSearchIndex.ts"; @@ -245,7 +246,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(${messageSearchText(sql)}), token.value) = 0 + WHERE instr(lower(${foldedMessageSearchText(sql)}), token.value) = 0 ) AND NOT EXISTS ( SELECT 1 FROM json_each(${shortSpellings}) AS token diff --git a/apps/server/src/orchestration-v2/messageSearchIndex.ts b/apps/server/src/orchestration-v2/messageSearchIndex.ts index 87d7256b6809d..2c3342f18673c 100644 --- a/apps/server/src/orchestration-v2/messageSearchIndex.ts +++ b/apps/server/src/orchestration-v2/messageSearchIndex.ts @@ -62,6 +62,10 @@ export const readableSearchText = (text: string) => { export const messageSearchText = (sql: SqlClient.SqlClient) => sql.literal(readableText("messages")); +/** A message's readable text with apostrophes folded, as the index stores it. */ +export const foldedMessageSearchText = (sql: SqlClient.SqlClient) => + sql.literal(foldApostrophes(readableText("messages"))); + /** * A message's stored text with apostrophes folded, what the index held before * migration 81. Migration 69's backfill uses it. `payload` is the message's