Spaces:
Running
Running
| /* =========================================================================== | |
| 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; | |
| $$; | |