首页
/ Supabase Edge Functions 原生 AI 推理:gte-small 文本向量与 pgvector 语义搜索实战

Supabase Edge Functions 原生 AI 推理:gte-small 文本向量与 pgvector 语义搜索实战

2026-09-06 17:25:27作者:宣利权Counsellor

本文基于 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-small model 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 条内容

数据流如下:

  1. embeddings 表插入/更新一行 content
  2. 数据库触发器(supabase_functions.http_request,基于 pg_net 扩展)发出 HTTP POST 到 generate-embedding
  3. generate-embeddinggte-small 计算 384 维向量,通过 supabaseAdmin 客户端回写到该行的 embedding 列;
  4. 客户端调用 search 函数 → 对搜索词生成向量 → 调用 query_embeddings RPC → 返回按相似度排序的匹配内容。

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.contentold_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.sqldb 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 + 向量库"模式,迁移到其他场景时核心决策不变:

  1. 推理放在边缘Supabase.ai.Session 让嵌入生成与数据存储同处 Supabase 平台内部,省去外部 Embedding API 的网络往返与密钥管理;
  2. 写路径由数据库事件驱动pg_net 触发器 + Webhook 函数实现"表变更即向量化",函数内的 content 变更比较是防止重复推理的轻量幂等手段;
  3. 读路径收敛为单个 RPCquery_embeddings 把阈值过滤与排序封装进 Postgres,调用侧只关心阈值与条数;归一化向量 + 内积 + HNSW(vector_ip_ops) 索引是三者一致配合的结果,改模型维度时必须同步修改 vector(384) 列宽与 query_embeddings 参数类型;
  4. 鉴权选择有边界verify_jwt = false + auth: 'secret' 适合服务间调用,若函数直接暴露给浏览器端用户,应改回 JWT 校验并移除 secret key 请求头。

所有相关文件集中在 examples/ai/edge-functions 目录下:函数源码、三个迁移、种子数据与本地配置一一对应,可直接复制为自己的项目起点。

登录后查看全文
热门项目推荐
相关项目推荐