news 2026/9/15 14:46:30

用 Portkey + Supabase pgvector 构建支持文章智能推荐应用:完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Portkey + Supabase pgvector 构建支持文章智能推荐应用:完整实战指南

用 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 形式维护文章,收到用户新查询后,由数据库智能推荐最相关的文章。

完整处理流程分为四步:

  1. 应用读取一个包含文章标题列表的文本文件;
  2. 通过Portkey调用 OpenAI 模型,将内容转换为 embedding;
  3. 将 embedding 存入pgvector,并创建一个支持相似度匹配的数据库函数;
  4. 当用户输入新查询时,应用基于该数据库函数返回最相关的文章。

环境准备:Portkey、Supabase、NodeJS 三件事

开始编码前,先完成三个环境的配置。

Portkey:创建虚拟密钥(Virtual Key)

  1. 注册并登录 Portkey 控制台;
  2. 复制你的 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 URLAPI 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定义,它把modelinputencoding_formatdimensionsuser等参数映射到 OpenAI 上游请求,其中model默认值正是text-embedding-ada-002

Step 2:在 Supabase 中创建数据表

使用 Supabase 的 SQL Editor 执行 SQL。本项目只需一张表support_articles,存储文章的title及其 embedding。你可以按需增加字段(如 description、tags)。

为简单起见,创建包含IDcontentembedding三列的表:

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:嵌入结果数组,每项含objectembedding(向量数组)、index
  • model:实际使用的嵌入模型;
  • usage:包含prompt_tokenstotal_tokens,可用于成本与用量统计。

这解释了为什么代码中用response.data[0].embedding就能取到第一个输入文本的 1536 维向量。

Step 4:创建数据库函数以查询相似匹配

接下来,在 Supabase 中创建一个数据库函数做向量相似度搜索。该函数接收用户查询向量作为参数,返回与用户查询最匹配的行,包含idcontent和相似度分数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/completionscompletions等端点并列。

2. 请求处理链:embeddingsHandler.ts 先解析 JSON 请求体与请求头,通过constructConfigFromRequestHeadersx-portkey-*头中还原 provider 配置,再调用tryTargetsRecursively(见 handlerUtils.ts)按目标组递归尝试路由——这也正是网关支持多 provider、多目标、失败回退与重试的基础。

3. 参数映射:openai/embed.ts 的OpenAIEmbedConfig声明了 OpenAI 嵌入接口的参数契约:model(必填,默认text-embedding-ada-002)、input(必填)、可选的encoding_formatdimensionsuser。请求体结构EmbedRequestBodyconfig+params)与响应结构EmbedResponse定义在 embedRequestBody.ts。

这意味着:教程中portkey.embeddings.create({ input, model })的调用方式,与向自托管 Gateway 发起POST /v1/embeddings的请求语义完全一致,生产环境可直接把 SDK 指向自部署网关,实现密钥托管、负载均衡、缓存与可观测性的统一。

小结与延伸

通过本文,你已经完成了一条完整的“文本 → 向量 → 检索”链路:

环节技术选型关键动作
内容向量化OpenAI(经 Portkey/虚拟密钥)portkey.embeddings.create
向量存储Supabase + pgvector建表vector(1536),插入 embedding
相似度检索数据库函数 + RPCmatch_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 14:43:49

服装AI质检:客户投诉的那件衣服,你的系统能查到它的质检记录吗?

1. 一个让质检负责人后背发凉的场景 “这件衣服客户投诉了&#xff0c;说是缝线开裂。你们质检是怎么过的&#xff1f;” 电话那头是销售总监的声音&#xff0c;带着一丝压不住的火气。你打开电脑&#xff0c;准备调出这件衣服的质检记录——然后愣住了。 这件衣服是哪一批的&a…

作者头像 李华
网站建设 2026/9/15 14:42:04

PSCAD 4.6.3专业版升级与电力系统仿真优化

1. PSCAD 4.6.3专业版核心升级解析作为电力系统电磁暂态仿真领域的标杆工具&#xff0c;PSCAD 4.6.3专业版的发布带来了多项关键改进。实测发现其并行计算效率提升约40%&#xff0c;特别是在处理含新能源接入的大型电网模型时&#xff0c;单次仿真时间从原先的27分钟缩短至16分…

作者头像 李华
网站建设 2026/9/15 14:41:05

如何从源码运行Evolver:贡献者完整教程(node index.js模式)

如何从源码运行Evolver&#xff1a;贡献者完整教程&#xff08;node index.js模式&#xff09; 【免费下载链接】evolver The GEP-powered self-evolving engine for AI agents. Auditable evolution with Genes, Capsules, and Events. | evomap.ai 项目地址: https://gitco…

作者头像 李华
网站建设 2026/9/15 14:40:23

反弹shell与DNSlog带外查询:从正反向连接到无回显数据带出全解析

开门见山说个现象&#xff1a;不管你是打CTF还是做授权渗透测试&#xff0c;反弹shell这一关基本绕不过去&#xff0c;而一说到反弹shell&#xff0c;就躲不开正反向连接的区别、各种语言构造姿势&#xff0c;以及在没有回显时怎么用DNSlog带外查询把数据偷出来。Day5这部分内容…

作者头像 李华
网站建设 2026/9/15 14:38:59

House of Spirit

gegenzgenzeggenzegePWN堆house of spirit-1 | iyheart的博客 跟着这个师傅学的堆这块知识 在buuctf中有这道题目&#xff1a;lctf2016_pwn200 正所谓回头看轻舟已过万重山&#xff0c;花了好长时间理解这道题&#xff0c;不是这个手法多难&#xff0c;恰恰暴露了我在栈帧那…

作者头像 李华