diff --git a/.github/workflows/api-ci.yml b/.github/workflows/api-ci.yml index 5394698..1b49ad6 100644 --- a/.github/workflows/api-ci.yml +++ b/.github/workflows/api-ci.yml @@ -9,6 +9,7 @@ on: - main paths: - 'api/**' + - 'db/**' - '.github/workflows/api-ci.yml' pull_request: branches: @@ -17,6 +18,7 @@ on: - main paths: - 'api/**' + - 'db/**' - '.github/workflows/api-ci.yml' jobs: diff --git a/db/init/02_tables_catalog.sql b/db/init/02_tables_catalog.sql index a44355f..3098cd3 100644 --- a/db/init/02_tables_catalog.sql +++ b/db/init/02_tables_catalog.sql @@ -9,7 +9,8 @@ CREATE TABLE catalog ( title TEXT, description TEXT, created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now() + updated_at TIMESTAMP DEFAULT now(), + search_vector tsvector ); -- Catalog links table: Stores related links for catalogs (e.g., self, root, child, item links) @@ -44,3 +45,64 @@ CREATE TABLE crawllog_catalog ( catalog_id INTEGER REFERENCES catalog(id) ON DELETE CASCADE, last_crawled TIMESTAMP ); + +-- ======================================== +-- FULL-TEXT SEARCH TRIGGERS +-- ======================================== + +-- Trigger function to auto-update search_vector when catalog is inserted or updated +-- Includes title, description, and all associated keywords for comprehensive search +CREATE OR REPLACE FUNCTION update_catalog_search_vector() +RETURNS TRIGGER AS $$ +BEGIN + NEW.search_vector := to_tsvector('simple', + coalesce(NEW.title, '') || ' ' || + coalesce(NEW.description, '') || ' ' || + coalesce( + ( + SELECT string_agg(k.keyword, ' ') + FROM catalog_keywords ck + JOIN keywords k ON k.id = ck.keyword_id + WHERE ck.catalog_id = NEW.id + ), + '' + ) + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER catalog_search_vector_update +BEFORE INSERT OR UPDATE ON catalog +FOR EACH ROW +EXECUTE FUNCTION update_catalog_search_vector(); + +-- Trigger function to update search_vector when keywords are added/removed +-- Ensures search index stays in sync with keyword changes +CREATE OR REPLACE FUNCTION update_catalog_search_vector_on_keyword_change() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE catalog + SET search_vector = to_tsvector('simple', + coalesce(title, '') || ' ' || + coalesce(description, '') || ' ' || + coalesce( + ( + SELECT string_agg(k.keyword, ' ') + FROM catalog_keywords ck + JOIN keywords k ON k.id = ck.keyword_id + WHERE ck.catalog_id = catalog.id + ), + '' + ) + ) + WHERE id = COALESCE(NEW.catalog_id, OLD.catalog_id); + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER catalog_keywords_update_vector +AFTER INSERT OR DELETE ON catalog_keywords +FOR EACH ROW +EXECUTE FUNCTION update_catalog_search_vector_on_keyword_change(); diff --git a/db/init/03_tables_collections.sql b/db/init/03_tables_collections.sql index 733bb08..1f72ab7 100644 --- a/db/init/03_tables_collections.sql +++ b/db/init/03_tables_collections.sql @@ -21,7 +21,8 @@ CREATE TABLE collection ( is_api BOOLEAN DEFAULT FALSE, is_active BOOLEAN DEFAULT TRUE, - full_json JSONB + full_json JSONB, + search_vector tsvector ); -- Collection summaries: Stores summaries for collection properties @@ -64,3 +65,64 @@ CREATE TABLE crawllog_collection ( collection_id INTEGER REFERENCES collection(id) ON DELETE CASCADE, last_crawled TIMESTAMP ); + +-- ======================================== +-- FULL-TEXT SEARCH TRIGGERS +-- ======================================== + +-- Trigger function to auto-update search_vector when collection is inserted or updated +-- Includes title, description, and all associated keywords for comprehensive search +CREATE OR REPLACE FUNCTION update_collection_search_vector() +RETURNS TRIGGER AS $$ +BEGIN + NEW.search_vector := to_tsvector('simple', + coalesce(NEW.title, '') || ' ' || + coalesce(NEW.description, '') || ' ' || + coalesce( + ( + SELECT string_agg(k.keyword, ' ') + FROM collection_keywords ck + JOIN keywords k ON k.id = ck.keyword_id + WHERE ck.collection_id = NEW.id + ), + '' + ) + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER collection_search_vector_update +BEFORE INSERT OR UPDATE ON collection +FOR EACH ROW +EXECUTE FUNCTION update_collection_search_vector(); + +-- Trigger function to update search_vector when keywords are added/removed +-- Ensures search index stays in sync with keyword changes +CREATE OR REPLACE FUNCTION update_collection_search_vector_on_keyword_change() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE collection + SET search_vector = to_tsvector('simple', + coalesce(title, '') || ' ' || + coalesce(description, '') || ' ' || + coalesce( + ( + SELECT string_agg(k.keyword, ' ') + FROM collection_keywords ck + JOIN keywords k ON k.id = ck.keyword_id + WHERE ck.collection_id = collection.id + ), + '' + ) + ) + WHERE id = COALESCE(NEW.collection_id, OLD.collection_id); + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER collection_keywords_update_vector +AFTER INSERT OR DELETE ON collection_keywords +FOR EACH ROW +EXECUTE FUNCTION update_collection_search_vector_on_keyword_change(); diff --git a/db/init/05_indexes.sql b/db/init/05_indexes.sql index 9349b88..7c0d74a 100644 --- a/db/init/05_indexes.sql +++ b/db/init/05_indexes.sql @@ -9,8 +9,8 @@ CREATE INDEX idx_catalog_title ON catalog (title); CREATE INDEX idx_catalog_updated_at ON catalog (updated_at); -CREATE INDEX idx_catalog_fulltext ON catalog -USING GIN (to_tsvector('simple', coalesce(title,'') || ' ' || coalesce(description,''))); +-- Full-text search index on computed search_vector column (includes title, description, and keywords) +CREATE INDEX idx_catalog_search_vector ON catalog USING GIN (search_vector); CREATE INDEX idx_catalog_links_catalog_id ON catalog_links (catalog_id); CREATE INDEX idx_catalog_keywords_catalog ON catalog_keywords (catalog_id); @@ -30,8 +30,8 @@ CREATE INDEX idx_collection_active ON collection (is_active); CREATE INDEX idx_collection_spatial ON collection USING GIST (spatial_extent); -CREATE INDEX idx_collection_fulltext ON collection -USING GIN (to_tsvector('simple', coalesce(title,'') || ' ' || coalesce(description,''))); +-- Full-text search index on computed search_vector column (includes title, description, and keywords) +CREATE INDEX idx_collection_search_vector ON collection USING GIN (search_vector); CREATE INDEX idx_collection_jsonb ON collection USING GIN (full_json);