Supabase向量搜索 #

一、向量搜索概述 #

1.1 什么是向量搜索 #

text
向量搜索特点
├── 基于语义相似度搜索
├── 支持自然语言查询
├── 支持多模态搜索
├── 适合AI应用
└── 使用pgvector扩展

1.2 应用场景 #

场景 说明
语义搜索 基于含义搜索
推荐系统 相似内容推荐
问答系统 找到相似问题
图像搜索 相似图片查找
RAG 检索增强生成

二、启用pgvector #

2.1 启用扩展 #

sql
-- 启用pgvector扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 查看已安装扩展
SELECT * FROM pg_extension WHERE extname = 'vector';

2.2 创建向量表 #

sql
-- 创建带向量列的表
CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    embedding vector(1536),  -- OpenAI ada-002 维度
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 创建向量索引
CREATE INDEX idx_documents_embedding 
ON documents 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

三、嵌入向量 #

3.1 使用OpenAI生成嵌入 #

typescript
// Edge Function: generate-embedding
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const { content } = await req.json()

  // 调用OpenAI API
  const response = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'text-embedding-ada-002',
      input: content,
    }),
  })

  const data = await response.json()
  const embedding = data.data[0].embedding

  // 存储到数据库
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  const { error } = await supabase
    .from('documents')
    .insert({ content, embedding })

  return new Response(JSON.stringify({ embedding }), {
    headers: { 'Content-Type': 'application/json' },
  })
})

3.2 自动生成嵌入 #

sql
-- 创建触发器自动生成嵌入
-- 注意:需要在Edge Function中实现

四、相似度搜索 #

4.1 余弦相似度 #

sql
-- 查找最相似的文档
SELECT 
    id,
    content,
    1 - (embedding <=> '[0.1, 0.2, ...]'::vector) as similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;

4.2 搜索函数 #

sql
CREATE OR REPLACE FUNCTION search_documents(
    query_embedding vector(1536),
    match_threshold FLOAT DEFAULT 0.7,
    match_count INTEGER DEFAULT 10
)
RETURNS TABLE(
    id BIGINT,
    content TEXT,
    similarity FLOAT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        documents.id,
        documents.content,
        1 - (documents.embedding <=> query_embedding) as similarity
    FROM documents
    WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
    ORDER BY documents.embedding <=> query_embedding
    LIMIT match_count;
END;
$$ LANGUAGE plpgsql;

4.3 客户端调用 #

typescript
async function searchSimilarDocuments(query: string) {
  // 1. 生成查询向量
  const embedding = await generateEmbedding(query)

  // 2. 搜索相似文档
  const { data, error } = await supabase.rpc('search_documents', {
    query_embedding: embedding,
    match_threshold: 0.7,
    match_count: 10,
  })

  return data
}

五、向量索引 #

5.1 IVFFlat索引 #

sql
-- 创建IVFFlat索引
CREATE INDEX idx_documents_embedding 
ON documents 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- lists参数建议
-- 数据量 < 1000: lists = 10
-- 数据量 < 10000: lists = 50
-- 数据量 < 100000: lists = 100
-- 数据量 > 100000: lists = sqrt(行数)

5.2 HNSW索引 #

sql
-- 创建HNSW索引(更高性能)
CREATE INDEX idx_documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (
    m = 16,        -- 连接数
    ef_construction = 64  -- 构建时搜索范围
);

5.3 索引选择 #

text
索引选择建议
├── IVFFlat
│   ├── 构建快
│   ├── 内存占用小
│   └── 适合中等规模数据
│
└── HNSW
    ├── 查询快
    ├── 构建慢
    ├── 内存占用大
    └── 适合大规模数据

六、RAG应用 #

6.1 知识库表结构 #

sql
CREATE TABLE knowledge_base (
    id BIGSERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536),
    source TEXT,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_knowledge_embedding 
ON knowledge_base 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

6.2 RAG搜索函数 #

sql
CREATE OR REPLACE FUNCTION rag_search(
    query_embedding vector(1536),
    match_count INTEGER DEFAULT 5
)
RETURNS TABLE(
    id BIGINT,
    title TEXT,
    content TEXT,
    source TEXT,
    similarity FLOAT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        knowledge_base.id,
        knowledge_base.title,
        knowledge_base.content,
        knowledge_base.source,
        1 - (knowledge_base.embedding <=> query_embedding) as similarity
    FROM knowledge_base
    ORDER BY knowledge_base.embedding <=> query_embedding
    LIMIT match_count;
END;
$$ LANGUAGE plpgsql;

6.3 完整RAG流程 #

typescript
// Edge Function: rag-chat
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const { question } = await req.json()

  // 1. 生成问题向量
  const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'text-embedding-ada-002',
      input: question,
    }),
  })
  const embeddingData = await embeddingResponse.json()
  const queryEmbedding = embeddingData.data[0].embedding

  // 2. 搜索相关知识
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  const { data: contexts } = await supabase.rpc('rag_search', {
    query_embedding: queryEmbedding,
    match_count: 5,
  })

  // 3. 构建提示词
  const context = contexts?.map((c: any) => c.content).join('\n\n')

  // 4. 调用LLM生成回答
  const chatResponse = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'gpt-4-turbo-preview',
      messages: [
        {
          role: 'system',
          content: 'Based on the following context, answer the question.',
        },
        {
          role: 'user',
          content: `Context:\n${context}\n\nQuestion: ${question}`,
        },
      ],
    }),
  })

  const chatData = await chatResponse.json()
  const answer = chatData.choices[0].message.content

  return new Response(JSON.stringify({
    answer,
    sources: contexts?.map((c: any) => ({ id: c.id, title: c.title })),
  }), {
    headers: { 'Content-Type': 'application/json' },
  })
})

七、向量操作 #

7.1 向量运算 #

sql
-- 向量加法
SELECT '[1,2,3]'::vector + '[4,5,6]'::vector;

-- 向量减法
SELECT '[4,5,6]'::vector - '[1,2,3]'::vector;

-- 向量乘法
SELECT '[1,2,3]'::vector * 2;

-- 内积
SELECT '[1,2,3]'::vector <#> '[4,5,6]'::vector;

-- 余弦距离
SELECT '[1,2,3]'::vector <=> '[4,5,6]'::vector;

-- L2距离
SELECT '[1,2,3]'::vector <-> '[4,5,6]'::vector;

7.2 向量维度 #

sql
-- 获取向量维度
SELECT vector_dims('[1,2,3]'::vector);

-- 获取向量范数
SELECT vector_norm('[1,2,3]'::vector);

八、最佳实践 #

8.1 数据预处理 #

typescript
// 文本分块
function chunkText(text: string, maxTokens: number = 500): string[] {
  const sentences = text.split(/[.!?]+/)
  const chunks: string[] = []
  let currentChunk = ''

  for (const sentence of sentences) {
    if ((currentChunk + sentence).length > maxTokens) {
      if (currentChunk) chunks.push(currentChunk.trim())
      currentChunk = sentence
    } else {
      currentChunk += sentence + '.'
    }
  }

  if (currentChunk) chunks.push(currentChunk.trim())

  return chunks
}

8.2 性能优化 #

text
向量搜索优化建议
├── 使用合适的索引
├── 限制返回数量
├── 设置相似度阈值
├── 批量插入向量
└── 定期维护索引

九、总结 #

向量搜索要点:

操作 说明
扩展 CREATE EXTENSION vector
存储 vector(1536)
索引 ivfflat / hnsw
搜索 <=> 余弦距离
相似度 1 - (embedding <=> query)

恭喜你完成了Supabase完全指南的学习!

最后更新:2026-03-28