• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

从零搭建RAG系统:用PostgreSQL pgvector做语义搜索的完整指南

AI Coding 实测 admin 4小时前 10次浏览 已收录 扫描二维码

为什么你需要自己的RAG

过去半年,我试了各种RAG方案。Pinecone方便但贵,Weaviate功能全但运维重,Chroma轻量但生产环境差点意思。最后选了PostgreSQL pgvector——不是因为它最炫,是因为我的数据本来就在PostgreSQL里,多一个扩展就能做向量搜索,少维护一套系统。

RAG(Retrieval-Augmented Generation)说白了就是:用户问一个问题,你先去数据库搜相关文档,然后把文档+问题一起扔给LLM,让它基于搜到的内容回答。好处很明显——回答有依据、能引用来源、还能控制LLM不胡扯。

pgvector 是什么

pgvector 是 PostgreSQL 的向量相似度搜索扩展。你可以在普通的 PostgreSQL 表里加一个向量类型的列,然后做余弦相似度、L2距离、内积等计算。最关键的是——它支持IVFFlat和HNSW索引,几百万条数据也能秒级响应。

据 PostgreSQL 官方社区调查,pgvector 已是 PostgreSQL 生态中增长最快的扩展之一,GitHub 上超过 1.2 万星。

第一步:安装和配置

安装 pgvector

-- 如果你的 PostgreSQL 是 14+
CREATE EXTENSION vector;

在 Ubuntu 上:

sudo apt install postgresql-16-pgvector

Mac:

brew install pgvector

Docker:

# docker-compose.yml
services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"

创建表和向量字段

CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536)  -- OpenAI ada-002 维度
);

-- 创建 HNSW 索引(推荐,性能比 IVFFlat 好)
CREATE INDEX ON documents 
USING hnsw (embedding vector_cosine_ops);

第二步:生成向量

// embed.ts
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function getEmbedding(text: string): Promise<number[]> {
  const resp = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
    dimensions: 1536,
  });
  return resp.data[0].embedding;
}

// 批量处理文档
async function indexDocuments(docs: { title: string; content: string }[]) {
  for (const doc of docs) {
    const embedding = await getEmbedding(doc.title + '\n' + doc.content);
    await sql`
      INSERT INTO documents (title, content, embedding)
      VALUES (${doc.title}, ${doc.content}, ${embedding}::vector)
    `;
  }
}

用 text-embedding-3-small 而不是 ada-002 的原因:价格便宜5倍,性能相当。据 OpenAI 官方数据,text-embedding-3-small 在 MTEB benchmark 上得分 62.3%,略高于 ada-002 的 61.0%。

第三步:语义搜索

// search.ts
export async function searchDocuments(query: string, limit = 5) {
  const queryEmbedding = await getEmbedding(query);
  
  const results = await sql`
    SELECT 
      id, title, content,
      1 - (embedding <=> ${queryEmbedding}::vector) AS similarity
    FROM documents
    ORDER BY embedding <=> ${queryEmbedding}::vector
    LIMIT ${limit}
  `;
  
  return results;
}

结合关键词搜索

SELECT id, title, content,
  (1 - (embedding <=> ${queryEmbedding}::vector)) * 0.7 
  + (ts_rank(to_tsvector('english', content), plainto_tsquery('english', ${query}))) * 0.3 
  AS combined_score
FROM documents
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', ${query})
   OR 1 - (embedding <=> ${queryEmbedding}::vector) > 0.7
ORDER BY combined_score DESC
LIMIT 10;

第四步:接入LLM做RAG

// rag.ts
export async function askWithRAG(question: string) {
  // 1. 检索相关文档
  const docs = await searchDocuments(question, 3);
  
  // 2. 构建上下文
  const context = docs.map(d => 
    `【${d.title}】\n${d.content.slice(0, 1000)}`
  ).join('\n\n');
  
  // 3. 调用LLM
  const resp = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: `基于以下资料回答用户问题。如果资料不足,直接说不知道。\n\n资料:\n${context}` },
      { role: 'user', content: question }
    ],
  });
  
  return {
    answer: resp.choices[0].message.content,
    sources: docs.map(d => ({ title: d.title, similarity: d.similarity })),
  };
}

这里用 gpt-4o-mini 而不是 gpt-4o 的原因:RAG场景下,LLM的主要工作是总结和引用,不需要太强的推理能力。gpt-4o-mini 价格是 gpt-4o 的 1/30,效果在这个场景下足够了。

性能调优经验

踩过几个坑,直接说结论:

1. HNSW 的 ef_search 参数:默认值太小。搜索时设大一点:

SET hnsw.ef_search = 100;  -- 默认40,设到100召回率明显提升

2. 分批生成嵌入向量:OpenAI API 有 rate limit。用 p-limit 控制并发:

import pLimit from 'p-limit';
const limit = pLimit(10);
const embeddings = await Promise.all(docs.map(d => limit(() => getEmbedding(d))));

3. 别把全文存进向量:content 截取前 1000 字生成嵌入就行。太长的文本会让向量表达”平均化”,反而搜不准。

4. 定期 reindex:HNSW 虽然不需要训练,但大量写入后查询性能会下降。每周跑一次:

REINDEX INDEX documents_embedding_idx;

总结

这套方案我在三个项目里用了,数据量从几万到上百万条,查询都在 100ms 以内。成本方面,pgvector 是免费的,你只需要付 PostgreSQL 的服务器钱。对比 Pinecone 的起步价 70 刀/月,自建 pgvector 在 10 刀/月的 VPS 上就能跑得很舒服。

如果你的项目数据量在 500 万条以下,别折腾专门的向量数据库,pgvector 足够了。

下一步可以做的:加上文档分块策略(chunking strategy),用 LangChain 或者直接自己写分块逻辑。分块大小对搜索质量影响很大——这个下次再聊。

喜欢 (0)
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。