用 Portkey + Supabase pgvector 构建支持文章智能推荐应用:完整实战指南
【免费下载链接】gatewayA blazing fast AI Gateway with integrated guardrails. Route to 1,600+ LLMs, 50+ AI Guardrails with 1 fast & friendly API.项目地址: https://gitcode.com/GitHub_Trending/ga/gateway
导读:本文基于开源仓库 Gateway(Portkey AI Gateway)的官方教程,逐步实现一个 NodeJS 应用:把一批“支持文章标题”通过 OpenAI 嵌入模型转化为向量并存入 Supabase 的 pgvector 扩展,当用户输入新查询时,借助数据库函数完成向量相似度检索并返回最匹配的文章。读完本文你将掌握:embedding 与向量检索的核心概念、Portkey 虚拟密钥(Virtual Key)的接入方式、pgvector 建表与相似度匹配函数的完整 SQL、以及如何用 Gateway 的/v1/embeddings接口对接底层实现。
什么是向量相似度搜索,它为何特殊?
简短的回答是:Embeddings(嵌入向量)。
将一段内容翻译成向量表示的技术称为嵌入(Embeddings)。它允许你用数学方式分析内容的语义——LLM 能够把内容转化为向量表示,并嵌入到向量空间中,两条内容的相似程度由它们向量之间的距离来决定。这些向量需要被存储到向量数据库中,本文选用 Supabase 并启用其pgvector扩展来存储向量。
嵌入向量示意(内容 → 向量 → 向量空间):
说明:本文旨在为“支持文章/文档推荐、搜索建议”这类问题提供一个可复现的起点与导航地图,请结合你的实际业务结构调整字段与阈值。
应用总览:整体工作流程
我们的应用利用 Supabase 向量数据库以 embedding 形式维护文章,收到用户新查询后,由数据库智能推荐最相关的文章。
完整处理流程分为四步:
- 应用读取一个包含文章标题列表的文本文件;
- 通过Portkey调用 OpenAI 模型,将内容转换为 embedding;
- 将 embedding 存入pgvector,并创建一个支持相似度匹配的数据库函数;
- 当用户输入新查询时,应用基于该数据库函数返回最相关的文章。
环境准备:Portkey、Supabase、NodeJS 三件事
开始编码前,先完成三个环境的配置。
Portkey:创建虚拟密钥(Virtual Key)
- 注册并登录 Portkey 控制台;
- 复制你的 OpenAI API Key,将其添加到 Portkey Vault(虚拟密钥)。
这会为你生成一个唯一标识符——虚拟密钥(Virtual Key),后续代码中可直接引用它,而无需在应用里明文保存 OpenAI 的原始 Key。从仓库源码可以看到,Gateway 在构建请求时会识别virtual_key这类标识(见 modelsHandler.ts 中HEADER_KEYS.VIRTUAL_KEY的处理),并在 handlerUtils.ts 中通过virtualKeyDetails解析密钥明细,实现密钥的托管与复用。
Supabase:创建项目并启用 Vector 扩展
进入 Supabase 创建一个新项目(示例中命名为 “Product Wiki”)。创建完成后会得到访问密钥,包括Project URL与API Key,请保存好。
项目就绪后,为了让数据库能存储 embedding,必须在Dashboard > Database > Extensions中启用Vector扩展。
NodeJS:初始化项目与文章数据
进入任意目标目录,初始化项目:
npm init -y查看生成的package.json。由于需要把文章列表存入数据库,先创建articles.txt并复制以下内容(10 条典型支持文章标题):
Update Your Operating System Resetting Your Password Maximizing Battery Life Cleaning Your Keyboard Protecting Against Malware Backing Up Your Data Troubleshooting Wi-Fi Issues Optimizing Your Workspace Understanding Cloud Storage Managing App Permissions打开index.js,开始编写代码。
Step 1:导入并认证 Portkey 与 Supabase
应用需要与 OpenAI(经由 Portkey)和 Supabase pgvector 数据库交互,因此先导入必要的 SDK 客户端:
import { Portkey } from 'portkey-ai'; import { createClient } from '@supabase/supabase-js'; import fs from 'fs'; const USER_QUERY = 'How to update my laptop?'; const supabase = createClient('https://rbhjxxxxxxxxxkr.supabase.co', process.env['SUPABASE_PROJECT_API_KEY']); const portkey = new Portkey({ apiKey: process.env['PORTKEY_API_KEY'], virtualKey: process.env['OPENAI_VIRTUAL_KEY'] });fs用于从articles.txt读取文章列表;USER_QUERY是稍后做相似度搜索用的用户查询;supabase客户端通过Project URL + API Key认证;portkey客户端使用Portkey API Key + 虚拟密钥认证,虚拟密钥决定实际路由到哪个上游(这里是 OpenAI)。
在 Gateway 仓库中,OpenAI 嵌入接口的鉴权与参数解析由 openai/embed.ts 的OpenAIEmbedConfig定义,它把model、input、encoding_format、dimensions、user等参数映射到 OpenAI 上游请求,其中model默认值正是text-embedding-ada-002。
Step 2:在 Supabase 中创建数据表
使用 Supabase 的 SQL Editor 执行 SQL。本项目只需一张表support_articles,存储文章的title及其 embedding。你可以按需增加字段(如 description、tags)。
为简单起见,创建包含ID、content、embedding三列的表:
create table support_articles ( id bigint primary key generated always as identity, content text, embedding vector (1536) );在 SQL Editor 中执行上述语句:
执行成功后,可在Database > Tables > support_articles中验证表已创建,Results 标签页会出现成功提示。
维度说明:
vector (1536)的维度必须与所用嵌入模型输出的向量维度一致。text-embedding-ada-002输出 1536 维向量,因此建表时声明vector(1536)。若更换模型(例如输出 3072 维的text-embedding-3-large),需同步修改建表与函数中的维度声明。
Step 3:读取文章、生成并存储 Embeddings
使用fs读取articles.txt,将每一行标题转换为 embedding。借助 Portkey,生成 embedding 的写法与直接使用 OpenAI SDK 完全一致,无需额外代码改动——这也是通过 AI Gateway 接入多个模型时的最大便利。
生成 embedding 的核心调用:
const response = await portkey.embeddings.create({ input: String(text), model: 'text-embedding-ada-002' }); return Array.from(response.data[0].embedding);存储到 Supabase:
await supabase.from('support_articles').insert({ content, embedding });把「读文件 → 生成 embedding → 写入 Supabase」整合在一起:
async function convertToEmbeddings(text) { const response = await portkey.embeddings.create({ input: String(text), model: 'text-embedding-ada-002' }); return Array.from(response.data[0].embedding); } async function readTitlesFromFile() { const titlesPath = './articles.txt'; const titles = fs .readFileSync(titlesPath, 'utf8') .split('\n') .map((title) => title.trim()); return titles; } async function storeSupportArticles() { const titles = await readTitlesFromFile(); titles.forEach(async function (title) { const content = title; const embedding = await convertToEmbeddings(content); await supabase.from('support_articles').insert({ content, embedding }); }); }就这些!——只需一行调用即可把所有条目写入 pgvector 数据库:
await storeSupportArticles();现在可以从 Table Editor 看到创建的行:
补充:响应结构里能拿到什么
portkey.embeddings.create返回的响应结构与 OpenAI 一致,仓库 embedRequestBody.ts 的EmbedResponse类型给出了完整字段:
object:响应对象类型(如"list");data:嵌入结果数组,每项含object、embedding(向量数组)、index;model:实际使用的嵌入模型;usage:包含prompt_tokens与total_tokens,可用于成本与用量统计。
这解释了为什么代码中用response.data[0].embedding就能取到第一个输入文本的 1536 维向量。
Step 4:创建数据库函数以查询相似匹配
接下来,在 Supabase 中创建一个数据库函数做向量相似度搜索。该函数接收用户查询向量作为参数,返回与用户查询最匹配的行,包含id、content和相似度分数similarity:
create or replace function match_documents ( query_embedding vector(1536), match_threshold float, match_count int ) returns table ( id bigint, content text, similarity float ) language sql stable as $$ select support_articles.id, -- documents here is the table name support_articles.content, 1 - (support_articles.embedding <=> query_embedding) as similarity -- <=> is cosine similarity search from support_articles where 1 - (support_articles.embedding <=> query_embedding) > match_threshold order by (support_articles.embedding <=> query_embedding) asc limit match_count; $$;在 SQL Editor 中执行(与建表步骤相同):
恭喜,现在support_articles表已具备返回向量相似度搜索结果的能力。
关键点拆解
<=>是 pgvector 提供的余弦距离运算符;1 - 余弦距离即余弦相似度,值越接近 1 表示越相似;match_threshold用于过滤相似度低于阈值的低质量匹配;match_count限制返回的行数;language sql stable声明该函数是稳定的 SQL 函数(不写库、结果确定),便于优化器使用。
Step 5:查询相似度匹配
Supabase 客户端可以通过远程过程调用(RPC)调用match_documents函数,找到与用户查询最接近的匹配:
async function findNearestMatch(queryEmbedding) { const { data } = await supabase.rpc('match_documents', { query_embedding: queryEmbedding, match_threshold: 0.5, match_count: 1 }); return data; }传入的参数与 Step 4 中声明函数时的形参一一对应:query_embedding(查询向量)、match_threshold(相似度阈值,示例为 0.5)、match_count(返回条数,示例为 1)。
组装完整执行流程:
const USER_QUERY = 'How to update my laptop?'; // Invoke the following Fn to store embeddings to Supabase // await storeSupportArticles(); const queryEmbedding = await convertToEmbeddings(USER_QUERY); let best_match = await findNearestMatch(queryEmbedding); console.info('The best match is: ', best_match);控制台输出:
The best match is: [ { id: 12, content: 'Update Your Operating System', similarity: 0.874387819265234 } ]用户查询"How to update my laptop?"与"Update Your Operating System"的余弦相似度高达约 0.874,成功命中预期文章。
观察与监控:一次查询的成本与延迟
针对上述用户查询,单次查询(含 embedding 生成)消耗约 6 tokens,成本约为 $0.0001。整个开发过程中共消耗约 2.4k tokens,平均延迟 383ms。这些数据全部来自Portkey Dashboard的可观测能力:
这类信息在生产环境实时监控中极具价值。建议你在进行中的项目里落地搜索类用例——推荐、建议、FAQ 匹配等场景都可复用本文方案。至此,你已经掌握了如何在开发中使用 embedding、并在生产环境中监控应用。
原理纵深:Gateway 中/v1/embeddings的底层实现
上述教程通过portkey-aiSDK 直连 Portkey 云服务。而在本开源仓库(Gateway)中,同样能力以自托管网关形式提供,其实现细节与教程完全同源:
1. 路由注册:网关在 src/index.ts 注册了POST /v1/embeddings路由,经requestValidator校验后交由embeddingsHandler处理,与chat/completions、completions等端点并列。
2. 请求处理链:embeddingsHandler.ts 先解析 JSON 请求体与请求头,通过constructConfigFromRequestHeaders从x-portkey-*头中还原 provider 配置,再调用tryTargetsRecursively(见 handlerUtils.ts)按目标组递归尝试路由——这也正是网关支持多 provider、多目标、失败回退与重试的基础。
3. 参数映射:openai/embed.ts 的OpenAIEmbedConfig声明了 OpenAI 嵌入接口的参数契约:model(必填,默认text-embedding-ada-002)、input(必填)、可选的encoding_format、dimensions、user。请求体结构EmbedRequestBody(config+params)与响应结构EmbedResponse定义在 embedRequestBody.ts。
这意味着:教程中portkey.embeddings.create({ input, model })的调用方式,与向自托管 Gateway 发起POST /v1/embeddings的请求语义完全一致,生产环境可直接把 SDK 指向自部署网关,实现密钥托管、负载均衡、缓存与可观测性的统一。
小结与延伸
通过本文,你已经完成了一条完整的“文本 → 向量 → 检索”链路:
| 环节 | 技术选型 | 关键动作 |
|---|---|---|
| 内容向量化 | OpenAI(经 Portkey/虚拟密钥) | portkey.embeddings.create |
| 向量存储 | Supabase + pgvector | 建表vector(1536),插入 embedding |
| 相似度检索 | 数据库函数 + RPC | match_documents+supabase.rpc |
| 监控 | Portkey Dashboard | 观察 token、延迟、成本 |
从仓库源码(index.ts、embeddingsHandler.ts、openai/embed.ts)可以看到,这套能力在 Gateway 中是一等公民 API,可直接迁移到生产环境自托管部署。后续你还可以尝试:把articles.txt替换为真实文档库并增加 description 字段;用text-embedding-3-large等更高维模型提升精度(记得同步修改vector(1536)与函数签名);在网关上叠加缓存与限流配置,进一步优化查询成本。
【免费下载链接】gatewayA blazing fast AI Gateway with integrated guardrails. Route to 1,600+ LLMs, 50+ AI Guardrails with 1 fast & friendly API.项目地址: https://gitcode.com/GitHub_Trending/ga/gateway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考