/* =========================================================================== RAG upgrade migration for public.rag_user_documents Run this in the Supabase SQL editor (or via `supabase db` / apply_migration). Adds: - pgvector embedding column (vector(768), BAAI/bge-base-en-v1.5) - metadata columns: source_type, url, created_at, chunk_type - full-text search column + GIN index (hybrid search) - HNSW index on the embedding (fast ANN) - match_rag_documents() RPC: vector + keyword search fused with RRF, with a similarity threshold and metadata filters. NOTE: the embedding column is migrated from double precision[] to vector(768). Existing rows must already hold 768-length vectors (they do, for bge-base-en-v1.5). Any rows produced with a different-dimension model must be re-embedded (re-run /chunk_pdf or /chunk_url) after this migration. =========================================================================== */ -- pgvector is created `with schema extensions` in this project; Supabase keeps -- `extensions` on the search_path, so `vector`, `<=>` and `vector_cosine_ops` -- resolve unqualified. create extension if not exists vector with schema extensions; -- --------------------------------------------------------------------------- -- 1) Schema changes -- --------------------------------------------------------------------------- -- Embedding: double precision[] -> vector(768) alter table public.rag_user_documents alter column embedding type vector(768) using (embedding::text::vector(768)); -- Metadata columns alter table public.rag_user_documents add column if not exists source_type text, add column if not exists url text, add column if not exists created_at timestamptz not null default now(), add column if not exists chunk_type text; -- Full-text search column (generated from content) alter table public.rag_user_documents add column if not exists fts tsvector generated always as (to_tsvector('english', coalesce(content, ''))) stored; -- --------------------------------------------------------------------------- -- 2) Indexes -- --------------------------------------------------------------------------- create index if not exists ix_rag_user_documents_embedding on public.rag_user_documents using hnsw (embedding vector_cosine_ops); create index if not exists ix_rag_user_documents_fts on public.rag_user_documents using gin (fts); create index if not exists ix_rag_user_documents_user_id on public.rag_user_documents using btree (user_id); create index if not exists ix_rag_user_documents_source_type on public.rag_user_documents using btree (source_type); create index if not exists ix_rag_user_documents_created_at on public.rag_user_documents using btree (created_at); -- --------------------------------------------------------------------------- -- 3) Hybrid retrieval RPC (vector + full-text, fused with Reciprocal Rank Fusion) -- --------------------------------------------------------------------------- create or replace function public.match_rag_documents( query_embedding vector(768), query_text text, p_user_id uuid, match_threshold float default 0.3, match_count int default 50, filter_source_type text default null, filter_url text default null, created_after timestamptz default null, rrf_k int default 60 ) returns table ( id uuid, content text, source_type text, url text, created_at timestamptz, doc_id uuid, chunk_type text, similarity float, rrf_score float ) language sql stable security definer set search_path = public, extensions as $$ with base as ( select d.* from public.rag_user_documents d where (p_user_id is null or d.user_id = p_user_id) and (filter_source_type is null or d.source_type = filter_source_type) and (filter_url is null or d.url ilike '%' || filter_url || '%') and (created_after is null or d.created_at >= created_after) ), -- Semantic half: only vectors above the similarity threshold vector_search as ( select base.id, 1 - (base.embedding <=> query_embedding) as similarity, row_number() over (order by base.embedding <=> query_embedding) as rank from base where 1 - (base.embedding <=> query_embedding) >= match_threshold order by base.embedding <=> query_embedding limit match_count ), -- Keyword half: full-text matches (independent of the vector threshold) keyword_search as ( select base.id, row_number() over ( order by ts_rank_cd(base.fts, websearch_to_tsquery('english', query_text)) desc ) as rank from base where query_text is not null and query_text <> '' and base.fts @@ websearch_to_tsquery('english', query_text) order by ts_rank_cd(base.fts, websearch_to_tsquery('english', query_text)) desc limit match_count ), fused as ( select coalesce(v.id, k.id) as id, coalesce(1.0 / (rrf_k + v.rank), 0.0) + coalesce(1.0 / (rrf_k + k.rank), 0.0) as rrf_score, v.similarity as similarity from vector_search v full outer join keyword_search k on v.id = k.id ) select b.id, b.content, b.source_type, b.url, b.created_at, b.doc_id, b.chunk_type, -- when a row is keyword-only, recompute similarity for display coalesce(f.similarity, 1 - (b.embedding <=> query_embedding)) as similarity, f.rrf_score from fused f join public.rag_user_documents b on b.id = f.id order by f.rrf_score desc limit match_count; $$;