本指南旨在演示如何将OpenAI嵌入存储在Supabase Vector(Postgres + pgvector)中,以实现语义搜索。
Supabase 是一个基于生产级SQL数据库Postgres构建的开源Firebase替代方案。由于Supabase Vector基于pgvector构建,您可以将嵌入向量存储在与其他应用数据相同的数据库中。结合pgvector的索引算法,向量搜索在大规模场景下仍能保持高速。
Supabase 添加了一系列服务和工具生态系统,以尽可能快速地开发应用程序(例如自动生成的REST API)。我们将使用这些服务在Postgres中存储和查询嵌入向量。
本指南涵盖:
- 设置您的数据库
- 创建SQL表以存储向量数据
- 生成OpenAI嵌入向量 使用OpenAI的JavaScript客户端
- 存储嵌入向量到您的SQL表中,使用Supabase JavaScript客户端
- 执行语义搜索 通过使用Postgres函数和Supabase JavaScript客户端对嵌入向量进行操作
设置数据库
首先前往 https://database.new 配置您的Supabase数据库。这将在Supabase云平台上创建一个Postgres数据库。或者,如果您更倾向于在本地使用Docker运行数据库,可以按照本地开发选项进行操作。
在工作室中,跳转到SQL编辑器并执行以下SQL以启用pgvector:
-- Enable the pgvector extension
create extension if not exists vector;在生产环境中,最佳实践是使用数据库迁移,以便所有SQL操作都能在源代码控制中管理。为了在本指南中保持简单,我们将直接在SQL编辑器中执行查询。如果您正在构建生产应用,可以随时将这些操作转移到数据库迁移中。
创建向量表
接下来我们将创建一个表来存储文档和嵌入向量。在SQL编辑器中运行:
create table documents (
id bigint primary key generated always as identity,
content text not null,
embedding vector (1536) not null
);由于Supabase基于Postgres构建,我们这里直接使用标准SQL。您可以根据应用需求自由修改此表。如果已有数据库表,只需在相应表中添加新的vector列即可。
关键需要理解的是vector数据类型,这是当我们之前启用pgvector扩展时新增的数据类型。向量的大小(此处为1536)表示嵌入的维度数量。由于本示例中我们使用OpenAI的text-embedding-3-small模型,因此将向量大小设置为1536。
让我们继续在这个表上创建一个向量索引,这样随着表的增长,未来的查询仍能保持高性能:
create index on documents using hnsw (embedding vector_ip_ops);该索引使用HNSW算法对存储在embedding列中的向量进行索引,特别是当使用内积运算符(<#>)时。我们稍后在实现匹配函数时会详细解释这个运算符。
让我们也遵循安全最佳实践,在表上启用行级安全:
alter table documents enable row level security;这将防止通过自动生成的REST API未经授权访问此表(稍后会详细介绍)。
生成OpenAI嵌入向量
本指南使用JavaScript生成嵌入向量,但您可以轻松修改它以使用任何OpenAI支持的语言。
如果您正在使用JavaScript,可以随意选择您偏好的服务器端JavaScript运行时环境(Node.js、Deno、Supabase边缘函数)。
如果你正在使用Node.js,首先安装openai作为依赖项:
npm install openai然后导入它:
import OpenAI from "openai";如果您正在使用Deno或Supabase Edge Functions,可以直接从URL导入openai:
import OpenAI from "https://esm.sh/openai@4";在这个示例中,我们从https://esm.sh导入,这是一个CDN服务,它会自动为您获取相应的NPM模块并通过HTTP提供服务。
接下来我们将使用text-embedding-3-small生成一个OpenAI嵌入向量:
const openai = new OpenAI();
const input = "The cat chases the mouse";
const result = await openai.embeddings.create({
input,
model: "text-embedding-3-small",
});
const [{ embedding }] = result.data;请注意,您需要一个OpenAI API密钥才能与OpenAI API进行交互。您可以通过名为OPENAI_API_KEY的环境变量传递此密钥,或者在实例化OpenAI客户端时手动设置:
const openai = new OpenAI({
apiKey: "<openai-api-key>",
});请记住:切勿在代码中硬编码API密钥。最佳实践是将其存储在.env文件中,并使用类似dotenv的库加载,或从外部密钥管理系统加载。
将嵌入向量存储到数据库中
Supabase 提供了一个自动生成的 REST API,能为您的每张表动态构建 REST 端点。这意味着您无需直接建立到数据库的 Postgres 连接——只需通过 REST API 即可与之交互。这在运行短期进程的无服务器环境中尤为有用,因为每次重新建立数据库连接可能代价高昂。
Supabase 提供了多个客户端库来简化与 REST API 的交互。在本指南中我们将使用JavaScript 客户端库,但您可以根据需要切换至您偏好的编程语言。
如果你正在使用Node.js,请安装@supabase/supabase-js作为依赖项:
npm install @supabase/supabase-js然后导入它:
import { createClient } from "@supabase/supabase-js";如果您正在使用Deno或Supabase边缘函数,可以直接从URL导入@supabase/supabase-js:
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";接下来我们将实例化Supabase客户端并进行配置,使其指向您的Supabase项目。在本指南中,我们会将Supabase URL和密钥的引用存储在.env文件中,但您可以根据应用程序处理配置的方式自由修改此设置。
如果您正在使用Node.js或Deno,请将您的Supabase URL和服务角色密钥添加到.env文件中。如果使用云平台,您可以在Supabase仪表板的设置页面找到这些信息。如果在本地运行Supabase,可以通过在终端执行npx supabase status命令来获取这些信息。
.env
SUPABASE_URL=<supabase-url>
SUPABASE_SERVICE_ROLE_KEY=<supabase-service-role-key>如果您正在使用Supabase边缘函数,这些环境变量会自动注入到您的函数中,因此您可以跳过上述步骤。
接下来我们将把这些环境变量引入到我们的应用中。
在Node.js中,安装dotenv依赖项:
npm install dotenv并从process.env中获取环境变量:
import { config } from "dotenv";
// Load .env file
config();
const supabaseUrl = process.env["SUPABASE_URL"];
const supabaseServiceRoleKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];在Deno中,使用dotenv标准库加载.env文件:
import { load } from "https://deno.land/std@0.208.0/dotenv/mod.ts";
// Load .env file
const env = await load();
const supabaseUrl = env["SUPABASE_URL"];
const supabaseServiceRoleKey = env["SUPABASE_SERVICE_ROLE_KEY"];在Supabase Edge Functions中,直接加载注入的环境变量即可:
const supabaseUrl = Deno.env.get("SUPABASE_URL");
const supabaseServiceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");接下来让我们实例化我们的 supabase 客户端:
const supabase = createClient(supabaseUrl, supabaseServiceRoleKey, {
auth: { persistSession: false },
});从这里开始,我们使用supabase客户端将文本和嵌入向量(之前生成的)插入到数据库中:
const { error } = await supabase.from("documents").insert({
content: input,
embedding,
});在生产环境中,最佳实践是检查响应
error以查看插入数据时是否存在任何问题,并相应地进行处理。
语义搜索
最后让我们对数据库中的嵌入向量执行语义搜索。此时我们假设您的documents表中已填充了多条可供搜索的记录。
让我们在Postgres中创建一个执行语义搜索查询的匹配函数。请在SQL编辑器中执行以下操作:
create function match_documents (
query_embedding vector (1536),
match_threshold float,
)
returns setof documents
language plpgsql
as $$
begin
return query
select *
from documents
where documents.embedding <#> query_embedding < -match_threshold
order by documents.embedding <#> query_embedding;
end;
$$;该函数接收一个query_embedding参数,表示由搜索查询文本生成的嵌入向量(稍后会详细说明)。同时它还接收一个match_threshold参数,用于指定文档嵌入向量的相似度阈值,只有达到该阈值时query_embedding才会被视为匹配成功。
在函数内部我们实现了查询功能,主要完成两件事:
- 筛选文档,仅包含嵌入向量与上述
match_threshold匹配的文档。由于<#>运算符执行的是负内积(而非正内积),我们在比较前需对相似度阈值取反。这意味着match_threshold值为1时表示最相似,-1表示最不相似。 - 按负内积(
<#>)升序排列文档。这使我们能够优先检索匹配度最高的文档。
由于OpenAI嵌入已归一化,我们选择使用内积(
<#>)运算符,因为它的性能略优于余弦距离(<=>)等其他运算符。需要注意的是,这种方法仅在嵌入向量归一化的情况下有效——如果未归一化,则应使用余弦距离。
现在我们可以通过应用程序使用supabase.rpc()方法来调用这个函数:
const query = "What does the cat chase?";
// First create an embedding on the query itself
const result = await openai.embeddings.create({
input: query,
model: "text-embedding-3-small",
});
const [{ embedding }] = result.data;
// Then use this embedding to search for matches
const { data: documents, error: matchError } = await supabase
.rpc("match_documents", {
query_embedding: embedding,
match_threshold: 0.8,
})
.select("content")
.limit(5);在这个示例中,我们将匹配阈值设为0.8。请根据您的数据效果调整此阈值。
请注意,由于match_documents返回一组documents,我们可以将此rpc()视为常规表查询。具体来说,这意味着我们可以向此查询链接其他命令,如select()和limit()。这里我们仅从documents表中选择我们关心的列(content),并限制返回的文档数量(本例中最多5个)。
此时,您已获得一份基于语义关系匹配查询的文档列表,并按相似度从高到低排序。
后续步骤
你可以将此示例作为其他语义搜索技术的基础,比如检索增强生成(RAG)。
有关OpenAI嵌入的更多信息,请阅读Embedding文档。
有关Supabase Vector的更多信息,请阅读AI & Vector文档。