128 lines
4.7 KiB
PL/PgSQL
128 lines
4.7 KiB
PL/PgSQL
-- ── Migration 006: Full-Text Search ──────────────────────────────────────────
|
|
-- Adds tsvector generated columns and GIN indexes to artboards and origins so
|
|
-- the natural-language cross-artboard query feature (Layer 10 / Phase 3) can do
|
|
-- weighted full-text search without re-scanning jsonb at query time.
|
|
--
|
|
-- search_artboards(query, workspace_id) is the primary search entry point.
|
|
-- It returns artboard rows ranked by text relevance using ts_rank_cd.
|
|
|
|
-- ── 1. Generated tsvector column on artboards ─────────────────────────────────
|
|
-- Covers: artboard name (weight A), route / renderUrl from metadata (weight B),
|
|
-- and the raw metadata_jsonb text (weight C).
|
|
|
|
ALTER TABLE artboards
|
|
ADD COLUMN IF NOT EXISTS search_vector tsvector
|
|
GENERATED ALWAYS AS (
|
|
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
|
|
setweight(to_tsvector('english', coalesce(metadata_jsonb::text, '')), 'C')
|
|
) STORED;
|
|
|
|
CREATE INDEX IF NOT EXISTS artboards_search_vector_idx
|
|
ON artboards USING GIN (search_vector);
|
|
|
|
-- ── 2. Generated tsvector column on origins ───────────────────────────────────
|
|
-- Covers: source_ref (weight A), source_metadata_jsonb text (weight B).
|
|
|
|
ALTER TABLE origins
|
|
ADD COLUMN IF NOT EXISTS search_vector tsvector
|
|
GENERATED ALWAYS AS (
|
|
setweight(to_tsvector('english', coalesce(source_ref, '')), 'A') ||
|
|
setweight(to_tsvector('english', coalesce(source_metadata_jsonb::text, '')), 'B')
|
|
) STORED;
|
|
|
|
CREATE INDEX IF NOT EXISTS origins_search_vector_idx
|
|
ON origins USING GIN (search_vector);
|
|
|
|
-- ── 3. Generated tsvector on intent_diffs ────────────────────────────────────
|
|
-- Covers: summary (weight A), changes_jsonb text (weight C).
|
|
-- Allows searching "show me every diff about the navigation bar" style queries.
|
|
|
|
ALTER TABLE intent_diffs
|
|
ADD COLUMN IF NOT EXISTS search_vector tsvector
|
|
GENERATED ALWAYS AS (
|
|
setweight(to_tsvector('english', coalesce(summary, '')), 'A') ||
|
|
setweight(to_tsvector('english', coalesce(changes_jsonb::text, '')), 'C')
|
|
) STORED;
|
|
|
|
CREATE INDEX IF NOT EXISTS intent_diffs_search_vector_idx
|
|
ON intent_diffs USING GIN (search_vector);
|
|
|
|
-- ── 4. search_artboards RPC ───────────────────────────────────────────────────
|
|
-- Parameters:
|
|
-- p_workspace_id — scope results to a single workspace
|
|
-- p_query — plain-text search string (converted to tsquery internally)
|
|
-- p_limit — max rows to return (default 20)
|
|
--
|
|
-- Returns artboards ranked by text relevance. Joins to origins and intent_diffs
|
|
-- to boost matches found in provenance or diff history.
|
|
--
|
|
-- Plain-to-tsquery converts arbitrary user text into a safe tsquery, allowing
|
|
-- partial words and multi-word phrases without syntax errors.
|
|
|
|
CREATE OR REPLACE FUNCTION search_artboards(
|
|
p_workspace_id UUID,
|
|
p_query TEXT,
|
|
p_limit INTEGER DEFAULT 20
|
|
)
|
|
RETURNS TABLE (
|
|
id UUID,
|
|
workspace_id UUID,
|
|
project_id UUID,
|
|
name TEXT,
|
|
origin_id UUID,
|
|
parent_artboard_id UUID,
|
|
metadata_jsonb JSONB,
|
|
created_at TIMESTAMPTZ,
|
|
updated_at TIMESTAMPTZ,
|
|
rank FLOAT4
|
|
)
|
|
LANGUAGE SQL STABLE AS $$
|
|
WITH q AS (
|
|
SELECT plainto_tsquery('english', p_query) AS tsq
|
|
)
|
|
SELECT
|
|
a.id,
|
|
a.workspace_id,
|
|
a.project_id,
|
|
a.name,
|
|
a.origin_id,
|
|
a.parent_artboard_id,
|
|
a.metadata_jsonb,
|
|
a.created_at,
|
|
a.updated_at,
|
|
-- Combine artboard rank with a bonus for matching origins or diffs
|
|
(
|
|
ts_rank_cd(a.search_vector, q.tsq, 32) * 1.5 +
|
|
coalesce((
|
|
SELECT max(ts_rank_cd(d.search_vector, q.tsq, 32))
|
|
FROM intent_diffs d
|
|
WHERE d.artboard_id = a.id
|
|
AND d.search_vector @@ q.tsq
|
|
), 0) +
|
|
coalesce((
|
|
SELECT ts_rank_cd(o.search_vector, q.tsq, 32)
|
|
FROM origins o
|
|
WHERE o.id = a.origin_id
|
|
AND o.search_vector @@ q.tsq
|
|
), 0)
|
|
) AS rank
|
|
FROM artboards a, q
|
|
WHERE
|
|
a.workspace_id = p_workspace_id
|
|
AND (
|
|
a.search_vector @@ q.tsq
|
|
OR EXISTS (
|
|
SELECT 1 FROM intent_diffs d
|
|
WHERE d.artboard_id = a.id
|
|
AND d.search_vector @@ q.tsq
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1 FROM origins o
|
|
WHERE o.id = a.origin_id
|
|
AND o.search_vector @@ q.tsq
|
|
)
|
|
)
|
|
ORDER BY rank DESC, a.updated_at DESC
|
|
LIMIT p_limit;
|
|
$$;
|