Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Do Full-Text Search in Supabase

Supabase supports full-text search through PostgreSQL's built-in tsvector and tsquery system. Add a tsvector column to your table, create a GIN index for fast lookups, and use the textSearch filter in the Supabase JS client to search across text columns. For multi-column search, combine multiple columns into a single tsvector using concatenation. PostgreSQL handles stemming, stop words, and ranking automatically.

What you'll learn

  • How to create tsvector columns for full-text search indexing
  • How to build GIN indexes for fast full-text search performance
  • How to use the Supabase JS client's textSearch filter
  • How to search across multiple columns and rank results by relevance
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minSupabase (all plans), @supabase/supabase-js v2+, PostgreSQL 14+March 2026RapidDev Engineering Team
TL;DR

Supabase supports full-text search through PostgreSQL's built-in tsvector and tsquery system. Add a tsvector column to your table, create a GIN index for fast lookups, and use the textSearch filter in the Supabase JS client to search across text columns. For multi-column search, combine multiple columns into a single tsvector using concatenation. PostgreSQL handles stemming, stop words, and ranking automatically.

Implementing Full-Text Search in Supabase with tsvector and GIN Indexes

PostgreSQL includes a powerful full-text search engine that supports stemming, stop word removal, and relevance ranking. Supabase exposes this through the textSearch filter in the JS client and the to_tsvector/to_tsquery SQL functions. This tutorial shows you how to add full-text search to your tables, index them for performance, and query them from your frontend. No external search service is needed.

Prerequisites

  • A Supabase project (free tier works)
  • A table with text columns you want to search
  • Access to the SQL Editor in the Supabase Dashboard
  • @supabase/supabase-js v2 installed in your project

Step-by-step guide

1

Add a tsvector column to your table

A tsvector column stores pre-processed text tokens optimized for search. Add a generated tsvector column that automatically updates when the source text columns change. Use the to_tsvector function with a language configuration (typically 'english') to process the text into searchable tokens. A generated column means you never need to update it manually — it always reflects the current data.

typescript
1-- Example: articles table with full-text search
2create table public.articles (
3 id bigint generated always as identity primary key,
4 title text not null,
5 body text not null,
6 author_id uuid references auth.users(id),
7 created_at timestamptz default now(),
8 -- Generated tsvector column combining title and body
9 fts tsvector generated always as (
10 setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
11 setweight(to_tsvector('english', coalesce(body, '')), 'B')
12 ) stored
13);
14
15alter table public.articles enable row level security;
16
17create policy "Public read articles" on public.articles
18 for select to anon, authenticated
19 using (true);

Expected result: The articles table has an fts column that automatically generates a tsvector from the title and body whenever a row is inserted or updated.

2

Create a GIN index for fast full-text search

Without an index, PostgreSQL performs a sequential scan on every search query, which is slow for large tables. A GIN (Generalized Inverted Index) index on the tsvector column dramatically speeds up full-text search by creating an inverted index of all tokens. This is the standard index type for full-text search in PostgreSQL.

typescript
1-- Create a GIN index on the tsvector column
2create index idx_articles_fts on public.articles using gin (fts);
3
4-- Verify the index was created
5select indexname, indexdef
6from pg_indexes
7where tablename = 'articles';

Expected result: The GIN index is created. Full-text search queries now use the index instead of scanning the entire table.

3

Query with the Supabase JS client's textSearch filter

The Supabase JS client provides a textSearch filter that translates to PostgreSQL's @@ operator. Pass the column name (the tsvector column) and the search query. The client automatically converts your search string into a tsquery. You can specify the query type: plain (default, AND between words), phrase (exact phrase match), or websearch (Google-style syntax with quotes, OR, and minus for exclusion).

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Basic search: all words must match
9const { data: results } = await supabase
10 .from('articles')
11 .select('id, title, body, created_at')
12 .textSearch('fts', 'supabase database')
13
14// Phrase search: exact phrase match
15const { data: phrase } = await supabase
16 .from('articles')
17 .select('id, title')
18 .textSearch('fts', 'row level security', { type: 'phrase' })
19
20// Websearch: Google-style syntax
21const { data: web } = await supabase
22 .from('articles')
23 .select('id, title')
24 .textSearch('fts', '"edge functions" OR "serverless" -lambda', {
25 type: 'websearch'
26 })

Expected result: Each query returns an array of articles matching the search criteria. Only rows where the tsvector matches the tsquery are returned.

4

Rank search results by relevance

By default, textSearch returns results in the table's natural order, not ranked by relevance. To rank results, create a database function that uses ts_rank or ts_rank_cd to score each row based on how well it matches the query. Call this function via supabase.rpc() and order by the rank score. The weights assigned to the tsvector columns (A, B, C, D) affect the ranking.

typescript
1-- Create a search function with ranking
2create or replace function search_articles(search_query text)
3returns table (
4 id bigint,
5 title text,
6 body text,
7 rank real
8)
9language sql
10as $$
11 select
12 a.id,
13 a.title,
14 a.body,
15 ts_rank(a.fts, websearch_to_tsquery('english', search_query)) as rank
16 from public.articles a
17 where a.fts @@ websearch_to_tsquery('english', search_query)
18 order by rank desc
19 limit 20;
20$$;
21
22-- Call from the JS client
23const { data } = await supabase.rpc('search_articles', {
24 search_query: 'supabase edge functions'
25})

Expected result: Results are returned ordered by relevance rank, with articles that match in the title scoring higher than articles that only match in the body.

5

Add search highlighting with ts_headline

Use the ts_headline function to generate highlighted search snippets that show the matched terms in context. This is useful for displaying search results with bolded keywords, similar to how Google highlights search terms in its results. ts_headline takes the original text, the query, and formatting options.

typescript
1-- Search function with highlighted snippets
2create or replace function search_articles_with_highlights(search_query text)
3returns table (
4 id bigint,
5 title text,
6 snippet text,
7 rank real
8)
9language sql
10as $$
11 select
12 a.id,
13 a.title,
14 ts_headline(
15 'english',
16 a.body,
17 websearch_to_tsquery('english', search_query),
18 'StartSel=<mark>, StopSel=</mark>, MaxWords=35, MinWords=15'
19 ) as snippet,
20 ts_rank(a.fts, websearch_to_tsquery('english', search_query)) as rank
21 from public.articles a
22 where a.fts @@ websearch_to_tsquery('english', search_query)
23 order by rank desc
24 limit 20;
25$$;
26
27const { data } = await supabase.rpc('search_articles_with_highlights', {
28 search_query: 'database security'
29})
30// data[0].snippet: "...configure <mark>database</mark> <mark>security</mark> policies..."

Expected result: Each search result includes a snippet with matched terms wrapped in <mark> tags, ready for display in a search results UI.

Complete working example

full-text-search.sql
1-- Articles table with full-text search
2create table public.articles (
3 id bigint generated always as identity primary key,
4 title text not null,
5 body text not null,
6 author_id uuid references auth.users(id),
7 created_at timestamptz default now(),
8 fts tsvector generated always as (
9 setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
10 setweight(to_tsvector('english', coalesce(body, '')), 'B')
11 ) stored
12);
13
14alter table public.articles enable row level security;
15create policy "Public read articles" on public.articles
16 for select to anon, authenticated using (true);
17create policy "Auth insert articles" on public.articles
18 for insert to authenticated with check (true);
19
20-- GIN index for fast full-text search
21create index idx_articles_fts on public.articles using gin (fts);
22
23-- Ranked search function
24create or replace function search_articles(search_query text)
25returns table (id bigint, title text, body text, rank real)
26language sql as $$
27 select a.id, a.title, a.body,
28 ts_rank(a.fts, websearch_to_tsquery('english', search_query)) as rank
29 from public.articles a
30 where a.fts @@ websearch_to_tsquery('english', search_query)
31 order by rank desc
32 limit 20;
33$$;
34
35-- Search with highlighted snippets
36create or replace function search_articles_with_highlights(search_query text)
37returns table (id bigint, title text, snippet text, rank real)
38language sql as $$
39 select a.id, a.title,
40 ts_headline('english', a.body,
41 websearch_to_tsquery('english', search_query),
42 'StartSel=<mark>, StopSel=</mark>, MaxWords=35, MinWords=15'
43 ) as snippet,
44 ts_rank(a.fts, websearch_to_tsquery('english', search_query)) as rank
45 from public.articles a
46 where a.fts @@ websearch_to_tsquery('english', search_query)
47 order by rank desc
48 limit 20;
49$$;

Common mistakes when doing Full-Text Search in Supabase

Why it's a problem: Not creating a GIN index on the tsvector column, causing full-text search to be slow on large tables

How to avoid: Always create a GIN index: CREATE INDEX idx_name ON table USING gin (fts_column). Without it, PostgreSQL scans every row.

Why it's a problem: Searching on the raw text column instead of the tsvector column with textSearch

How to avoid: Use textSearch on the tsvector column (e.g., 'fts'), not on the raw text columns (e.g., 'title' or 'body'). The tsvector column is what the search engine reads.

Why it's a problem: Using ilike for search instead of full-text search, missing stemming and ranking capabilities

How to avoid: Use textSearch for proper full-text search. ilike does simple pattern matching without stemming ('running' would not match 'run') and cannot rank results.

Why it's a problem: Forgetting to wrap text columns in coalesce() when building the tsvector, causing errors on NULL values

How to avoid: Always use coalesce(column, '') when building tsvector from nullable columns. to_tsvector fails on NULL input.

Best practices

  • Use generated tsvector columns so the search index updates automatically when source columns change
  • Create a GIN index on every tsvector column for fast search performance
  • Assign weights with setweight to prioritize title matches over body matches in relevance ranking
  • Use websearch_to_tsquery for user-facing search boxes — it supports Google-style syntax naturally
  • Create database functions with ts_rank for relevance-ordered results and call them via supabase.rpc()
  • Use ts_headline to generate highlighted snippets for search result display
  • Wrap nullable columns in coalesce() when building tsvector to prevent NULL errors
  • For multilingual content, store the language configuration per row and use it dynamically in the tsvector

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I want to add full-text search to my Supabase articles table. Show me how to create a tsvector column combining title and body, add a GIN index, query with the JS client's textSearch filter, and create a database function that returns results ranked by relevance with highlighted snippets.

Supabase Prompt

Create a Supabase articles table with a generated tsvector column combining title (weight A) and body (weight B), a GIN index, and two database functions: one for ranked search and one for search with ts_headline highlighted snippets. Show the JS client calls for textSearch and rpc.

Frequently asked questions

Does Supabase full-text search handle different word forms like 'running' and 'run'?

Yes. PostgreSQL's full-text search includes stemming, which reduces words to their root form. A search for 'running' matches documents containing 'run', 'runs', 'running', and 'ran' when using the English language configuration.

Can I search across multiple tables at once?

Not directly with a single query. Create a database function that searches multiple tables and returns a unified result set using UNION ALL, or create a materialized view that combines the data.

What is the difference between textSearch types: plain, phrase, and websearch?

Plain requires all words to be present (AND logic). Phrase requires words to appear in exact order. Websearch supports Google-style syntax with double quotes for exact phrases, OR for alternatives, and dash prefix for exclusion.

Do I need an external search service like Algolia or Typesense?

For most applications, PostgreSQL's built-in full-text search is sufficient. External search services offer features like typo tolerance, faceted search, and geo-search that PostgreSQL does not provide natively.

How do I handle multilingual content in full-text search?

Store the language configuration per row (e.g., a 'language' column) and use it dynamically: to_tsvector(language_column, text_column). PostgreSQL supports many language configurations out of the box.

Can I combine full-text search with other Supabase filters?

Yes. Chain textSearch with other filters like eq, gt, or order: supabase.from('articles').select('*').textSearch('fts', 'query').eq('status', 'published').order('created_at', { ascending: false }).

Can RapidDev help implement full-text search in my Supabase project?

Yes. RapidDev can design your search schema, create weighted tsvector columns and GIN indexes, build ranked search functions, and implement search UIs with highlighted results.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.