本指南的目的是演示如何将 OpenAI 嵌入存储在 Supabase Vector (Postgres + pgvector) 中,以用于语义搜索。
Supabase 是一个开源的 Firebase 替代方案,构建于 Postgres 之上,这是一个生产级的 SQL 数据库。由于 Supabase Vector 构建于 pgvector 之上,您可以将嵌入存储在与应用程序其余数据相同的数据库中。当与 pgvector 的索引算法结合使用时,向量搜索在大规模数据下仍然快速。
Supabase 添加了一个服务和工具生态系统,以尽可能快速地进行应用程序开发(例如 自动生成的 REST API)。我们将使用这些服务在 Postgres 中存储和查询嵌入。
本指南涵盖
首先访问 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 未经授权访问此表(稍后会详细介绍)。
本指南使用 JavaScript 生成嵌入,但您可以轻松地修改它以使用 OpenAI 支持的任何 语言。
如果您使用 JavaScript,请随意使用您喜欢的任何服务器端 JavaScript 运行时环境(Node.js、Deno、Supabase Edge Functions)。
如果您使用 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 Edge Functions,您可以直接从 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 Edge Functions,这些环境变量将自动注入到您的函数中,因此您可以跳过上述步骤。
接下来,我们将把这些环境变量拉入我们的应用程序。
在 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 文档。