为什么你需要向量数据库?
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 文件。你需要:
- 用Python库(如 PyPDF2)提取文本。
- 用文本分割器(如 langchain 的 RecursiveCharacterTextSplitter)将长文本切成语义连贯的小块,比如每块500字符,重叠50字符。
- 对每一块文本,调用嵌入模型API(如OpenAI, Cohere,或本地部署的BGE、M3E等)将其转换为向量。
- 将 (文本块, 向量, 元数据) 插入到我们刚才创建的 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()
第二步:查询与回答。 当用户提问“这个产品如何保修?”时:
- 将用户问题用同样的嵌入模型转换为向量。
- 在数据库中执行向量相似度搜索,找到最相关的几个文本块。
- 将这些文本块作为“上下文”,和用户问题一起,构造一个提示词(Prompt),发送给大语言模型(如GPT-4、Claude或本地LLM)。
- 将大模型返回的答案呈现给用户。
这个流程的代码片段如下:
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…