向量搜索
Supabase 通过 pgvector 扩展支持向量存储和相似度搜索,这是构建 AI 应用的核心能力。本章节介绍如何使用 pgvector 实现语义搜索、推荐系统和 RAG 应用。
向量搜索概述
向量搜索是将文本、图像等数据转换为向量(数字数组),然后通过计算向量之间的相似度来找到最相关的内容。这种方法比传统的关键词搜索更智能,能够理解语义关系。
应用场景
- 语义搜索:根据含义而非关键词搜索
- 推荐系统:推荐相似内容
- RAG(检索增强生成):为 AI 提供上下文
- 图像搜索:根据图像特征搜索相似图片
- 异常检测:识别异常数据点
pgvector 简介
pgvector 是 PostgreSQL 的扩展,提供:
- 向量存储(最多 2000 维)
- 相似度计算(L2、内积、余弦)
- 向量索引(IVFFlat、HNSW)
启用 pgvector
安装扩展
-- 启用 pgvector 扩展
create extension if not exists vector;
在 Supabase Dashboard 中,进入 Database → Extensions,搜索 "vector" 并启用。
验证安装
-- 检查扩展是否安装
select * from pg_extension where extname = 'vector';
-- 查看可用函数
select proname from pg_proc where proname like '%vector%';
创建向量表
基本表结构
-- 创建文档表
create table documents (
id bigint primary key generated always as identity,
content text not null,
embedding vector(1536), -- OpenAI text-embedding-ada-002 是 1536 维
metadata jsonb default '{}'::jsonb,
created_at timestamptz default now()
);
-- 添加注释
comment on table documents is '存储文档及其向量嵌入';
comment on column documents.embedding is '文本的向量表示';
向量维度选择
不同的嵌入模型有不同的维度:
| 模型 | 维度 | 说明 |
|---|---|---|
| OpenAI text-embedding-ada-002 | 1536 | 通用文本嵌入 |
| OpenAI text-embedding-3-small | 1536 | 新版小模型 |
| OpenAI text-embedding-3-large | 3072 | 新版大模型 |
| Cohere embed-english-v3.0 | 1024 | 英文嵌入 |
| Cohere embed-multilingual-v3.0 | 1024 | 多语言嵌入 |
| HuggingFace all-MiniLM-L6-v2 | 384 | 开源小模型 |
生成向量嵌入
使用 Edge Function 调用 OpenAI
// supabase/functions/embed/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Deno.serve(async (req: Request) => {
const { text } = await req.json()
// 调用 OpenAI API
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`
},
body: JSON.stringify({
model: 'text-embedding-ada-002',
input: text
})
})
const data = await response.json()
const embedding = data.data[0].embedding
return new Response(JSON.stringify({ embedding }), {
headers: { 'Content-Type': 'application/json' }
})
})
使用触发器自动生成
-- 创建函数调用嵌入 API
create or replace function generate_embedding()
returns trigger as $$
declare
embedding vector;
begin
-- 这里需要使用 pg_net 或其他方式调用外部 API
-- 简化示例:假设嵌入已通过应用生成
return new;
end;
$$ language plpgsql;
-- 创建触发器
create trigger on_document_insert
before insert on documents
for each row
execute function generate_embedding();
在应用中生成
import { createClient } from '@supabase/supabase-js'
import OpenAI from 'openai'
const supabase = createClient(
'https://your-project.supabase.co',
'your-service-role-key'
)
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})
async function addDocument(content, metadata = {}) {
// 生成嵌入
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: content
})
const embedding = response.data[0].embedding
// 存储到数据库
const { data, error } = await supabase
.from('documents')
.insert({
content,
embedding,
metadata
})
.select()
return data
}
向量搜索
基本查询
-- 查找最相似的文档
select
id,
content,
1 - (embedding <=> '[0.1, 0.2, ...]'::vector) as similarity
from documents
order by embedding <=> '[0.1, 0.2, ...]'::vector
limit 10;
相似度运算符
pgvector 支持三种相似度计算:
-- 余弦距离(推荐)
-- 值越小越相似,范围 [0, 2]
select * from documents
order by embedding <=> query_vector
limit 10;
-- L2 距离(欧几里得距离)
-- 值越小越相似
select * from documents
order by embedding <-> query_vector
limit 10;
-- 内积(负内积)
-- 值越小越相似
select * from documents
order by embedding <#> query_vector
limit 10;
在应用中搜索
async function searchDocuments(query) {
// 生成查询向量
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: query
})
const queryEmbedding = response.data[0].embedding
// 使用 RPC 调用向量搜索
const { data, error } = await supabase.rpc('search_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: 10
})
return data
}
创建搜索函数
-- 创建向量搜索函数
create or replace function search_documents(
query_embedding vector,
match_threshold float default 0.7,
match_count int default 10
)
returns table (
id bigint,
content text,
similarity float,
metadata jsonb
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity,
documents.metadata
from documents
where 1 - (documents.embedding <=> query_embedding) > match_threshold
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;
创建向量索引
对于大量数据,需要创建索引加速搜索:
IVFFlat 索引
-- 创建 IVFFlat 索引
create index on documents
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
-- lists 参数建议设置为 rows / 1000
-- 例如 100 万行数据,lists = 1000
HNSW 索引
-- 创建 HNSW 索引(更快的查询,更慢的构建)
create index on documents
using hnsw (embedding vector_cosine_ops)
with (
m = 16, -- 连接数,影响召回率
ef_construction = 64 -- 构建时的搜索范围
);
索引选择
| 索引类型 | 构建速度 | 查询速度 | 内存使用 | 适用场景 |
|---|---|---|---|---|
| IVFFlat | 快 | 中 | 低 | 中等数据量 |
| HNSW | 慢 | 快 | 高 | 大数据量、高召回率 |
过滤搜索
结合元数据过滤:
-- 创建搜索函数支持过滤
create or replace function search_documents_with_filter(
query_embedding vector,
filter_metadata jsonb default '{}'::jsonb,
match_threshold float default 0.7,
match_count int default 10
)
returns table (
id bigint,
content text,
similarity float,
metadata jsonb
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity,
documents.metadata
from documents
where
1 - (documents.embedding <=> query_embedding) > match_threshold
and documents.metadata @> filter_metadata
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;
使用示例:
// 搜索特定类别的文档
const { data } = await supabase.rpc('search_documents_with_filter', {
query_embedding: embedding,
filter_metadata: { category: 'technology' },
match_threshold: 0.7,
match_count: 10
})
RAG 应用
RAG(Retrieval-Augmented Generation)是构建 AI 应用的常用模式:
完整 RAG 流程
import OpenAI from 'openai'
import { createClient } from '@supabase/supabase-js'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
)
async function ragChat(question) {
// 1. 生成问题向量
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: question
})
const questionEmbedding = embeddingResponse.data[0].embedding
// 2. 搜索相关文档
const { data: documents } = await supabase.rpc('search_documents', {
query_embedding: questionEmbedding,
match_threshold: 0.7,
match_count: 5
})
// 3. 构建上下文
const context = documents
.map(doc => doc.content)
.join('\n\n---\n\n')
// 4. 调用 LLM 生成回答
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: '你是一个助手,根据提供的上下文回答问题。如果上下文中没有相关信息,请说明。'
},
{
role: 'user',
content: `上下文:\n${context}\n\n问题:${question}`
}
]
})
return {
answer: completion.choices[0].message.content,
sources: documents
}
}
分块存储长文档
-- 创建文档块表
create table document_chunks (
id bigint primary key generated always as identity,
document_id bigint references documents(id) on delete cascade,
content text not null,
embedding vector(1536),
chunk_index int not null,
created_at timestamptz default now()
);
-- 创建索引
create index idx_document_chunks_document on document_chunks(document_id);
create index idx_document_chunks_embedding on document_chunks
using ivfflat (embedding vector_cosine_ops) with (lists = 100);
async function addLongDocument(content, metadata = {}) {
// 创建文档记录
const { data: doc } = await supabase
.from('documents')
.insert({ content, metadata })
.select()
.single()
// 分块(简单按字符数分块)
const chunkSize = 1000
const chunks = []
for (let i = 0; i < content.length; i += chunkSize) {
chunks.push({
document_id: doc.id,
content: content.slice(i, i + chunkSize),
chunk_index: Math.floor(i / chunkSize)
})
}
// 为每个块生成嵌入
for (const chunk of chunks) {
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: chunk.content
})
chunk.embedding = response.data[0].embedding
}
// 存储块
await supabase.from('document_chunks').insert(chunks)
return doc
}
推荐系统
基于向量相似度的推荐:
-- 创建用户偏好表
create table user_preferences (
id bigint primary key generated always as identity,
user_id uuid references auth.users(id),
item_id bigint,
embedding vector(1536),
created_at timestamptz default now()
);
-- 推荐函数
create or replace function get_recommendations(
user_id uuid,
match_count int default 10
)
returns table (
id bigint,
content text,
similarity float
)
language plpgsql
as $$
declare
user_embedding vector;
begin
-- 计算用户偏好向量(平均)
select avg(embedding) into user_embedding
from user_preferences
where user_preferences.user_id = get_recommendations.user_id;
-- 查找相似内容
return query
select
documents.id,
documents.content,
1 - (documents.embedding <=> user_embedding) as similarity
from documents
where documents.id not in (
select item_id from user_preferences
where user_preferences.user_id = get_recommendations.user_id
)
order by documents.embedding <=> user_embedding
limit match_count;
end;
$$;
性能优化
批量插入
async function batchInsert(documents) {
// 批量生成嵌入
const texts = documents.map(d => d.content)
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: texts // 批量处理
})
const embeddings = response.data.map(d => d.embedding)
// 组合数据
const data = documents.map((doc, i) => ({
...doc,
embedding: embeddings[i]
}))
// 批量插入
await supabase.from('documents').insert(data)
}
预热索引
-- 强制索引使用
set enable_seqscan = off;
-- 或使用提示
select /*+ IndexScan(documents) */ * from documents
order by embedding <=> query_vector
limit 10;
监控查询性能
-- 查看执行计划
explain analyze
select * from documents
order by embedding <=> '[0.1, ...]'::vector
limit 10;
最佳实践
1. 选择合适的嵌入模型
- 通用场景:OpenAI text-embedding-ada-002
- 多语言:Cohere embed-multilingual
- 成本敏感:HuggingFace 开源模型
2. 合理设置维度
- 高维度 = 更精确,但存储和计算成本更高
- 可以使用降维技术(如 PCA)减少维度
3. 定期更新索引
-- 重建索引
reindex index documents_embedding_idx;
4. 监控召回率
-- 测试召回率
select count(*) from documents
where 1 - (embedding <=> query_vector) > 0.8;
下一步
掌握向量搜索后,你可以继续学习: