Semantic Search
Semantic Search with pgvector and Supabase Edge Functions
Semantic search interprets the meaning behind user queries rather than exact keywords. It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships.
Since Supabase Edge Runtime v1.36.0 you can run the gte-small
model natively within Supabase Edge Functions without any external dependencies! This allows you to easily generate text embeddings without calling any external APIs!
In this tutorial you're implementing three parts:
- A
generate-embedding
database webhook edge function which generates embeddings when a content row is added (or updated) in thepublic.embeddings
table. - A
query_embeddings
Postgres function which allows us to perform similarity search from an egde function via Remote Procedure Call (RPC). - A
search
edge function which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result.
You can find the complete example code on GitHub
Create the database table and webhook
Given the following table definition:
_10create extension if not exists vector with schema extensions;_10_10create table embeddings (_10 id bigint primary key generated always as identity,_10 content text not null,_10 embedding vector (384)_10);_10alter table embeddings enable row level security;_10_10create index on embeddings using hnsw (embedding vector_ip_ops);
You can deploy the following edge function as a database webhook to generate the embeddings for any text content inserted into the table:
_21const model = new Supabase.ai.Session('gte-small')_21_21Deno.serve(async (req) => {_21 const payload: WebhookPayload = await req.json()_21 const { content, id } = payload.record_21_21 // Generate embedding._21 const embedding = await model.run(content, {_21 mean_pool: true,_21 normalize: true,_21 })_21_21 // Store in database._21 const { error } = await supabase_21 .from('embeddings')_21 .update({ embedding: JSON.stringify(embedding) })_21 .eq('id', id)_21 if (error) console.warn(error.message)_21_21 return new Response('ok')_21})
Create a Postgres Function and RPC
With the embeddings now stored in your Postgres database table, you can query them from Supabase Edge Functions by utilizing Remote Procedure Calls (RPC).
Given the following Postgres Function:
_24-- Matches document sections using vector similarity search on embeddings_24--_24-- Returns a setof embeddings so that we can use PostgREST resource embeddings (joins with other tables)_24-- Additional filtering like limits can be chained to this function call_24create or replace function query_embeddings(embedding vector(384), match_threshold float)_24returns setof embeddings_24language plpgsql_24as $$_24begin_24 return query_24 select *_24 from embeddings_24_24 -- The inner product is negative, so we negate match_threshold_24 where embeddings.embedding <#> embedding < -match_threshold_24_24 -- Our embeddings are normalized to length 1, so cosine similarity_24 -- and inner product will produce the same query results._24 -- Using inner product which can be computed faster._24 --_24 -- For the different distance functions, see https://github.com/pgvector/pgvector_24 order by embeddings.embedding <#> embedding;_24end;_24$$;
Query vectors in Supabase Edge Functions
You can use supabase-js
to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your Supabase Edge Function:
_25const model = new Supabase.ai.Session('gte-small')_25_25Deno.serve(async (req) => {_25 const { search } = await req.json()_25 if (!search) return new Response('Please provide a search param!')_25 // Generate embedding for search term._25 const embedding = await model.run(search, {_25 mean_pool: true,_25 normalize: true,_25 })_25_25 // Query embeddings._25 const { data: result, error } = await supabase_25 .rpc('query_embeddings', {_25 embedding,_25 match_threshold: 0.8,_25 })_25 .select('content')_25 .limit(3)_25 if (error) {_25 return Response.json(error)_25 }_25_25 return Response.json({ search, result })_25})
That's it, you now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions!