Supabase Edge Functions 原生 AI 推理:gte-small 文本向量与 pgvector 语义搜索实战
本文基于 Supabase 官方仓库 examples/ai/edge-functions 示例,讲解如何在 Supabase Edge Functions 中运行本地文本嵌入模型(gte-small),实现"数据变更自动向量化 + pgvector 相似度检索"的完整语义搜索链路。读完本文,你可以掌握 Supabase.ai 会话 API、数据库 Webhook 触发器、Postgres RPC 向量检索函数的用法,以及从本地调试到云端部署的完整操作路径。
1. 核心能力:在 Edge Runtime 中原生运行嵌入模型
自 Supabase Edge Runtime v1.36.0 起,可以在 Edge Functions 内直接运行 gte-small 文本嵌入模型,无需任何外部依赖、无需调用任何外部 Embedding API。这在 示例说明 中被明确描述:
Since Supabase Edge Runtime v1.36.0 you can run the
gte-smallmodel natively within Supabase Edge Functions without any external dependencies!
该模型输出 384 维向量,与示例中数据库列定义 vector (384) 严格对应(见 建表迁移)。两个 Edge Function 都通过同一行代码创建模型会话:
const model = new Supabase.ai.Session('gte-small')
这是 Edge Runtime 注入的全局对象,不需要 import 任何模型库。调用时使用统一的生成参数:
const embedding = await model.run(content, {
mean_pool: true, // 对 token 向量做平均池化
normalize: true, // 归一化到单位长度
})
normalize: true 不是可有可无的参数——它直接决定了后文向量检索函数能使用更高效的内积运算(见第 4 节)。
2. 演示架构:三个组件构成的闭环
该示例由三部分协同工作(引自 README):
| 组件 | 类型 | 职责 |
|---|---|---|
generate-embedding |
数据库 Webhook Edge Function | 当 public.embeddings 表发生 INSERT/UPDATE 时生成向量并回写 |
query_embeddings |
Postgres 函数 | 供 Edge Function 通过 RPC 执行相似度搜索 |
search |
Edge Function | 对外接口:将搜索词转向量,经 RPC 查询后返回最匹配的 3 条内容 |
数据流如下:
- 向
embeddings表插入/更新一行content; - 数据库触发器(
supabase_functions.http_request,基于pg_net扩展)发出 HTTP POST 到generate-embedding; generate-embedding用gte-small计算 384 维向量,通过supabaseAdmin客户端回写到该行的embedding列;- 客户端调用
search函数 → 对搜索词生成向量 → 调用query_embeddingsRPC → 返回按相似度排序的匹配内容。
3. 数据库层:向量表、索引与检索函数
3.1 embeddings 表
建表迁移 20240408072601_embeddings.sql 内容完整如下,注意两处关键细节:扩展安装在 extensions schema 下,以及 HNSW 索引配合 vector_ip_ops(内积操作符):
create extension if not exists pg_net with schema extensions;
create extension if not exists vector with schema extensions;
create table embeddings (
id bigint primary key generated always as identity,
content text not null,
embedding vector (384)
);
alter table embeddings enable row level security;
create index on embeddings using hnsw (embedding vector_ip_ops);
pg_net:Postgres 发起异步 HTTP 请求的扩展,是数据库 Webhook 触发的底层依赖;vector (384):pgvector 固定维度向量类型,与gte-small的输出维度一致;- HNSW 索引 +
vector_ip_ops:为内积距离建立近似最近邻索引,检索走索引而非全表扫描。
3.2 query_embeddings 检索函数
20240410031515_vector-search.sql 定义了一个 setof 返回的函数,函数头部的注释解释了设计动机:返回 setof embeddings 以便 PostgREST 将其当作资源使用(可与其他表 join、可在调用链上继续拼接过滤条件):
create or replace function query_embeddings(embedding vector(384), match_threshold float)
returns setof embeddings
language plpgsql
as $$
#variable_conflict use_variable
begin
return query
select *
from embeddings
-- The inner product is negative, so we negate match_threshold
where embeddings.embedding <#> embedding < -match_threshold
-- Our embeddings are normalized to length 1, so cosine similarity
-- and inner product will produce the same query results.
-- Using inner product which can be computed faster.
order by embeddings.embedding <#> embedding;
end;
$$;
实现细节值得逐条理解:
<#>是 pgvector 的负内积距离操作符(值越大表示越接近)。因为内积结果取负,过滤条件写作embedding <#> embedding < -match_threshold,即"匹配阈值以内";- 注释明确说明:由于生成端开启了
normalize: true,所有向量都是单位长度,此时内积与余弦相似度产生完全相同的结果排序,而内积计算更快,因此选用内积而非<->(余弦距离); match_threshold由调用方传入,示例中固定为0.8(见第 5 节);- 函数没有内置
LIMIT,返回行数由调用侧的 PostgREST.limit()控制。
4. Webhook 触发器:让数据库变更自动驱动向量化
20240410041607_database-webhook.sql 是部署时需要人工填写的一处,文件内包含两套模板:
-- 托管项目:把 <PROJECT-REF> 和 <SUPABASE_SECRET_KEY> 填入后取消注释,再执行 supabase db push
-- CREATE TRIGGER "on_inserted_or_updated_embedding"
-- AFTER INSERT
-- OR
-- UPDATE OF content ON public.embeddings FOR EACH ROW
-- EXECUTE FUNCTION supabase_functions.http_request (
-- 'https://<PROJECT-REF>.supabase.co/functions/v1/generate-embedding',
-- 'POST',
-- '{"Content-type":"application/json","apikey":"<SUPABASE_SECRET_KEY>"}',
-- '{}',
-- '5000'
-- );
以及一套仅用于本地测试的版本(文件注释标注"FOR LOCAL TESTING ONLY. REMOVE OR COMMENT OUT WHEN PUSHING TO HOSTED PROJECT!"),区别只在于目标地址改为本地下游网关:
-- 'http://kong:8000/functions/v1/generate-embedding',
supabase_functions.http_request 的参数依次为:目标 URL、HTTP 方法、请求头 JSON、body JSON、超时毫秒数(5000)。触发器只监听 INSERT 和 content 列的 UPDATE,且请求头携带 apikey: <SUPABASE_SECRET_KEY>——因为函数端关闭了 JWT 校验、改用 secret key 鉴权(见第 5 节)。
README 同时给出了一条 GUI 路径:在 Dashboard 的 Database Webhook 设置中选择 public.embeddings 表、勾选 INSERT 与 Update、请求头加入 secret key,效果等价于手工建触发器。
5. Edge Function 源码解析
5.1 generate-embedding:去重、推理、回写
完整源码见 generate-embedding/index.ts:
import { withSupabase } from 'npm:@supabase/server@^1'
import { Database, Tables } from '../_shared/database.types.ts'
type EmbeddingsRecord = Tables<'embeddings'>
interface WebhookPayload {
type: 'INSERT' | 'UPDATE' | 'DELETE'
table: string
record: EmbeddingsRecord
schema: 'public'
old_record: null | EmbeddingsRecord
}
const model = new Supabase.ai.Session('gte-small')
// Called with a secret key on the `apikey` header. Deploy with verify_jwt = false.
export default {
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
const payload: WebhookPayload = await req.json()
const { content, id } = payload.record
// Check if content has changed.
if (content === payload?.old_record?.content) {
return Response.json({ status: 'ok - no change' })
}
// Generate embedding
const embedding = await model.run(content, {
mean_pool: true,
normalize: true,
})
// Store in DB
const { error } = await ctx.supabaseAdmin
.from('embeddings')
.update({
embedding: JSON.stringify(embedding),
})
.eq('id', id)
if (error) {
console.warn(error.message)
return Response.json({ error: error.message }, { status: 500 })
}
return Response.json({ status: 'ok - updated' })
}),
}
源码中体现的关键设计:
withSupabase({ auth: 'secret' }, ...):来自@supabase/server的高阶封装,校验apikey头是否为项目的 secret key,并通过ctx.supabaseAdmin提供免 RLS 的管理员客户端。对应地,函数必须部署为verify_jwt = false(源码注释与 config.toml 中的配置一致);- 内容变更去重:UPDATE 触发时先比较
record.content与old_record.content,无变化直接返回ok - no change,避免重复推理浪费模型算力; - 回写:用
JSON.stringify(embedding)把 384 维数组序列化为文本存入embedding列,supabaseAdmin绕过 RLS 直接更新。
5.2 search:查询侧的端到端流程
完整源码见 search/index.ts:
const model = new Supabase.ai.Session('gte-small')
export default {
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
const { search } = await req.json()
if (!search) {
return Response.json({ error: 'Please provide a search param!' }, { status: 400 })
}
// Generate embedding for search term.
const embedding = await model.run(search, {
mean_pool: true,
normalize: true,
})
// Query embeddings.
const { data: result, error } = await ctx.supabaseAdmin
.rpc('query_embeddings', {
embedding: JSON.stringify(embedding),
match_threshold: 0.8,
})
.select('content')
.limit(3)
if (error) {
return Response.json(error)
}
return Response.json({ search, result })
}),
}
调用链:req.json() 解析 search 参数 → model.run 生成查询向量 → ctx.supabaseAdmin.rpc('query_embeddings', {...}) 通过 PostgREST 调用数据库函数,其中 match_threshold: 0.8 是相似度下限,.select('content') 利用第 3.2 节所述"setof 资源"特性只取内容列,.limit(3) 取最相近的 3 条 → 返回 { search, result }。
文件尾部还附了本地调试注释(原文保留):先 supabase start,再 supabase functions serve,然后:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/search' \
--header 'apikey: <SUPABASE_SECRET_KEY>' \
--header 'Content-Type: application/json' \
--data '{"search":"vehicles"}'
5.3 类型保障
两个函数共享 database.types.ts,其中 query_embeddings 的 RPC 签名(Args: { embedding: string; match_threshold: number },返回 embeddings 行数组)与 embeddings 表的 Row/Insert/Update 类型均为手写声明,使 withSupabase<Database>、Tables<'embeddings'> 等泛型在编译期获得完整类型检查。
6. 本地配置:config.toml 中的两个关键开关
config.toml 中与该演示直接相关的配置:
project_id = "ai-in-edge-functions"
[functions.generate-embedding]
enabled = true
verify_jwt = false
[functions.search]
enabled = true
verify_jwt = false
# Experimental features may be deprecated any time
[experimental.webhooks]
enabled = true
- 两个函数均
verify_jwt = false:关闭 JWT 校验,改由auth: 'secret'用 secret key 鉴权——这是"函数作为机器对机器 Webhook 端点"的典型配置,公开 API 场景不应照搬; [experimental.webhooks] enabled = true:本地环境下启用数据库 Webhook 实验特性,README 的部署步骤中也要求用supabase config push在远端项目打开同一开关;[db.seed]指向./seed.sql,db reset时会自动灌入种子数据。
7. 种子数据:12 条预计算向量
seed.sql 一次性插入 12 条内容及其预计算好的 384 维向量,涵盖 Bed、Car、Train、Cat、Dog、Apple、Boat、Mouse、Chair、Tomato、Desk、Banana,例如:
INSERT INTO "public"."embeddings" ("content", "embedding") OVERRIDING SYSTEM VALUE VALUES
('Bed', '[-0.006822244,-0.0073390524,0.040399525,...]'),
('Car', '[-0.013675096,0.027324528,0.06942244,...]'),
...
使用 OVERRIDING SYSTEM VALUE 是为兼容 id bigint generated always as identity 列(该列不允许普通 VALUES 直接插入主键,但这里主键由数据库生成、种子只提供 content 与 embedding)。预置向量的意义在于:本地调试 search 函数时无需等待 Webhook 往返即可立即得到可复现的检索结果;而当你手动插入新的 content 行时,Webhook 链路才会被真正触发,走一遍完整的"推理 → 回写"流程。
8. 部署:从本地示例到托管项目
README 给出的部署序列共五步,每步都有明确的落点文件:
# 1. 关联项目
supabase link
# 2. 部署两个 Edge Function(generate-embedding 与 search)
supabase functions deploy
# 3. 推送项目配置,开启 webhooks 实验特性
supabase config push
# 4. 打开 database-webhook 迁移,填入 generate-embedding 的项目地址与 secret key
# (见 supabase/migrations/20240410041607_database-webhook.sql)
# 5. 推送数据库 schema(含触发器)
supabase db push
顺序不能颠倒:函数必须先于触发器存在,否则首次数据变更时 Webhook 会打到不存在的端点;config push 必须先于 db push,因为触发器依赖服务端 Webhook 特性已启用。
9. 运行验证:curl 发起语义搜索
部署完成后,用 secret key 调用搜索端点(README 原样命令):
curl -i --location --request POST 'https://<PROJECT-REF>.supabase.co/functions/v1/search' \
--header 'apikey: <SUPABASE_SECRET_KEY>' \
--header 'Content-Type: application/json' \
--data '{"search":"vehicles"}'
预期响应形如 {"search":"vehicles","result":[{"content":"Car"},{"content":"Boat"},{"content":"Train"}]}——搜索词 "vehicles"(车辆)会命中 Car/Boat/Train 这类语义相近的种子数据,且由于 match_threshold: 0.8 的阈值约束,低相似度的内容(如 Apple、Chair)不会混入前 3 条结果。本地环境则将 URL 换成 http://127.0.0.1:54321/functions/v1/search 即可(见第 5.2 节)。
10. 小结:这套模式的可迁移要点
从源码结构看,该示例沉淀了一套可复用的"边缘 AI + 向量库"模式,迁移到其他场景时核心决策不变:
- 推理放在边缘:
Supabase.ai.Session让嵌入生成与数据存储同处 Supabase 平台内部,省去外部 Embedding API 的网络往返与密钥管理; - 写路径由数据库事件驱动:
pg_net触发器 + Webhook 函数实现"表变更即向量化",函数内的 content 变更比较是防止重复推理的轻量幂等手段; - 读路径收敛为单个 RPC:
query_embeddings把阈值过滤与排序封装进 Postgres,调用侧只关心阈值与条数;归一化向量 + 内积 + HNSW(vector_ip_ops) 索引是三者一致配合的结果,改模型维度时必须同步修改vector(384)列宽与query_embeddings参数类型; - 鉴权选择有边界:
verify_jwt = false+auth: 'secret'适合服务间调用,若函数直接暴露给浏览器端用户,应改回 JWT 校验并移除 secret key 请求头。
所有相关文件集中在 examples/ai/edge-functions 目录下:函数源码、三个迁移、种子数据与本地配置一一对应,可直接复制为自己的项目起点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00