ByteNoteByteNote

字节笔记本

2026年8月28日

Supabase pgvector:Postgres 里的 AI 工具箱

API中转
¥120

做 RAG、语义搜索、知识库问答时,很多人第一反应是上独立向量库。其实如果你已经在用 Postgres,尤其是 Supabase,开一个 pgvector 扩展往往就够了:向量、元数据、权限、备份都在同一套数据库里。

Supabase 把这套能力整理成了开源 AI 工具箱:用 Postgres + pgvector 存嵌入,用客户端库读写,再用 Edge Functions 或外部模型生成向量。下面按「能跑起来」的顺序走一遍。

为什么用 Postgres 存向量

独立向量库(Pinecone、Weaviate 等)在超大规模 ANN 检索上很强,但多一套基础设施也意味着多一套同步、权限和运维。很多业务数据本来就在 Postgres:用户、文档、权限、审计日志。向量放旁边,用 SQL 做过滤就很自然:

  • 「只搜某个 workspace 的文档」
  • 「只返回公开且未删除的条目」
  • 「按创建时间 + 相似度一起排序」

Supabase 的立场很直白:最好的向量库,往往是你已经在用的那套库。

官方总览见 AI & Vectors

启用 pgvector

扩展在 Postgres 里的名字叫 vector(不是 pgvector)。在 SQL Editor 里执行:

sql
create extension vector
with
  schema extensions;

也可以在 Dashboard → Database → Extensions 里搜 vector 打开。

建一张带向量列的表

维度必须和嵌入模型一致。例如开源模型 gte-small 是 384 维:

sql
create table documents (
  id serial primary key,
  title text not null,
  body text not null,
  embedding extensions.vector(384)
);

换 OpenAI text-embedding-3-small 时,常见是 1536 维,把括号里的数字改掉即可。混用不同模型产出的向量去比距离,结果没有意义。

写入嵌入

思路很简单:先把文本变成向量,再和正文一起 insert。前端或 Node 里可以用 Transformers.js 本地算(不必每次打云端 API):

js
import { pipeline } from '@huggingface/transformers'

const generateEmbedding = await pipeline(
  'feature-extraction',
  'Supabase/gte-small'
)

const body = 'Hello world!'
const output = await generateEmbedding(body, {
  pooling: 'mean',
  normalize: true,
})
const embedding = Array.from(output.data)

const { error } = await supabase.from('documents').insert({
  title: 'First post!',
  body,
  embedding,
})

生产环境更常见的是:文档入库时用 Edge Function / 后台任务调 OpenAI、Hugging Face 等生成嵌入,再写回同一行。

相似度查询

pgvector 提供三种距离算子:

算子含义
<->欧氏距离
<#>负内积
<=>余弦距离

向量已归一化时,点积通常最快。PostgREST 目前不直接暴露这些算子,所以官方做法是包一层 SQL 函数,再用客户端 rpc() 调用:

sql
create or replace function match_documents (
  query_embedding extensions.vector(384),
  match_threshold float,
  match_count int
)
returns table (
  id bigint,
  title text,
  body text,
  similarity float
)
language sql stable
as $$
  select
    documents.id,
    documents.title,
    documents.body,
    1 - (documents.embedding <=> query_embedding) as similarity
  from documents
  where 1 - (documents.embedding <=> query_embedding) > match_threshold
  order by (documents.embedding <=> query_embedding) asc
  limit match_count;
$$;

客户端侧:

js
const { data: documents } = await supabase.rpc('match_documents', {
  query_embedding: embedding,
  match_threshold: 0.78,
  match_count: 10,
})

阈值先变成向量,再丢进 match_documentsmatch_threshold 要按自己的数据试,太低会捞一堆不相关的,太高又容易空结果。

更细的列定义和过滤示例见 Vector columnspgvector 扩展说明

和关键词检索怎么配合

纯语义搜索擅长「说法不同、意思接近」;纯关键词擅长专有名词、错误码、接口名。文档搜索里两者经常要一起用,也就是混合检索(hybrid search):向量召回一批,再和全文检索结果融合排序。

Supabase 文档把语义搜索、关键词搜索、混合搜索都列成可落地的方向,并有 Next.js + OpenAI 文档问答、CLIP 图像搜索等模板可参考。

数据量大了再加索引

表小的时候暴力扫描够用。行数上去之后再给向量列加 IVFFlat 或 HNSW 索引。注意:若查询里还带 category_id = … 这类过滤,索引可能返回不足 LIMIT 条;需要按官方建议做迭代扫描,或把过滤条件设计进函数里。

什么时候该换独立向量库

  • 向量量级到亿级、延迟要求极严
  • 需要复杂的多租户向量产品能力,而你又不想在 Postgres 上自己堆
  • 团队已经把向量链路和业务库强行拆开运维

否则,先用 Postgres + pgvector 把产品跑通通常更省事。不少团队(包括从 Pinecone / 自建 RDS 迁过来的案例)最后发现:业务过滤、权限和备份跟向量绑在一起,反而更简单。

小结

  1. 打开 vector 扩展
  2. 建表时向量维度对齐嵌入模型
  3. 写入时「文本 → embedding → insert」
  4. 用 SQL 函数 + rpc() 做相似度检索
  5. 需要时再上混合检索和向量索引

一套 Postgres,就能同时扛关系数据和向量检索。对多数中小规模的 RAG / 语义搜索来说,这已经是够用且好维护的起点。

分享: