使用 Supabase Vector 进行语义搜索
本指南旨在演示如何将 OpenAI 嵌入存储在 Supabase Vector(Postgres + pgvector)中,以实现语义搜索。
Supabase 是一个开源的 Firebase 替代品,建立在 Postgres 之上,这是一个生产级的 SQL 数据库。由于 Supabase Vector 基于 pgvector,您可以将嵌入存储在与应用程序其他数据相同的数据库中。结合 pgvector 的索引算法,向量搜索在大规模时仍然保持快速。
Supabase 添加了一系列服务和工具,以尽可能快速地进行应用程序开发(例如自动生成的 REST API)。我们将使用这些服务在 Postgres 中存储和查询嵌入。
本指南涵盖:
- 设置数据库
- 创建一个可以存储向量数据的 SQL 表
- 使用 OpenAI 的 JavaScript 客户端生成 OpenAI 嵌入
- 使用 Supabase JavaScript 客户端将嵌入存储在 SQL 表中
- 使用 Postgres 函数和 Supabase JavaScript 客户端对嵌入进行语义搜索
设置数据库
首先前往 https://database.new 配置您的 Supabase 数据库。这将在 Supabase 云平台上创建一个 Postgres 数据库。或者,如果您更喜 欢使用 Docker 在本地运行数据库,可以按照本地开发选项进行操作。
在工作室中,跳转到 SQL 编辑器 并执行以下 SQL 以启用 pgvector:
-- 启用 pgvector 扩展
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 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";
// 加载 .env 文件
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";
// 加载 .env 文件
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 编辑器 中执行以下操作:
创建函数 match_documents (
query_embedding 向量 (1536),
match_threshold 浮点数,
)
返回集合 documents
语言 plpgsql
as $$
开始
返回查询
选择 *
从 documents
其中 documents.embedding <#> query_embedding < -match_threshold
按 documents.embedding <#> query_embedding 排序;
结束;
$$;
这个函数接受一个 query_embedding
,它代表了从搜索查询文本生成的嵌入(稍后会详细介绍)。它还接受一个 match_threshold
,这个阈值指定了文档嵌入与 query_embedding
的相似度必须达到多少才能被视为匹配。
在函数内部,我们实现了查询操作,它主要做两件事:
- 过滤文档,只包括那些嵌入与上述
match_threshold
匹配的文档。由于<#>
操作符执行的是负内积(与正内积相反),我们在比较之前先对相似度阈值取反。这意味着match_threshold
为 1 时最相似,-1 时最不相似。 - 按负内积(
<#>
)升序排列文档。这使我们能够先检索最匹配的文档。
由于 OpenAI 的嵌入是标准化的,我们选择使用内积(
<#>
),因为它比其他操作符(如余弦距离<=>
)稍微高效一些。但需要注意的是,这仅在嵌入标准化的情况下有效——如果不是,则应使用余弦距离。
现在,我们可以使用 supabase.rpc()
方法从应用程序中调用这个函数:
const query = "What does the cat chase?";
// 首先为查询本身创建一个嵌入
const result = await openai.embeddings.create({
input: query,
model: "text-embedding-3-small",
});
const [{ embedding }] = result.data;
// 然后使用这个嵌入来搜索匹配项
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 嵌入的更多信息,请阅读 嵌入 文档。
有关 Supabase Vector 的更多信息,请阅读 AI & Vector 文档。