Edge Functions

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:

  1. A generate-embedding database webhook edge function which generates embeddings when a content row is added (or updated) in the public.embeddings table.
  2. A query_embeddings Postgres function which allows us to perform similarity search from an egde function via Remote Procedure Call (RPC).
  3. 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:


_10
create extension if not exists vector with schema extensions;
_10
_10
create table embeddings (
_10
id bigint primary key generated always as identity,
_10
content text not null,
_10
embedding vector (384)
_10
);
_10
alter table embeddings enable row level security;
_10
_10
create 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:


_21
const model = new Supabase.ai.Session('gte-small')
_21
_21
Deno.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
_24
create or replace function query_embeddings(embedding vector(384), match_threshold float)
_24
returns setof embeddings
_24
language plpgsql
_24
as $$
_24
begin
_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;
_24
end;
_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:


_25
const model = new Supabase.ai.Session('gte-small')
_25
_25
Deno.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!