RAG 知识库设计:pgvector 方案

RAG 知识库的存储层设计:为什么 MVP 阶段用 pgvector 而不是专用向量数据库、知识 schema 怎么分层、向量检索的索引选择、embedding 生成时机。

#type / concept #status / growing #tech / ai #resource / postgresql

[!info] related notes

RAG 知识库设计:pgvector 方案

这篇解决什么问题

RAG 系统需要一个地方存储知识文档的向量,以便语义检索。专用向量数据库(Milvus、Pinecone、Qdrant)功能强大,但引入新组件意味着额外的运维成本。对于 MVP 和中小规模场景,PostgreSQL + pgvector 可能是更务实的选择。

选型决策:pgvector vs 专用向量数据库

维度pgvector专用向量数据库
运维已有 PostgreSQL,零额外组件需要额外部署
事务ACID,与业务数据同库独立系统
查询可以 JOIN 业务表需要应用层拼接
性能(百万级)够用更优
性能(亿级)吃力专为此设计
混合查询天然支持(向量 + SQL 条件)需要特殊语法

决策原则:知识库规模 < 100 万条、需要 JOIN 业务数据、不想引入新组件 → pgvector。

知识 Schema 设计

知识库不应该是一个扁平的”文档+向量”表。按知识的来源和粒度分层:

knowledge_sources     # 来源(一个视频、一篇文章、一份指南)
  └─ knowledge_units     # 知识单元(一个完整的知识点)
       └─ knowledge_clips     # 片段(单元内的子片段,可定位到原文位置)

knowledge_sources

CREATE TABLE knowledge_sources (
    id SERIAL PRIMARY KEY,
    source_key VARCHAR(255) UNIQUE NOT NULL,  -- 唯一标识(用于去重)
    source_type VARCHAR(50) NOT NULL,          -- 'video' | 'article' | 'curated'
    title TEXT NOT NULL,
    author VARCHAR(255),
    problem_slug VARCHAR(100),                 -- 问题分类标签
    language VARCHAR(10),
    ingest_status VARCHAR(50) DEFAULT 'pending',
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW()
);

knowledge_units(核心表)

CREATE TABLE knowledge_units (
    id SERIAL PRIMARY KEY,
    source_id INTEGER REFERENCES knowledge_sources(id),
    unit_type VARCHAR(50) NOT NULL,   -- 知识类型
    title TEXT NOT NULL,
    summary TEXT,
    body_markdown TEXT NOT NULL,
    embedding vector(1536),           -- pgvector 向量列
    tags TEXT[],
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX ON knowledge_units USING ivfflat (embedding vector_cosine_ops);

unit_type 的价值:不只是存储,它让后续的意图感知重排成为可能。

unit_type内容用户问的时候
definition概念解释”什么是颈椎前伸”
self_check自测方法”怎么判断有没有”
exercise训练动作”怎么改善/练什么”
cause原因分析”为什么会这样”
warning风险禁忌”有什么要注意的”

向量检索查询

SELECT
    ku.id, ku.title, ku.summary, ku.body_markdown,
    ku.unit_type, ku.tags,
    ks.title as source_title,
    1 - (ku.embedding <=> $1::vector) as similarity
FROM knowledge_units ku
JOIN knowledge_sources ks ON ku.source_id = ks.id
ORDER BY ku.embedding <=> $1::vector
LIMIT $2
  • <=> 是 pgvector 的 cosine distance 运算符
  • 1 - distance = similarity(0~1)
  • 可以同时 JOIN 来源表获取元信息

索引选择

索引类型适用场景特点
ivfflat< 100 万条快速,需要先有数据后建索引
hnsw> 100 万条更精确,内存占用更大
CREATE INDEX ON knowledge_units
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

注意:ivfflat 索引需要在有数据后重建才生效。空表建索引没用。

Embedding 生成时机

入库时生成,不是检索时:

# 入库:为每个 unit 生成 embedding
embedding = await embedding_generator.generate(
    unit.title + "\n" + unit.summary + "\n" + unit.body_markdown
)

检索时只为查询文本生成一次 embedding:

# 检索:只为查询生成 embedding
query_embedding = await embedding_generator.generate(user_query)
# 然后用向量相似度搜索

常见错误

把所有内容塞进一个 unit

# ❌ 一个 10 分钟的视频就是一个 unit
# 检索出来的结果太长,LLM 无法聚焦

# ✅ 按知识点拆分
# "颈椎前伸的定义" 是一个 unit
# "颈椎前伸的自测方法" 是一个 unit
# "颈椎前伸的矫正动作" 是一个 unit

忽略 unit_type

# ❌ 所有 unit 都是同一个 type
# 无法做意图感知重排

# ✅ 标记 unit_type
# 用户问"怎么练"时,boost exercise 类型的结果

检索时才生成 embedding

# ❌ 检索时为库中所有文档生成 embedding
for doc in all_docs:
    doc.embedding = await generate(doc.text)  # 极慢

# ✅ 入库时生成,检索时只为查询生成
query_embedding = await generate(query)
创建于 2026/6/25 更新于 2026/6/25