PostgreSQL与pgvector实战:手把手教你搭建向量数据库,解锁RAG应用新场景[转载]

11 阅读12分钟

为什么你需要向量数据库?

RAG,全称是检索增强生成,它解决了一个大问题:如何让大模型“知道”它原本不知道的事情?比如,你想让一个AI客服回答你公司内部的产品手册内容,或者让一个AI助手帮你分析你个人笔记里的信息。大模型本身没“读过”你的手册或笔记,RAG就是那个给它“开小灶”的机制。

简单来说,RAG的工作流程分三步:第一步,把你私有的、非结构化的文档(比如PDF、Word、网页)切分成小块,并转换成一种叫“向量”的数学表示;第二步,把这些向量存起来,建立一个高效的“记忆库”;第三步,当用户提问时,把问题也转换成向量,然后去“记忆库”里快速找到最相关的几块内容,最后把这些内容作为背景信息,一起喂给大模型,让它生成精准的回答。

PostgreSQL变成向量数据库

整个流程的核心,就是第二步:存储和检索向量。这就是向量数据库 的用武之地。 PostgreSQL通过pgvector插件,就能变成一个向量数据库

pgvector的基本操作与向量搜索

pgvector引入了一个新的数据类型 vector,用来存储向量。你可以在建表时直接使用它。

假设我们要构建一个文档知识库,每段文档切片后,通过一个嵌入模型(比如OpenAI的 text-embedding-3-small)转换成一个1536维的向量。我们可以这样建表:

-- 创建一个存储文档块和对应向量的表
CREATE TABLE document_chunks (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL, -- 文档文本内容
    embedding vector(1536), -- 向量,维度是1536
    metadata JSONB, -- 可以存一些元信息,比如来源文件、页码等
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

注意这里: embedding vector(1536), -- 向量,维度是1536

插入一点数据:

-- 插入示例数据。这里的向量值是随便写的,仅作演示。
INSERT INTO document_chunks (content, embedding) VALUES
('PostgreSQL是一个功能强大的开源关系数据库。', '[0.1, 0.2, 0.3, ...]'), -- 这里省略了1536个数字
('pgvector扩展为PostgreSQL增加了向量相似性搜索能力。', '[0.4, 0.5, 0.6, ...]'),
('RAG应用利用向量数据库检索相关信息来增强大模型生成。', '[0.7, 0.8, 0.9, ...]');

当用户提问“什么是pgvector?”时,我们同样把这个问题转换成向量(假设是 [0.42, 0.51, 0.59, ...]),然后去表里找最相似的向量。pgvector提供了几种操作符和函数,最常用的是 <=>(余弦距离运算符)和 <->(欧氏距离或内积距离,取决于索引类型)。

-- 使用余弦相似度搜索最匹配的3个文档块
SELECT
   id,
   content,
   1 - (embedding <=> '[0.42, 0.51, 0.59, ...]') AS cosine_similarity -- <=> 返回余弦距离,1-距离=相似度
FROM document_chunks
ORDER BY embedding <=> '[0.42, 0.51, 0.59, ...]' -- 按距离从小到大排序,越小的越相似
LIMIT 3;

这个查询会返回与问题向量余弦距离最小的三条记录,也就是最相关的内容。你可以把返回的 content 字段拼接起来,作为上下文喂给大模型,让它生成答案。

但是,如果你的表里有几百万甚至上亿条向量,这种全表扫描的线性查找会慢得无法接受。这时候,就必须请出索引了。pgvector支持几种索引类型来加速搜索,最常用的是 ivfflat 索引(基于倒排文件)。创建索引的语句也很直观:

-- 在embedding列上创建IVFFLAT索引,用于加速余弦相似度搜索
CREATE INDEX ON document_chunks USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100); -- lists参数需要根据你的数据量调整,通常取 sqrt(行数)

创建索引会花费一些时间,但创建完成后,上面的 ORDER BY ... LIMIT 查询就会利用索引,速度提升几个数量级。这里有个小经验:索引最好在你有一定量的数据(比如几万条)之后再创建,并且 lists 参数需要根据数据分布和数量进行调整,以达到精度和速度的平衡。

实战RAG场景:构建你的第一个智能问答助手

理论说再多,不如动手做一个。我们来勾勒一个最简单的RAG应用流程,把前面学的串起来。这个应用的目标是:基于一份产品说明书,回答用户的问题。

第一步:知识库入库。  假设你有一个 product_manual.pdf 文件。你需要:

  1. 用Python库(如 PyPDF2)提取文本。
  2. 用文本分割器(如 langchain 的 RecursiveCharacterTextSplitter)将长文本切成语义连贯的小块,比如每块500字符,重叠50字符。
  3. 对每一块文本,调用嵌入模型API(如OpenAI, Cohere,或本地部署的BGE、M3E等)将其转换为向量。
  4. 将 (文本块, 向量, 元数据) 插入到我们刚才创建的 document_chunks 表中。

Python代码片段示意核心步骤:

import psycopg2
from openai import OpenAI # 或其他嵌入模型客户端
 
# 1. 连接数据库
conn = psycopg2.connect(host="你的服务器", dbname="vector_db", user="my_vector_user", password="你的密码")
cur = conn.cursor()
 
# 2. 假设我们已经有了文本块列表 text_chunks
for chunk in text_chunks:
    # 3. 调用嵌入模型获取向量
    response = client.embeddings.create(model="text-embedding-3-small", input=chunk)
    embedding_vector = response.data[0].embedding # 这是一个浮点数列表
 
    # 4. 插入数据库。注意:pgvector的Python驱动(如psycopg2)可以直接接受列表作为vector类型。
    cur.execute(
        "INSERT INTO document_chunks (content, embedding) VALUES (%s, %s)",
        (chunk, embedding_vector)
    )
 
conn.commit()
cur.close()
conn.close()

第二步:查询与回答。  当用户提问“这个产品如何保修?”时:

  1. 将用户问题用同样的嵌入模型转换为向量。
  2. 在数据库中执行向量相似度搜索,找到最相关的几个文本块。
  3. 将这些文本块作为“上下文”,和用户问题一起,构造一个提示词(Prompt),发送给大语言模型(如GPT-4、Claude或本地LLM)。
  4. 将大模型返回的答案呈现给用户。

这个流程的代码片段如下:

def answer_question(question: str):
    # 1. 将问题转换为向量
    response = client.embeddings.create(model="text-embedding-3-small", input=question)
    question_embedding = response.data[0].embedding
 
    # 2. 向量搜索
    cur.execute("""
        SELECT content FROM document_chunks
        ORDER BY embedding <=> %s
        LIMIT 5
    """, (question_embedding,))
    relevant_chunks = [row[0] for row in cur.fetchall()]
 
    # 3. 构造Prompt
    context = "\n\n".join(relevant_chunks)
    prompt = f"""基于以下产品说明书片段,请回答问题。
    
    说明书内容:
    {context}
    
    问题:{question}
    
    答案:"""
 
    # 4. 调用LLM生成答案
    llm_response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )
    return llm_response.choices[0].message.content

看,一个最核心的RAG流程就实现了。当然,真实的工业级应用会比这复杂得多,需要考虑分块策略、元数据过滤、重排序、对话历史管理等等。但万变不离其宗,核心就是 “向量化存储 -> 相似度检索 -> 上下文增强生成” 这个三板斧。用PostgreSQL + pgvector,你可以在一个熟悉的关系型数据库环境里,稳稳地实现前两步,这让整个技术栈的维护和调试都变得简单直接。

性能调优与避坑指南

用了一段时间后,你可能会关心:我的向量搜索够快吗?数据量大了怎么办?这里分享一些我踩过坑后总结的经验。

关于索引:

  • ivfflat 索引不是银弹。它在创建时需要指定 lists 参数。这个参数本质上是将向量空间分成多少个聚类中心。lists 越大,搜索精度越高,但创建索引越慢,索引体积也越大。一个常见的启发式设置是 lists = sqrt(行数)。对于100万行数据,可以试试 lists = 1000。
  • 索引是在已有数据上建立的。如果你后续会持续插入大量新数据,索引的效率可能会下降,因为新数据可能不属于之前划分的任何聚类。这时,你需要定期 REINDEX 或者使用 lists 参数更大的索引。pgvector的维护者也在持续优化索引算法,关注新版本特性。
  • 除了 ivfflat,对于超高维(比如超过2000维)或海量数据(十亿级),可以关注一下 hnsw 索引,它在某些场景下性能和精度平衡得更好,但创建速度更慢,索引更大。

关于查询:

  • 搜索时,你可以通过 SET ivfflat.probes = 10; 这样的会话级命令来动态调整 probes 参数。它控制搜索时检查的聚类列表数量。增加 probes 能提高精度但降低速度,反之亦然。这是一个在速度和召回率之间做权衡的利器。
  • 如果你的查询总是结合向量相似度和一些元数据过滤(比如 WHERE category = 'manual' AND embedding <=> ...),考虑创建复合索引或者先过滤再搜索,取决于你的数据分布。

关于运维:

  • 备份恢复: 因为pgvector是扩展,你的备份流程(比如 pg_dump)需要确保在恢复时,目标数据库已经安装了pgvector扩展,否则恢复包含 vector 类型数据的表会失败。通常的做法是先在新环境创建扩展,再恢复数据。
  • 监控: 像监控普通PostgreSQL一样监控它。关注连接数、CPU、内存,特别是向量索引扫描相关的性能指标。向量搜索是计算密集型操作,可能会消耗较多CPU。
  • 版本升级: 升级PostgreSQL主版本(如14升16)时,pgvector扩展可能需要重新编译安装。务必在测试环境充分验证。 我遇到的一个典型“坑”是,早期数据量少的时候没建索引,查询飞快。数据涨到几十万后,一个查询要好几秒,这才意识到问题。所以,如果你的数据量预期会增长,尽早规划并创建合适的索引,别等到用户体验变差了再补救。另一个坑是,不同嵌入模型生成的向量维度不同,建表时 vector(维度) 一定要写对,否则插入会失败。最好在你的应用里,把维度作为一个配置项,和使用的嵌入模型强关联。

总的来说,把PostgreSQL当成向量数据库来用,在中小规模数据量(百万到千万级向量)下,性能完全够用,而且带来的架构简化收益是巨大的。它让你可以继续使用熟悉的SQL工具链、事务、JOIN操作(是的,你可以把向量表和用户表关联起来!),去实现更复杂的AI应用逻辑。

文字转向量

"介绍下baai/bge-large-zh..."点击查看元宝的回答yb.tencent.com/s/rE9bqgIBD…

sentence-transformers库(使用最简单)

库会自动处理分词、编码和池化等步骤。

# 安装库
# pip install sentence-transformers

from sentence_transformers import SentenceTransformer

# 加载模型
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')

# 准备文本(支持单条字符串或字符串列表)
sentences = ["今天天气真好", "北京是中国的首都"]

# 生成向量
# normalize_embeddings=True 对结果进行归一化,通常能提升相似度计算效果
embeddings = model.encode(sentences, normalize_embeddings=True)

print(f"向量维度:{embeddings.shape}")  # 例如 (2, 1024)
print(f"第一条文本的向量:{embeddings[0][:5]}...")  # 查看前5个维度

FlagEmbedding库(官方推荐)

BGE模型的原生库,提供更多针对检索任务的优化选项。

# 安装库
# pip install FlagEmbedding

from FlagEmbedding import FlagModel

# 加载模型
model = FlagModel('BAAI/bge-large-zh-v1.5',
                  query_instruction_for_retrieval="为这个句子生成表示以用于检索相关文章:",
                  use_fp16=True) # 使用FP16加速,需要GPU支持

# 编码查询和文档(对于非对称检索任务,如问答,建议为查询添加指令)
query = ["如何学习人工智能?"]
documents = ["机器学习是人工智能的一个分支。", "深度学习需要大量的数据和算力。"]

# 为查询生成向量(会自动添加指令)
query_embeddings = model.encode_queries(query)
# 为文档生成向量
doc_embeddings = model.encode(documents)

print(query_embeddings.shape, doc_embeddings.shape)

使用 transformers库 (更底层,更灵活)

这种方式需要手动处理分词和池化。

# 安装库
# pip install transformers torch

from transformers import AutoTokenizer, AutoModel
import torch
import torch.nn.functional as F

# 加载模型和分词器
model_name = 'BAAI/bge-large-zh-v1.5'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# 准备文本
sentences = ["今天天气真好", "北京是中国的首都"]
inputs = tokenizer(sentences, padding=True, truncation=True, max_length=512, return_tensors='pt')

# 模型推理
with torch.no_grad():
    outputs = model(**inputs)
    # 取[CLS] token的向量作为句子表示,并进行L2归一化
    embeddings = outputs.last_hidden_state[:, 0]
    embeddings = F.normalize(embeddings, p=2, dim=1)

print(embeddings.shape)  # torch.Size([2, 1024])

其他注意事项

  • 指令前缀:在进行检索任务(如问答)时,为查询语句添加指令前缀(如“为这个句子生成表示以用于检索相关文章:”)可以显著提升效果。FlagEmbedding库和 sentence-transformers的最新版本通常会自动处理这一点。
  • 向量归一化:计算余弦相似度前,务必对生成的向量进行L2归一化(normalize_embeddings=True),这是保证相似度计算准确性的标准做法
  • 文本长度:模型最大支持512个token的输入,超长文本会被自动截断。对于长文档,常见的处理方式是先分段,再对每段的向量取平均或使用其他池化策略
  • 性能:如果拥有支持CUDA的GPU,启用 use_fp16=True可以大幅提升编码速度并减少显存占用
  • 对于大多数应用场景,推荐使用 sentence-transformers,其接口最为简洁。如果您构建的是检索系统,可以关注 FlagEmbedding库对查询和文档的非对称编码优化。模型文件首次加载时会从Hugging Face下载,请确保网络通畅。

主要内容转自: blog.csdn.net/weixin_2922…