feat: Add full text search over chat messages (#27126)

Closes CODAGT-721
Closes CODAGT-722
Closes CODAGT-723
Closes CODAGT-724
Closes CODAGT-725

This PR adds the database and API pieces necessary to support full-text
chat message search.

- Adds required chat schema for full-text search
- Adds dbpurge job to populate search_tsv in the background
- Adds `search` parameter to GetChats query
- Adds `search` filter to `searchquery.Chats`
- Wires chat search filter into chats API

> Implemented by Coder Agents, reviewed and tested by a human.
This commit is contained in:
Cian Johnston
2026-07-16 15:21:57 +01:00
committed by GitHub
parent 9862f10484
commit f7481c5d08
26 changed files with 1756 additions and 35 deletions
+37 -3
View File
@@ -783,6 +783,18 @@ BEGIN
END;
$$;
CREATE FUNCTION chat_message_search_text(content jsonb) RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE
AS $$
SELECT CASE WHEN jsonb_typeof(content) = 'array' THEN (
SELECT string_agg(part->>'text', ' ' ORDER BY ordinality)
FROM jsonb_array_elements(content) WITH ORDINALITY AS t(part, ordinality)
WHERE part->>'type' = 'text'
) END
$$;
COMMENT ON FUNCTION chat_message_search_text(content jsonb) IS 'Extracts searchable content from chat_messages. Returns NULL for scalar JSON strings (content_version=0). Immutable as it is used in indexes.';
CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -1365,6 +1377,7 @@ CREATE FUNCTION set_chat_message_revision_before() RETURNS trigger
AS $$
DECLARE
chat_snapshot_version bigint;
cmp chat_messages;
BEGIN
IF TG_OP = 'INSERT' AND NEW.revision IS NOT NULL THEN
RAISE EXCEPTION 'chat_messages.revision must be assigned by trigger';
@@ -1379,7 +1392,9 @@ BEGIN
RAISE EXCEPTION 'chat_messages.revision must be assigned by trigger';
END IF;
IF OLD IS NOT DISTINCT FROM NEW THEN
cmp := NEW;
cmp.search_tsv := OLD.search_tsv;
IF OLD IS NOT DISTINCT FROM cmp THEN
RETURN NEW;
END IF;
END IF;
@@ -1396,6 +1411,8 @@ BEGIN
END;
$$;
COMMENT ON FUNCTION set_chat_message_revision_before() IS 'Component of chatd. Updates chat_snapshot_version when any fields of chat_messages change. Excludes changes to search_tsv as it is not relevant to chatd''s processing loop.';
CREATE FUNCTION sync_chat_retry_state() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -1446,7 +1463,7 @@ BEGIN
SELECT DISTINCT n.chat_id
FROM chat_message_history_new_rows n
JOIN chat_message_history_old_rows o ON o.id = n.id
WHERE o IS DISTINCT FROM n
WHERE (to_jsonb(o) - 'search_tsv') IS DISTINCT FROM (to_jsonb(n) - 'search_tsv')
) AS affected
WHERE c.id = affected.chat_id
AND (
@@ -1457,6 +1474,8 @@ BEGIN
END;
$$;
COMMENT ON FUNCTION update_chat_history_after_message_update() IS 'Component of chatd. Updates history_version and generation_attempt on chats when chat_messages is updated. Excludes changes to search_tsv.';
CREATE TABLE ai_gateway_keys (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -1946,11 +1965,14 @@ CREATE TABLE chat_messages (
provider_response_id text,
api_key_id text,
revision bigint NOT NULL,
reasoning_effort chat_reasoning_effort
reasoning_effort chat_reasoning_effort,
search_tsv tsvector
);
COMMENT ON COLUMN chat_messages.reasoning_effort IS 'Stores the selected effort for the turn triggered by this message.';
COMMENT ON COLUMN chat_messages.search_tsv IS 'Used for full text search. NULL initially, populated async via background job.';
CREATE SEQUENCE chat_messages_id_seq
START WITH 1
INCREMENT BY 1
@@ -4717,6 +4739,8 @@ CREATE UNIQUE INDEX idx_chat_debug_steps_run_step ON chat_debug_steps USING btre
CREATE INDEX idx_chat_debug_steps_stale ON chat_debug_steps USING btree (updated_at) WHERE (finished_at IS NULL);
CREATE INDEX idx_chat_diff_statuses_pr_title_fts ON chat_diff_statuses USING gin (to_tsvector('simple'::regconfig, pull_request_title));
CREATE INDEX idx_chat_diff_statuses_stale_at ON chat_diff_statuses USING btree (stale_at);
CREATE INDEX idx_chat_diff_statuses_url_lower ON chat_diff_statuses USING btree (lower(url)) WHERE ((url IS NOT NULL) AND (url <> ''::text));
@@ -4737,6 +4761,12 @@ CREATE INDEX idx_chat_messages_created_at ON chat_messages USING btree (created_
CREATE INDEX idx_chat_messages_owner_spend ON chat_messages USING btree (chat_id, created_at) WHERE (total_cost_micros IS NOT NULL);
CREATE INDEX idx_chat_messages_search_tsv ON chat_messages USING gin (search_tsv) WHERE ((search_tsv IS NOT NULL) AND (deleted = false) AND (visibility = ANY (ARRAY['user'::chat_message_visibility, 'both'::chat_message_visibility])) AND (role = ANY (ARRAY['user'::chat_message_role, 'assistant'::chat_message_role])));
COMMENT ON INDEX idx_chat_messages_search_tsv IS 'Partial index over chat_messages used for populating search_tsv in the background. Only defined over ''searchable'' rows of chat_messages where search_tsv is NULL.';
CREATE INDEX idx_chat_messages_search_tsv_pending ON chat_messages USING btree (id DESC) WHERE ((search_tsv IS NULL) AND (deleted = false) AND (visibility = ANY (ARRAY['user'::chat_message_visibility, 'both'::chat_message_visibility])) AND (role = ANY (ARRAY['user'::chat_message_role, 'assistant'::chat_message_role])));
CREATE INDEX idx_chat_messages_user_prompts ON chat_messages USING btree (chat_id, id DESC) WHERE ((deleted = false) AND (role = 'user'::chat_message_role) AND (visibility = ANY (ARRAY['user'::chat_message_visibility, 'both'::chat_message_visibility])));
CREATE INDEX idx_chat_model_configs_ai_provider_id ON chat_model_configs USING btree (ai_provider_id);
@@ -4763,6 +4793,10 @@ CREATE INDEX idx_chats_parent_chat_id ON chats USING btree (parent_chat_id);
CREATE INDEX idx_chats_root_chat_id ON chats USING btree (root_chat_id);
CREATE INDEX idx_chats_title_fts ON chats USING gin (to_tsvector('simple'::regconfig, title));
COMMENT ON INDEX idx_chats_title_fts IS 'Used for full text search. Defined over all rows of the chats table.';
CREATE INDEX idx_chats_worker_acquisition_candidates ON chats USING btree (status, updated_at, id) WHERE (archived = false);
CREATE INDEX idx_chats_workspace ON chats USING btree (workspace_id);