-- Phase 10: relations (threads, edits, reactions, references) and -- full-text search. -- content.m.relates_to of each event, written out so that "the children of -- this event" is an index lookup. Both are '' for an event that relates to -- nothing, and are cleared when the event is redacted: the redaction -- algorithm strips the content they came from. ALTER TABLE roomserver_events ADD COLUMN relates_to_id TEXT NOT NULL DEFAULT ''; ALTER TABLE roomserver_events ADD COLUMN rel_type TEXT NOT NULL DEFAULT ''; -- The events already stored. Only a relation with both fields a string -- counts, which is the rule the roomserver applies to new events. The LIKE -- keeps the cast to jsonb off the events that cannot have one. UPDATE roomserver_events e SET relates_to_id = r.rel ->> 'event_id', rel_type = r.rel ->> 'rel_type' FROM ( SELECT event_id, event_json::jsonb -> 'content' -> 'm.relates_to' AS rel FROM roomserver_events WHERE redacted_by = '' AND event_json LIKE '%m.relates_to%' ) r WHERE e.event_id = r.event_id AND jsonb_typeof(r.rel -> 'event_id') = 'string' AND jsonb_typeof(r.rel -> 'rel_type') = 'string'; -- /relations, the bundled summaries and /threads all start from a parent -- and read its children in stream order. CREATE INDEX IF NOT EXISTS roomserver_events_relations_idx ON roomserver_events (room_id, relates_to_id, rel_type, stream_ordering); -- The full-text index /search reads: one row per searchable event, keyed by -- the event's stream ordering, which is unique across the server and is -- what a redaction finds the row by. Outliers are not here: an event joins -- the index when it joins a timeline. -- -- The 'simple' configuration lower-cases and splits on word boundaries -- without stemming for any one language, which is the right default for a -- server whose rooms are in every language at once. CREATE TABLE IF NOT EXISTS roomserver_event_search ( stream_ordering BIGINT NOT NULL PRIMARY KEY, room_id TEXT NOT NULL, event_id TEXT NOT NULL, search_key TEXT NOT NULL, sender TEXT NOT NULL, event_type TEXT NOT NULL, body TEXT NOT NULL, vector TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', body)) STORED ); CREATE INDEX IF NOT EXISTS roomserver_event_search_vector_idx ON roomserver_event_search USING GIN (vector); CREATE INDEX IF NOT EXISTS roomserver_event_search_room_idx ON roomserver_event_search (room_id, stream_ordering); INSERT INTO roomserver_event_search (stream_ordering, room_id, event_id, search_key, sender, event_type, body) SELECT stream_ordering, room_id, event_id, k.search_key, sender, event_type, k.body FROM ( SELECT stream_ordering, room_id, event_id, sender, event_type, event_json::jsonb -> 'content' AS content FROM roomserver_events WHERE event_type IN ('m.room.message', 'm.room.name', 'm.room.topic') AND NOT outlier AND redacted_by = '' ) e CROSS JOIN LATERAL ( SELECT CASE e.event_type WHEN 'm.room.message' THEN 'content.body' WHEN 'm.room.name' THEN 'content.name' ELSE 'content.topic' END AS search_key, CASE e.event_type WHEN 'm.room.message' THEN e.content ->> 'body' WHEN 'm.room.name' THEN e.content ->> 'name' ELSE e.content ->> 'topic' END AS body ) k WHERE k.body IS NOT NULL AND jsonb_typeof(e.content -> substr(k.search_key, 9)) = 'string' ON CONFLICT (stream_ordering) DO NOTHING;