跳到主要内容

向量搜索

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-0021536通用文本嵌入
OpenAI text-embedding-3-small1536新版小模型
OpenAI text-embedding-3-large3072新版大模型
Cohere embed-english-v3.01024英文嵌入
Cohere embed-multilingual-v3.01024多语言嵌入
HuggingFace all-MiniLM-L6-v2384开源小模型

生成向量嵌入

使用 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;

下一步

掌握向量搜索后,你可以继续学习: