Skip to content

Commit 6869245

Browse files
Allow poll questions to be searchable
1 parent 7bb7c2c commit 6869245

File tree

4 files changed

+88
-21
lines changed

4 files changed

+88
-21
lines changed

ts/sql/Server.node.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ import type {
267267
import { sqlLogger } from './sqlLogger.node.js';
268268
import { permissiveMessageAttachmentSchema } from './server/messageAttachments.std.js';
269269
import { getFilePathsOwnedByMessage } from '../util/messageFilePaths.std.js';
270+
import { createMessagesOnInsertTrigger } from './migrations/1500-search-polls.std.js';
270271

271272
const {
272273
forEach,
@@ -8878,29 +8879,23 @@ function enableFSyncAndCheckpoint(db: WritableDB): void {
88788879
}
88798880

88808881
function enableMessageInsertTriggersAndBackfill(db: WritableDB): void {
8881-
const createTriggersQuery = `
8882-
DROP TRIGGER IF EXISTS messages_on_insert;
8883-
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages
8884-
WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL
8885-
BEGIN
8886-
INSERT INTO messages_fts
8887-
(rowid, body)
8888-
VALUES
8889-
(new.rowid, new.body);
8890-
END;
8882+
db.transaction(() => {
8883+
backfillMentionsTable(db);
8884+
backfillMessagesFtsTable(db);
8885+
8886+
db.exec('DROP TRIGGER IF EXISTS messages_on_insert');
8887+
db.exec(createMessagesOnInsertTrigger);
88918888

8892-
DROP TRIGGER IF EXISTS messages_on_insert_insert_mentions;
8889+
db.exec('DROP TRIGGER IF EXISTS messages_on_insert_insert_mentions');
8890+
db.exec(`
88938891
CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages
88948892
BEGIN
88958893
INSERT INTO mentions (messageId, mentionAci, start, length)
88968894
${selectMentionsFromMessages}
88978895
AND messages.id = new.id;
88988896
END;
8899-
`;
8900-
db.transaction(() => {
8901-
backfillMentionsTable(db);
8902-
backfillMessagesFtsTable(db);
8903-
db.exec(createTriggersQuery);
8897+
`);
8898+
89048899
createOrUpdateItem(db, {
89058900
id: 'messageInsertTriggersDisabled',
89068901
value: false,
@@ -8912,9 +8907,9 @@ function backfillMessagesFtsTable(db: WritableDB): void {
89128907
db.exec(`
89138908
DELETE FROM messages_fts;
89148909
INSERT OR REPLACE INTO messages_fts (rowid, body)
8915-
SELECT rowid, body
8910+
SELECT rowid, searchableText
89168911
FROM messages
8917-
WHERE isViewOnce IS NOT 1 AND storyId IS NULL;
8912+
WHERE isSearchable = 1;
89188913
`);
89198914
}
89208915

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2025 Signal Messenger, LLC
2+
// SPDX-License-Identifier: AGPL-3.0-only
3+
4+
import type { WritableDB } from '../Interface.std.js';
5+
import { sql } from '../util.std.js';
6+
7+
export const createMessagesOnInsertTrigger = sql`
8+
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages
9+
WHEN new.isSearchable IS 1
10+
BEGIN
11+
INSERT INTO messages_fts
12+
(rowid, body)
13+
VALUES
14+
(new.rowid, new.searchableText);
15+
END;
16+
`[0];
17+
18+
const createMessagesOnUpdateTrigger = sql`
19+
CREATE TRIGGER messages_on_update AFTER UPDATE ON messages
20+
WHEN
21+
new.isSearchable IS 1 AND old.searchableText IS NOT new.searchableText
22+
BEGIN
23+
UPDATE messages_fts SET body = new.searchableText WHERE rowId = new.rowId;
24+
END;
25+
`[0];
26+
27+
export default function updateToSchemaVersion1500(db: WritableDB): void {
28+
db.exec(
29+
`ALTER TABLE messages ADD COLUMN isSearchable INT
30+
GENERATED ALWAYS AS (isViewOnce IS NOT 1 AND storyId IS NULL) VIRTUAL;`
31+
);
32+
33+
// Must be kept in sync with logic in getSearchableTextAndBodyRanges
34+
db.exec(`
35+
ALTER TABLE messages ADD COLUMN searchableText TEXT GENERATED ALWAYS AS (
36+
CASE
37+
WHEN json->'poll' IS NOT NULL THEN json->'poll'->>'question'
38+
ELSE body
39+
END
40+
) VIRTUAL;
41+
`);
42+
43+
// If the messages_on_insert query is updated, enableMessageInsertTriggersAndBackfill
44+
// and backfillMessagesFtsTable must be as well
45+
db.exec('DROP TRIGGER IF EXISTS messages_on_insert;');
46+
db.exec(createMessagesOnInsertTrigger);
47+
48+
db.exec('DROP TRIGGER IF EXISTS messages_on_update;');
49+
db.exec(createMessagesOnUpdateTrigger);
50+
}

ts/sql/migrations/index.node.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import updateToSchemaVersion1460 from './1460-attachment-duration.std.js';
125125
import updateToSchemaVersion1470 from './1470-kyber-triple.std.js';
126126
import updateToSchemaVersion1480 from './1480-chat-folders-remove-duplicates.std.js';
127127
import updateToSchemaVersion1490 from './1490-lowercase-notification-profiles.std.js';
128+
import updateToSchemaVersion1500 from './1500-search-polls.std.js';
128129

129130
import { DataWriter } from '../Server.node.js';
130131

@@ -1607,6 +1608,8 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
16071608
{ version: 1470, update: updateToSchemaVersion1470 },
16081609
{ version: 1480, update: updateToSchemaVersion1480 },
16091610
{ version: 1490, update: updateToSchemaVersion1490 },
1611+
1612+
{ version: 1500, update: updateToSchemaVersion1500 },
16101613
];
16111614

16121615
export class DBVersionFromFutureError extends Error {

ts/state/selectors/search.preload.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import {
3030
} from './conversations.dom.js';
3131

3232
import { hydrateRanges } from '../../util/BodyRange.node.js';
33+
import type { RawBodyRange } from '../../types/BodyRange.std.js';
3334
import { createLogger } from '../../logging/log.std.js';
3435
import { getOwn } from '../../util/getOwn.std.js';
36+
import type { MessageAttributesType } from '../../model-types.js';
3537

3638
const log = createLogger('search');
3739

@@ -196,6 +198,23 @@ type CachedMessageSearchResultSelectorType = (
196198
targetedMessageId?: string
197199
) => MessageSearchResultPropsDataType;
198200

201+
/** Must be kept in sync with messages.searchableText virtual column */
202+
function getSearchableTextAndBodyRanges(message: MessageAttributesType): {
203+
text: string | undefined;
204+
bodyRanges: ReadonlyArray<RawBodyRange> | undefined;
205+
} {
206+
if (message.poll) {
207+
return {
208+
text: message.poll.question,
209+
bodyRanges: undefined,
210+
};
211+
}
212+
213+
return {
214+
text: message.body,
215+
bodyRanges: message.bodyRanges,
216+
};
217+
}
199218
export const getCachedSelectorForMessageSearchResult = createSelector(
200219
getUserConversationId,
201220
getConversationSelector,
@@ -213,6 +232,7 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
213232
searchConversationId?: string,
214233
targetedMessageId?: string
215234
) => {
235+
const { text, bodyRanges } = getSearchableTextAndBodyRanges(message);
216236
return {
217237
from,
218238
to,
@@ -221,9 +241,8 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
221241
conversationId: message.conversationId,
222242
sentAt: message.sent_at,
223243
snippet: message.snippet || '',
224-
bodyRanges:
225-
hydrateRanges(message.bodyRanges, conversationSelector) || [],
226-
body: message.body || '',
244+
bodyRanges: hydrateRanges(bodyRanges, conversationSelector) || [],
245+
body: text ?? '',
227246

228247
isSelected: Boolean(
229248
targetedMessageId && message.id === targetedMessageId

0 commit comments

Comments
 (0)