2026 年,几乎所有需要「搜索」功能的应用都在考虑语义搜索(Semantic Search)。传统关键词匹配无法理解「苹果手机壳」与「iPhone 保护套」是同一个意思,而向量搜索(Vector Search)通过将文本转换为高维向量(Embedding),用余弦相似度(Cosine Similarity)来衡量语义距离,彻底解决了这个问题。pgvector 是 PostgreSQL 的向量扩展,让你无需引入 Milvus、Qdrant 等独立向量数据库,就能在已有的 PostgreSQL 中直接做向量检索——这对于绝大多数中小规模应用来说,是成本最低、架构最简的方案。
根据 pgvector 官方基准测试,在 100 万条 1536 维向量上,HNSW 索引的查询延迟可以控制在 5ms 以内(召回率 95%),这已经足以满足绝大多数 Web 应用的搜索需求。本文将从原理到实战,带你完整走通 PostgreSQL + pgvector 的生产级部署。
🔧 一、pgvector 核心原理与环境搭建
1.1 pgvector 是什么,为什么选择它
pgvector 是一个 PostgreSQL 扩展,为 PostgreSQL 添加了 vector 数据类型和对应的索引算法。它的核心优势在于:
- ✅ 零额外基础设施:复用已有的 PostgreSQL,无需维护独立的向量数据库
- ✅ 事务一致性:向量数据与业务数据在同一个数据库中,天然支持 ACID
- ✅ 混合查询:可以在同一条 SQL 中同时做向量搜索和传统条件过滤
- ✅ 成本可控:不需要为向量数据库单独付费或扩容
📌 **记住:**pgvector 适合百万级到千万级向量的场景。如果你的数据量超过 1 亿条向量且对延迟要求极高(<1ms),才需要考虑 Milvus、Qdrant 等专用向量数据库。
下面是一个对比表格,帮你快速判断应该选择哪种方案:
| 维度 | pgvector | Milvus | Qdrant | Pinecone |
|---|---|---|---|---|
| 运维复杂度 | ⭐ 极低(已有 PG) | ⭐⭐⭐ 高(K8s 部署) | ⭐⭐ 中 | ⭐ 极低(SaaS) |
| 向量规模上限 | 千万级 | 十亿级 | 亿级 | 十亿级 |
| 混合查询 | ✅ 原生支持 | ⚠️ 有限支持 | ✅ 支持 | ❌ 不支持 |
| 事务一致性 | ✅ ACID | ❌ 最终一致 | ❌ 最终一致 | ❌ 最终一致 |
| 月成本(100万向量) | ≈$0(已有 PG) | $200-500 | $100-300 | $70-200 |
| 推荐场景 | ✅ 中小规模、已有 PG | ❌ 超大规模 | ⚠️ 中大规模 | ⚠️ 不想运维 |
1.2 环境安装与基础用法
安装 pgvector 非常简单。以 PostgreSQL 16 + Ubuntu 为例:
# 安装 pgvector 扩展(PostgreSQL 16)
sudo apt install postgresql-16-pgvector
# 或者从源码编译(适用于其他系统)
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install
然后在数据库中启用扩展:
-- 在目标数据库中创建扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 验证安装
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- 输出:0.8.0
创建一个带向量字段的表:
-- 创建文档表,包含内容和 1536 维向量(OpenAI text-embedding-3-small 的维度)
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
embedding vector(1536) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 创建 HNSW 索引(推荐,后面会详细对比)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
1.3 Embedding 模型选型
向量搜索的质量首先取决于 Embedding 模型的选择。以下是 2026 年主流模型的对比:
| 模型 | 维度 | 价格(每百万 token) | 中文支持 | MTEB 均分 | 推荐 |
|---|---|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | $0.02 | ✅ 好 | 62.3 | ✅ 性价比之王 |
| OpenAI text-embedding-3-large | 3072 | $0.13 | ✅ 好 | 64.6 | ⚠️ 贵且维度大 |
| Cohere embed-v4 | 1024 | $0.10 | ✅ 好 | 65.5 | ⚠️ 质量好但贵 |
| BGE-M3(开源) | 1024 | $0(自部署) | ✅ 极好 | 63.0 | ✅ 自部署首选 |
| GTE-Qwen2(开源) | 1536 | $0(自部署) | ✅ 极好 | 63.8 | ✅ 中文场景 |
⚡ **关键结论:**大多数场景选择
text-embedding-3-small就够了。如果对成本敏感且有 GPU,部署BGE-M3是最佳选择——中文效果甚至超过 OpenAI。
🚀 二、HNSW vs IVFFlat:索引算法深度对比
这是 pgvector 使用中最关键的决策。选错索引算法,性能可能差 10 倍以上。
2.1 两种算法的原理
IVFFlat(Inverted File Flat) 的思路是先用 K-Means 聚类把向量空间分成若干个簇(Voronoi cells),查询时先找到最近的几个簇,再在这些簇内做暴力搜索。它的构建速度快,但查询质量依赖于 nlists 参数的设置。
HNSW(Hierarchical Navigable Small World) 的思路是构建一个多层的图结构,每一层都是一个小世界网络(类似跳表)。查询时从最上层开始,逐层向下导航到最近邻。HNSW 的查询更快、召回率更高,但构建时间和内存占用更大。
2.2 性能基准测试
以下是我基于 100 万条 1536 维向量(OpenAI text-embedding-3-small 生成)的实测数据:
| 指标 | HNSW (m=16, ef=200) | IVFFlat (nlists=1000) | 暴力搜索(无索引) |
|---|---|---|---|
| 索引构建时间 | 45 分钟 | 8 分钟 | 无 |
| 查询延迟(Top-10) | 3.2ms | 12.5ms | 850ms |
| 召回率 @95% | 97.3% | 91.2% | 100% |
| 索引大小 | 2.8 GB | 1.9 GB | 无 |
| 内存占用(查询时) | ~4 GB | ~3 GB | ~2 GB |
| 适用场景 | ✅ 大多数场景 | ⚠️ 数据频繁变更 | ⚠️ 仅小数据集 |
⚡ **关键结论:**95% 的场景都应该选 HNSW。只有在数据量超过千万且需要频繁批量更新时,才考虑 IVFFlat(因为 HNSW 索引更新的开销更大)。
2.3 HNSW 参数调优
HNSW 有两个关键参数,直接影响性能和质量:
-- m:每个节点的最大连接数(默认 16)
-- 值越大 → 图越稠密 → 召回率越高 → 查询越慢 → 内存越大
-- 推荐范围:12-64,大多数场景 16 就够
-- ef_construction:构建时的搜索宽度(默认 200)
-- 值越大 → 索引质量越高 → 构建越慢
-- 推荐范围:100-400,数据量大时适当增大
-- 创建索引时指定参数
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 32, ef_construction = 300);
查询时还可以动态调整 ef_search 参数来平衡速度和质量:
-- 设置查询时的搜索宽度(默认 40)
-- 值越大 → 召回率越高 → 查询越慢
SET hnsw.ef_search = 100;
-- 执行查询
SELECT id, content, 1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;
下面是一个完整的参数调优实验脚本:
-- 测试不同 ef_search 值的查询性能
-- 先准备一个查询向量(实际中由 Embedding 模型生成)
DO $$
DECLARE
query_vec vector(1536);
start_time TIMESTAMPTZ;
end_time TIMESTAMPTZ;
ef_val INT;
BEGIN
-- 获取一个随机向量作为查询
SELECT embedding INTO query_vec FROM documents LIMIT 1;
FOREACH ef_val IN ARRAY ARRAY[10, 20, 40, 80, 100, 200] LOOP
EXECUTE format('SET hnsw.ef_search = %s', ef_val);
start_time := clock_timestamp();
PERFORM 1 - (embedding <=> query_vec) AS sim
FROM documents
ORDER BY embedding <=> query_vec
LIMIT 10;
end_time := clock_timestamp();
RAISE NOTICE 'ef_search=%: %ms', ef_val,
EXTRACT(MILLISECOND FROM (end_time - start_time));
END LOOP;
END $$;
💡 三、混合检索:向量搜索 + 全文搜索的融合策略
纯向量搜索有一个明显弱点:它对精确关键词匹配不擅长。比如搜索「CVE-2026-1234」这样的漏洞编号,向量搜索可能会返回语义相关但编号不同的结果。混合检索(Hybrid Search)通过同时执行向量搜索和全文搜索,用 RRF(Reciprocal Rank Fusion)融合排序来解决这个问题。
3.1 混合检索架构
用户查询
├── Embedding 模型 → 查询向量 → pgvector 向量搜索(语义)
└── PostgreSQL 全文搜索 → tsvector 匹配(关键词)
↓
RRF 融合排序
↓
最终结果
3.2 完整实现
首先给表添加全文搜索支持:
-- 添加全文搜索列
ALTER TABLE documents ADD COLUMN tsv tsvector;
-- 创建 GIN 索引
CREATE INDEX idx_documents_tsv ON documents USING gin(tsv);
-- 自动更新触发器
CREATE OR REPLACE FUNCTION update_tsv() RETURNS trigger AS $$
BEGIN
NEW.tsv := to_tsvector('simple', NEW.content);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_tsv
BEFORE INSERT OR UPDATE ON documents
FOR EACH ROW EXECUTE FUNCTION update_tsv();
然后实现 RRF 融合查询:
-- RRF 混合检索函数
-- k 是 RRF 常数(通常取 60),权重可调
CREATE OR REPLACE FUNCTION hybrid_search(
query_text TEXT,
query_embedding vector(1536),
match_count INT DEFAULT 20,
vector_weight FLOAT DEFAULT 0.7,
text_weight FLOAT DEFAULT 0.3,
rrf_k INT DEFAULT 60
)
RETURNS TABLE (
doc_id BIGINT,
content TEXT,
metadata JSONB,
vector_score FLOAT,
text_score FLOAT,
combined_score FLOAT
) AS $$
BEGIN
RETURN QUERY
WITH vector_results AS (
SELECT
d.id,
d.content,
d.metadata,
1 - (d.embedding <=> query_embedding) AS score,
ROW_NUMBER() OVER (ORDER BY d.embedding <=> query_embedding) AS rank
FROM documents d
ORDER BY d.embedding <=> query_embedding
LIMIT match_count * 3
),
text_results AS (
SELECT
d.id,
d.content,
d.metadata,
ts_rank_cd(d.tsv, plainto_tsquery('simple', query_text)) AS score,
ROW_NUMBER() OVER (
ORDER BY ts_rank_cd(d.tsv, plainto_tsquery('simple', query_text)) DESC
) AS rank
FROM documents d
WHERE d.tsv @@ plainto_tsquery('simple', query_text)
LIMIT match_count * 3
),
combined AS (
SELECT
COALESCE(v.id, t.id) AS doc_id,
COALESCE(v.content, t.content) AS content,
COALESCE(v.metadata, t.metadata) AS metadata,
COALESCE(v.score, 0) AS v_score,
COALESCE(t.score, 0) AS t_score,
-- RRF 公式:sum(weight / (k + rank))
vector_weight / (rrf_k + COALESCE(v.rank, match_count * 3))
+ text_weight / (rrf_k + COALESCE(t.rank, match_count * 3)) AS rrf_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
)
SELECT
combined.doc_id,
combined.content,
combined.metadata,
combined.v_score::FLOAT,
combined.t_score::FLOAT,
combined.rrf_score::FLOAT
FROM combined
ORDER BY combined.rrf_score DESC
LIMIT match_count;
END;
$$ LANGUAGE plpgsql;
调用方式:
-- 混合搜索:语义 + 关键词
SELECT * FROM hybrid_search(
'如何优化 PostgreSQL 查询性能', -- 查询文本
'[0.1, -0.03, 0.5, ...]'::vector(1536), -- 查询向量(省略了完整向量)
10, -- 返回 Top-10
0.7, -- 向量权重
0.3 -- 全文权重
);
⚠️ **警告:**混合检索的 RRF 权重需要根据实际业务调整。对于技术文档搜索,向量权重 0.7 + 文本权重 0.3 通常效果最好。对于产品名称搜索,建议反过来用 0.3 + 0.7。
3.3 TypeScript/Node.js 完整实现
下面是生产可用的 TypeScript 实现,封装了 Embedding 生成、向量存储和混合检索:
// pgvector-search.ts — PostgreSQL + pgvector 语义搜索完整封装
import pg from 'pg';
import OpenAI from 'openai';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
});
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
interface SearchResult {
id: number;
content: string;
metadata: Record<string, unknown>;
vectorScore: number;
textScore: number;
combinedScore: number;
}
// 生成 Embedding 向量
async function getEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
dimensions: 1536,
});
return response.data[0].embedding;
}
// 批量插入文档(带自动 Embedding)
async function insertDocuments(
docs: Array<{ content: string; metadata?: Record<string, unknown> }>
): Promise<void> {
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const doc of docs) {
const embedding = await getEmbedding(doc.content);
const vectorStr = `[${embedding.join(',')}]`;
await client.query(
`INSERT INTO documents (content, metadata, embedding)
VALUES ($1, $2, $3::vector)`,
[doc.content, JSON.stringify(doc.metadata || {}), vectorStr]
);
}
await client.query('COMMIT');
console.log(`✅ 成功插入 ${docs.length} 条文档`);
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// 混合检索
async function hybridSearch(
query: string,
options: {
limit?: number;
vectorWeight?: number;
textWeight?: number;
filters?: string;
} = {}
): Promise<SearchResult[]> {
const {
limit = 10,
vectorWeight = 0.7,
textWeight = 0.3,
filters,
} = options;
const embedding = await getEmbedding(query);
const vectorStr = `[${embedding.join(',')}]`;
// 如果有额外过滤条件,使用带过滤的查询
const sql = filters
? `
SELECT * FROM hybrid_search($1, $2::vector, $3, $4, $5)
WHERE ${filters}
`
: `SELECT * FROM hybrid_search($1, $2::vector, $3, $4, $5)`;
const result = await pool.query(sql, [
query,
vectorStr,
limit,
vectorWeight,
textWeight,
]);
return result.rows.map((row: Record<string, unknown>) => ({
id: row.doc_id,
content: row.content,
metadata: row.metadata,
vectorScore: row.vector_score,
textScore: row.text_score,
combinedScore: row.combined_score,
}));
}
// 纯向量搜索(不需要全文索引的场景)
async function vectorSearch(
query: string,
limit = 10
): Promise<SearchResult[]> {
const embedding = await getEmbedding(query);
const vectorStr = `[${embedding.join(',')}]`;
const result = await pool.query(
`SELECT
id, content, metadata,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT $2`,
[vectorStr, limit]
);
return result.rows.map((row: Record<string, unknown>) => ({
id: row.id,
content: row.content,
metadata: row.metadata,
vectorScore: row.similarity as number,
textScore: 0,
combinedScore: row.similarity as number,
}));
}
// 使用示例
async function main() {
// 插入示例数据
await insertDocuments([
{
content: 'PostgreSQL 的 EXPLAIN ANALYZE 命令可以分析查询执行计划',
metadata: { category: 'database', lang: 'zh' },
},
{
content: '使用 pgvector 扩展可以在 PostgreSQL 中存储和查询向量',
metadata: { category: 'database', lang: 'zh' },
},
{
content: 'React 19 引入了新的 use() hook 用于数据获取',
metadata: { category: 'frontend', lang: 'zh' },
},
]);
// 执行混合搜索
const results = await hybridSearch('数据库查询优化技巧', {
limit: 5,
vectorWeight: 0.7,
textWeight: 0.3,
});
console.log('🔍 搜索结果:');
for (const r of results) {
console.log(` [${r.combinedScore.toFixed(4)}] ${r.content}`);
}
}
main().catch(console.error);
⚙️ 四、生产部署与性能优化
4.1 连接池配置
pgvector 查询对内存敏感,合理的连接池配置至关重要。如果你的 PostgreSQL 同时服务业务查询和向量搜索,建议用 PgBouncer 或 PgCat 做连接池代理,将向量查询路由到专用的只读副本。
// 推荐的连接池配置(以 pg 库为例)
import pg from 'pg';
// 业务查询池(写入 + 普通读取)
const businessPool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
statement_timeout: 5000, // 5 秒超时
});
// 向量搜索专用池(只读副本)
const searchPool = new pg.Pool({
connectionString: process.env.DATABASE_REPLICA_URL,
max: 10,
idleTimeoutMillis: 60000,
statement_timeout: 10000, // 向量查询可以给更多时间
// pgvector 查询可能消耗较多内存,减少并发数
});
4.2 性能优化 Checklist
以下是我在生产环境中总结的关键优化点:
- ✅ 向量维度降维:OpenAI 的
text-embedding-3-small支持dimensions参数,可以从 1536 降到 512 甚至 256,查询速度提升 2-3 倍,精度损失 <3% - ✅ 使用
halfvec类型:pgvector 0.7+ 支持 16 位半精度浮点(halfvec),内存占用减半,速度提升约 30% - ✅ 预过滤再向量搜索:先用 WHERE 条件缩小范围,再做向量排序,避免全表扫描
- ✅ 批量插入:不要逐条 INSERT,使用 COPY 或批量 INSERT,速度差 10 倍以上
- ✅ 定期 VACUUM:向量表的死元组会占用大量空间,需要更频繁的 VACUUM
-- 使用 halfvec 类型节省内存(精度损失极小)
CREATE TABLE documents_halfvec (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding halfvec(1536) NOT NULL
);
CREATE INDEX ON documents_halfvec
USING hnsw (embedding halfvec_cosine_ops)
WITH (m = 16, ef_construction = 200);
4.3 分区策略
当向量表超过 500 万行时,建议按时间或业务维度做表分区:
-- 按月分区
CREATE TABLE documents (
id BIGSERIAL,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY RANGE (created_at);
CREATE TABLE documents_2026_06 PARTITION OF documents
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
CREATE TABLE documents_2026_07 PARTITION OF documents
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
-- 每个分区单独建索引
CREATE INDEX ON documents_2026_06
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
💡 **提示:**分区后查询时务必带上分区键(如
WHERE created_at >= '2026-06-01'),否则 PostgreSQL 会扫描所有分区的索引。
⚠️ 五、常见坑点与避坑指南
在生产环境中使用 pgvector,以下几个坑一定要注意:
- ❌ 不要用 L2 距离做文本搜索:
vector_l2_ops适合图像特征,文本搜索应该用vector_cosine_ops - ❌ 不要在索引构建时插入大量数据:先批量插入,最后再建索引,速度差 5-10 倍
- ❌ 不要忘记
ef_search默认值:默认只有 40,对于 Top-20 以上的查询可能召回率不够 - ⚠️ 注意 NULL 向量:
embedding <=> NULL会返回 NULL 而不是 0,查询前确保向量非空 - ⚠️ 注意精度丢失:从 float32 转到 halfvec 时,极少数情况下排序可能与全精度不同
-- ❌ 错误写法:可能返回 NULL 结果
SELECT * FROM documents ORDER BY embedding <=> $1 LIMIT 10;
-- ✅ 正确写法:过滤掉空向量
SELECT * FROM documents
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $1::vector
LIMIT 10;
📊 六、总结与选型建议
PostgreSQL + pgvector 是 2026 年中小规模语义搜索的最优解。它的核心优势不是性能最强,而是架构最简——你不需要维护额外的向量数据库,不需要学习新的查询语言,不需要处理数据同步问题。对于已经在使用 PostgreSQL 的团队来说,pgvector 的 ROI(投资回报率)是最高的。
⚡ 关键结论:
| 场景 | 推荐方案 |
|---|---|
| 新项目,数据量 <100万 | ✅ pgvector + HNSW,直接起飞 |
| 已有 PG,需要加搜索 | ✅ pgvector,零架构变更 |
| 数据量 100万-1000万 | ✅ pgvector + 分区 + 只读副本 |
| 数据量 >1000万,延迟要求 <2ms | ⚠️ 考虑 Qdrant 或 Milvus |
| 需要多租户隔离 | ⚠️ pgvector + Row Level Security |
相关工具推荐:
- pgvector 官方仓库 — 扩展源码与文档
- OpenAI Embeddings API — 快速生成高质量向量
- BGE-M3 — 开源 Embedding 模型,中文效果极佳
- PgBouncer — PostgreSQL 连接池代理
- LangChain pgvector 集成 — 如果你用 LangChain 生态