纲要
pgvector扩展- 向量检索核心算法
- 暴力检索(Brute Force / Exact KNN)
- 近似最近邻(ANN):
HNSW与IVFFlat
- 过滤策略
- Pre-filter(预过滤)
- Post-filter(后过滤)
- 迭代索引扫描(Iterative Index Scan,
pgvector≥ 0.8.0)strict_order模式relaxed_order模式
- 索引关键参数
HNSW:m、ef_construction、hnsw.ef_searchIVFFlat:lists、ivfflat.probes
- 性能优化手段
- 降维(Dimensionality Reduction)
- 量化(Quantization):
halfvec类型 - 部分索引(Partial Index)
- 分区表(Partitioning)与分区剪枝(Partition Pruning)
- 并行创建索引(Parallel Index Build)
maintenance_work_mem调优
- 硬边界与限制
- 向量维度上限:
vector类型上限 2,000 维 topk返回上限:1,000 条- PostgreSQL 8KB 页面限制与 TOAST
- 向量维度上限:
向量检索核心算法
在 pgvector 扩展中,向量相似度检索主要沿着两条技术路径展开:暴力检索(Brute Force) 与近似最近邻(ANN)。
暴力检索(Exact KNN)
暴力检索将目标向量与数据库中的每一个向量逐一计算相似度距离,返回精确的 top-k 结果。其优势在于 100% 召回率 ,但检索时间复杂度为 O(n),随着数据量增大,检索速度呈线性下降。在 pgvector 中,未创建 ANN 索引时默认执行精确最近邻搜索。
sql
-- 精确最近邻搜索(无索引)
SELECT id, content, embedding <=> '[3,1,2]' AS distance
FROM items
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;
近似最近邻(ANN)
ANN 通过索引结构仅扫描部分候选向量,以牺牲少量召回率为代价换取数量级的检索加速。pgvector 原生支持两种 ANN 索引类型:HNSW 和 IVFFlat。
过滤策略:Pre-filter、Post-filter 与迭代扫描
在实际业务场景中,向量相似度检索通常需要与业务属性过滤条件(如 category_id、status 等)组合使用。pgvector 中处理过滤条件的方式直接决定了查询的性能与召回率。
Pre-filter(预过滤)
先按业务条件通过 B-tree 等索引缩小候选集,再对过滤后的结果进行向量检索。
sql
-- Pre-filter:先过滤 category_id,再计算向量距离
SELECT id, embedding <=> '[3,1,2]' AS distance
FROM items
WHERE category_id = 123
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;
优点 :过滤后数据量小,向量计算精确。缺点:当过滤条件选择率低(匹配行数少)时性能良好;但当过滤条件匹配大量数据时,向量索引无法被有效利用。
Post-filter(后过滤)
先通过 ANN 索引进行向量检索获取 top-k 候选,再应用业务条件过滤。
sql
-- Post-filter:先向量检索,再过滤 category_id
SELECT id, embedding <=> '[3,1,2]' AS distance
FROM items
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10; -- 应用程序层再过滤 category_id = 123
优点 :充分利用向量索引加速。缺点:若 top-k 候选中满足过滤条件的记录不足 k 条,则最终结果集可能为空或不完整。
迭代索引扫描(Iterative Index Scan)--- pgvector ≥ 0.8.0
pgvector 0.8.0 版本引入了迭代索引扫描机制,当初始 ANN 扫描返回的结果不满足查询条件时,自动扩大扫描范围直至获取足够的结果或达到配置上限。
sql
-- 启用 HNSW 迭代扫描(严格顺序模式)
SET hnsw.iterative_scan = 'strict_order';
-- 启用 IVFFlat 迭代扫描(宽松顺序模式)
SET ivfflat.iterative_scan = 'relaxed_order';
迭代扫描提供两种模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
strict_order |
严格保证结果按距离排序 | 对排序准确性要求高的场景 |
relaxed_order |
允许距离排序略有偏差,但更激进地扩大扫描范围 | 对召回率要求更高、可接受轻微排序偏差的场景 |
sql
-- 结合迭代扫描的过滤查询(pgvector ≥ 0.8.0)
BEGIN;
SET LOCAL hnsw.iterative_scan = 'strict_order';
SET LOCAL hnsw.ef_search = 200;
SELECT id, embedding <=> '[3,1,2]' AS distance
FROM items
WHERE category_id = 123
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;
COMMIT;
ANN 索引详解
HNSW(Hierarchical Navigable Small World)
HNSW 构建多层图结构,查询时在图结构中导航而非全表扫描。它不需要训练步骤,可以在空表上创建。
sql
-- 创建 HNSW 索引(余弦距离)
CREATE INDEX items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
HNSW 关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
m |
16 | 每层最大连接数。值越高召回率越高,但索引构建变慢、内存占用增大 |
ef_construction |
64 | 构建图时动态候选列表大小。值越高索引质量越好,但构建时间增加。须满足 ef_construction ≥ 2 * m |
hnsw.ef_search |
40 | 查询时动态候选列表大小(会话级参数)。值越高召回率越高,但查询变慢 |
sql
-- 调整查询时的 ef_search
SET hnsw.ef_search = 100;
-- 单查询级别调整
BEGIN;
SET LOCAL hnsw.ef_search = 200;
SELECT * FROM items ORDER BY embedding <=> '[3,1,2]' LIMIT 10;
COMMIT;
IVFFlat(Inverted File with Flat Compression)
IVFFlat 在索引构建时将向量划分为多个聚类(列表),查询时仅探测部分列表。必须先有数据才能创建索引,因为需要基于现有数据进行聚类训练。
sql
-- 创建 IVFFlat 索引(余弦距离,100 个列表)
CREATE INDEX items_embedding_ivfflat ON items
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
IVFFlat 关键参数:
| 参数 | 说明 | 调优建议 |
|---|---|---|
lists |
索引构建时的聚类数量 | 通常建议 lists = rows / 1000,如 100 万行数据设 lists = 1000 |
ivfflat.probes |
查询时探测的列表数量(会话级参数,默认 1) | 建议 probes = lists / 10。值越高召回率越高,查询越慢 |
sql
-- 调整查询时的 probes
SET ivfflat.probes = 10;
-- 单查询级别调整
BEGIN;
SET LOCAL ivfflat.probes = 20;
SELECT * FROM items ORDER BY embedding <=> '[3,1,2]' LIMIT 10;
COMMIT;
HNSW vs IVFFlat 对比
| 维度 | HNSW | IVFFlat |
|---|---|---|
| 构建时间 | 较长 | 较短 |
| 内存占用 | 较高 | 较低 |
| 查询性能(速度-召回率权衡) | 更优 | 较劣 |
| 是否需要现有数据 | 否(可在空表创建) | 是(需要数据训练聚类) |
| 数据增量处理 | 自动支持增量插入 | 数据变化后需重建索引以保证最佳召回率 |
HNSW 是大多数工作负载的推荐默认选项,当构建时间或内存成为瓶颈时考虑 IVFFlat。
性能优化手段
降维(Dimensionality Reduction)
将向量从高维度(如 768、1024)降至更低维度(如 256、384),可显著降低存储和内存占用,通常只会带来轻微的召回率损失。
量化(Quantization)
pgvector 0.7.0 引入了 halfvec 类型,使用 2 字节浮点数(float16)替代 4 字节浮点数(float32),存储空间减少 50%。
sql
-- 使用 halfvec 类型存储向量(pgvector ≥ 0.7.0)
CREATE TABLE items_half (
id SERIAL PRIMARY KEY,
embedding halfvec(768) -- 768 维,float16 精度
);
-- 在 halfvec 列上创建 HNSW 索引
CREATE INDEX items_half_embedding_hnsw ON items_half
USING hnsw (embedding halfvec_l2_ops)
WITH (m = 16, ef_construction = 64);
部分索引(Partial Index)
当查询仅针对特定条件(如特定 category_id)时,可创建部分向量索引,大幅缩小索引体积。
sql
-- 仅为 category_id = 123 的数据创建向量索引
CREATE INDEX items_category_123_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WHERE category_id = 123;
对比全量索引与部分索引的大小:
sql
-- 全量 HNSW 索引
CREATE INDEX items_embedding_hnsw_full ON items
USING hnsw (embedding vector_cosine_ops);
-- 部分 HNSW 索引(仅 category_id = 123)
CREATE INDEX items_embedding_hnsw_partial ON items
USING hnsw (embedding vector_cosine_ops)
WHERE category_id = 123;
-- 查看索引大小对比
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size
FROM pg_indexes
WHERE tablename = 'items';
分区表(Partitioning)与分区剪枝
通过 PostgreSQL 分区表将数据按分区键拆分,结合分区剪枝(Partition Pruning) 仅扫描相关分区,可显著提升查询效率。
sql
-- 按 category_id 创建分区表
CREATE TABLE items (
id SERIAL,
embedding vector(768),
category_id INT
) PARTITION BY LIST (category_id);
-- 创建各个分区
CREATE TABLE items_cat_1 PARTITION OF items FOR VALUES IN (1);
CREATE TABLE items_cat_2 PARTITION OF items FOR VALUES IN (2);
CREATE TABLE items_cat_3 PARTITION OF items FOR VALUES IN (3);
-- 在每个分区上独立创建向量索引
CREATE INDEX items_cat_1_embedding_hnsw ON items_cat_1
USING hnsw (embedding vector_cosine_ops);
-- 查询时自动进行分区剪枝
SELECT * FROM items
WHERE category_id = 1
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;
并行创建索引
通过调整 max_parallel_maintenance_workers 参数,利用并行能力加速大规模索引构建。
sql
-- 设置并行维护工作进程数
SET max_parallel_maintenance_workers = 7;
-- 创建索引(将利用并行)
CREATE INDEX CONCURRENTLY items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
maintenance_work_mem 调优
增大 maintenance_work_mem 可减少索引构建过程中的磁盘 I/O,显著提升构建速度:
sql
-- 会话级调整
SET maintenance_work_mem = '2GB';
-- 然后创建索引
CREATE INDEX items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops);
硬边界与限制
在使用 pgvector 进行向量检索时,必须正视以下硬性限制:
| 限制项 | 值 | 说明 |
|---|---|---|
vector 类型维度上限 |
2,000 维 | 在 vector 类型上建索引的上限 |
halfvec 类型维度上限 |
4,000 维 | pgvector ≥ 0.7.0 |
bit 类型维度上限 |
64,000 维 | 二进制向量 |
topk 返回上限 |
1,000 条 | 单次查询最多返回 1,000 条结果 |
| PostgreSQL 页面大小 | 8 KB | 影响 TOAST 存储策略 |
Top-k 与过滤条件的性能陷阱
当 ORDER BY ... LIMIT k 与 WHERE 过滤条件组合使用时,PostgreSQL 优化器无法预知 满足过滤条件的记录是否足以填满 k 条结果。若实际满足条件的记录不足 k 条,优化器可能扫描整个索引直至确认无法找到更多匹配记录,导致性能骤降。
sql
-- 危险查询:若 category_id = 999 只有 5 条记录,优化器可能全索引扫描
SELECT * FROM items
WHERE category_id = 999
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;
解决方案:
- 迭代索引扫描 (
pgvector≥ 0.8.0):自动扩大扫描范围直至获取足够结果 - 提高
ef_search或probes:增大初始候选集规模 - 部分索引:为高频过滤条件单独建索引
- 组合索引:在过滤列上建立 B-tree 索引配合向量索引
sql
-- 方案:组合索引(B-tree + 向量索引同时使用)
-- 先通过 B-tree 索引过滤,再通过向量索引排序
CREATE INDEX items_category_id_btree ON items (category_id);
-- 查询优化器可能选择:B-tree 索引过滤 + 向量距离计算
EXPLAIN SELECT * FROM items
WHERE category_id = 123
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;
API 速览
pgvector 扩展
所属库 :pgvector(PostgreSQL 扩展)
安装:
sql
CREATE EXTENSION vector;
核心数据类型
| 类型 | 说明 | 适用版本 |
|---|---|---|
vector(n) |
n 维浮点向量(float32),上限 2,000 维 | 所有版本 |
halfvec(n) |
n 维半精度向量(float16),上限 4,000 维 | ≥ 0.7.0 |
sparsevec |
稀疏向量,上限 1,000 个非零元素 | ≥ 0.7.0 |
bit(n) |
二进制向量,上限 64,000 维 | ≥ 0.7.0 |
距离函数与操作符
| 距离类型 | 操作符 | 操作符类 |
|---|---|---|
| 欧几里得距离(L2) | <-> |
vector_l2_ops |
| 余弦距离 | <=> |
vector_cosine_ops |
| 内积 | <#> |
vector_ip_ops |
| 曼哈顿距离(L1) | <+> |
vector_l1_ops |
sql
-- 余弦相似度查询(返回相似度分数 0-1)
SELECT id, 1 - (embedding <=> '[3,1,2]') AS similarity
FROM items
ORDER BY embedding <=> '[3,1,2]'
LIMIT 5;
索引创建 API
HNSW 索引:
sql
CREATE INDEX index_name ON table_name
USING hnsw (column_name operator_class)
WITH (m = int, ef_construction = int);
IVFFlat 索引:
sql
CREATE INDEX index_name ON table_name
USING ivfflat (column_name operator_class)
WITH (lists = int);
会话级查询参数
| 参数 | 默认值 | 适用索引 | 说明 |
|---|---|---|---|
hnsw.ef_search |
40 | HNSW | 查询时动态候选列表大小 |
ivfflat.probes |
1 | IVFFlat | 查询时探测的列表数量 |
hnsw.iterative_scan |
关闭 | HNSW | 迭代扫描模式(strict_order / relaxed_order) |
ivfflat.iterative_scan |
关闭 | IVFFlat | 迭代扫描模式(strict_order / relaxed_order) |
sql
-- 设置会话级参数
SET hnsw.ef_search = 100;
SET ivfflat.probes = 10;
-- 查看当前设置
SHOW hnsw.ef_search;
SHOW ivfflat.probes;
Demo 完整示例
以下是一个基于 Node.js + pg 驱动 + pgvector 的完整向量检索 Demo。
环境准备
bash
# 安装依赖
npm init -y
npm install pg dotenv
# 确保 PostgreSQL 已安装 pgvector 扩展
# psql -c "CREATE EXTENSION IF NOT EXISTS vector;"
完整代码
js
// index.js
const { Client } = require('pg');
require('dotenv').config();
const config = {
host: process.env.PGHOST || 'localhost',
port: process.env.PGPORT || 5432,
database: process.env.PGDATABASE || 'testdb',
user: process.env.PGUSER || 'postgres',
password: process.env.PGPASSWORD || 'postgres',
};
// 生成随机向量(模拟 embedding)
function randomVector(dim) {
const vec = [];
for (let i = 0; i < dim; i++) {
vec.push(Math.random());
}
return vec;
}
async function main() {
const client = new Client(config);
await client.connect();
console.log('Connected to PostgreSQL');
// 1. 创建扩展和表
await client.query(`CREATE EXTENSION IF NOT EXISTS vector;`);
await client.query(`
DROP TABLE IF EXISTS items CASCADE;
CREATE TABLE items (
id SERIAL PRIMARY KEY,
embedding VECTOR(128),
category_id INT,
created_at TIMESTAMP DEFAULT NOW()
);
`);
console.log('Table created');
// 2. 插入 10,000 条测试数据
console.log('Inserting 10,000 rows...');
const batchSize = 1000;
for (let i = 0; i < 10; i++) {
const values = [];
for (let j = 0; j < batchSize; j++) {
const vec = randomVector(128);
const category = Math.floor(Math.random() * 10) + 1;
values.push(`('[${vec.join(',')}]', ${category})`);
}
await client.query(`
INSERT INTO items (embedding, category_id)
VALUES ${values.join(', ')}
`);
}
console.log('Data inserted');
// 3. 创建 HNSW 索引
await client.query(`
CREATE INDEX items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
`);
console.log('HNSW index created');
// 4. 精确最近邻搜索(无索引)
const queryVec = randomVector(128);
const startExact = Date.now();
const resExact = await client.query(`
SELECT id, category_id,
embedding <=> $1 AS distance
FROM items
ORDER BY embedding <=> $1
LIMIT 5
`, [`[${queryVec.join(',')}]`]);
console.log(`Exact search (${Date.now() - startExact}ms):`, resExact.rows);
// 5. 带过滤条件的 ANN 搜索(使用迭代扫描,pgvector ≥ 0.8.0)
await client.query(`SET hnsw.ef_search = 100;`);
await client.query(`SET hnsw.iterative_scan = 'strict_order';`);
const startFiltered = Date.now();
const resFiltered = await client.query(`
SELECT id, category_id,
embedding <=> $1 AS distance
FROM items
WHERE category_id = $2
ORDER BY embedding <=> $1
LIMIT 5
`, [`[${queryVec.join(',')}]`, 5]);
console.log(`Filtered ANN search (${Date.now() - startFiltered}ms):`, resFiltered.rows);
// 6. 查看索引大小
const resIndexSize = await client.query(`
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'items'
AND indexname LIKE '%hnsw%';
`);
console.log('Index size:', resIndexSize.rows);
await client.end();
}
main().catch(console.error);
运行说明
bash
# 1. 启动 PostgreSQL(确保已安装 pgvector)
# 2. 创建数据库
createdb testdb
# 3. 运行 Demo
node index.js
技术点总结
| 技术点 | 说明 |
|---|---|
pgvector 扩展安装 |
CREATE EXTENSION vector |
VECTOR(n) 类型 |
存储 n 维浮点向量 |
| HNSW 索引创建 | USING hnsw (column vector_cosine_ops) WITH (m, ef_construction) |
| 余弦距离查询 | ORDER BY embedding <=> query_vec LIMIT k |
| 迭代扫描 | SET hnsw.iterative_scan = 'strict_order'(≥ 0.8.0) |
| 带过滤条件的向量检索 | WHERE category_id = ? ORDER BY embedding <=> ? LIMIT ? |
多语言示例
以下基于相同的 pgvector 向量检索场景,分别提供 Go、Python、Java 三种语言的完整可运行示例,每种示例均实现:创建 pgvector 扩展、建表、插入 10,000 条 128 维随机向量、创建 HNSW 索引、执行精确搜索和带过滤条件的 ANN 搜索(使用迭代扫描)。
Go 示例
完整代码
go
// main.go
package main
import (
"context"
"fmt"
"log"
"math/rand"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func randomVector(dim int) string {
vec := make([]string, dim)
for i := 0; i < dim; i++ {
vec[i] = fmt.Sprintf("%f", rand.Float64())
}
return "[" + strings.Join(vec, ",") + "]"
}
func main() {
rand.Seed(time.Now().UnixNano())
ctx := context.Background()
connStr := "postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable"
pool, err := pgxpool.New(ctx, connStr)
if err != nil {
log.Fatal("Unable to connect:", err)
}
defer pool.Close()
log.Println("Connected to PostgreSQL")
// 1. 创建扩展和表
_, err = pool.Exec(ctx, `CREATE EXTENSION IF NOT EXISTS vector;`)
if err != nil {
log.Fatal(err)
}
_, err = pool.Exec(ctx, `
DROP TABLE IF EXISTS items CASCADE;
CREATE TABLE items (
id SERIAL PRIMARY KEY,
embedding VECTOR(128),
category_id INT,
created_at TIMESTAMP DEFAULT NOW()
);
`)
if err != nil {
log.Fatal(err)
}
log.Println("Table created")
// 2. 插入 10,000 条数据
log.Println("Inserting 10,000 rows...")
batchSize := 1000
for i := 0; i < 10; i++ {
var values []string
for j := 0; j < batchSize; j++ {
vec := randomVector(128)
category := rand.Intn(10) + 1
values = append(values, fmt.Sprintf("('%s', %d)", vec, category))
}
sql := fmt.Sprintf("INSERT INTO items (embedding, category_id) VALUES %s", strings.Join(values, ", "))
_, err = pool.Exec(ctx, sql)
if err != nil {
log.Fatal(err)
}
}
log.Println("Data inserted")
// 3. 创建 HNSW 索引
_, err = pool.Exec(ctx, `
CREATE INDEX items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
`)
if err != nil {
log.Fatal(err)
}
log.Println("HNSW index created")
queryVec := randomVector(128)
// 4. 精确搜索
start := time.Now()
rows, err := pool.Query(ctx, `
SELECT id, category_id, embedding <=> $1 AS distance
FROM items
ORDER BY embedding <=> $1
LIMIT 5
`, queryVec)
if err != nil {
log.Fatal(err)
}
var results []struct{ ID int; Category int; Distance float64 }
for rows.Next() {
var r struct{ ID int; Category int; Distance float64 }
err = rows.Scan(&r.ID, &r.Category, &r.Distance)
if err != nil {
log.Fatal(err)
}
results = append(results, r)
}
rows.Close()
log.Printf("Exact search (%dms): %+v", time.Since(start).Milliseconds(), results)
// 5. 带过滤条件的 ANN 搜索(迭代扫描)
_, err = pool.Exec(ctx, `SET hnsw.ef_search = 100;`)
if err != nil {
log.Fatal(err)
}
_, err = pool.Exec(ctx, `SET hnsw.iterative_scan = 'strict_order';`)
if err != nil {
log.Fatal(err)
}
start = time.Now()
rows, err = pool.Query(ctx, `
SELECT id, category_id, embedding <=> $1 AS distance
FROM items
WHERE category_id = $2
ORDER BY embedding <=> $1
LIMIT 5
`, queryVec, 5)
if err != nil {
log.Fatal(err)
}
var filteredResults []struct{ ID int; Category int; Distance float64 }
for rows.Next() {
var r struct{ ID int; Category int; Distance float64 }
err = rows.Scan(&r.ID, &r.Category, &r.Distance)
if err != nil {
log.Fatal(err)
}
filteredResults = append(filteredResults, r)
}
rows.Close()
log.Printf("Filtered ANN search (%dms): %+v", time.Since(start).Milliseconds(), filteredResults)
// 6. 索引大小
rows, err = pool.Query(ctx, `
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'items' AND indexname LIKE '%hnsw%';
`)
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var name, size string
rows.Scan(&name, &size)
log.Printf("Index %s size: %s", name, size)
}
rows.Close()
}
运行说明
bash
# 1. 安装 Go 1.18+
# 2. 初始化模块
go mod init pgvector-demo
go get github.com/jackc/pgx/v5
# 3. 确保 PostgreSQL 已安装 pgvector,并创建数据库 testdb
createdb testdb
# 4. 运行
go run main.go
代码说明
- 使用
pgxpool连接池,提升并发性能。 randomVector生成 128 维随机浮点向量,格式为 PostgreSQL 数组字符串[0.1,0.2,...]。- 批量插入:每 1000 条为一批,减少网络往返。
- 精确搜索:通过
ORDER BY embedding <=> $1 LIMIT 5获得最近邻。 - 带过滤搜索:设置
hnsw.ef_search=100和hnsw.iterative_scan='strict_order',并添加WHERE category_id=$2条件。 - 使用
pg_relation_size查看索引占用空间。
技术点总结
| 技术点 | 说明 |
|---|---|
pgx/v5 驱动 |
高性能 PostgreSQL 驱动,支持 pgvector 类型 |
| 批量插入 | 减少网络 I/O,提升写入吞吐 |
| 参数化查询 | 使用 $1 占位符防止 SQL 注入 |
| 会话级参数设置 | 通过 SET 命令调整 ef_search 和迭代扫描模式 |
| 上下文管理 | 使用 context.Context 控制超时和取消 |
Python 示例
完整代码
python
# main.py
import asyncio
import asyncpg
import random
import time
async def random_vector(dim: int) -> str:
vec = [str(random.random()) for _ in range(dim)]
return "[" + ",".join(vec) + "]"
async def main():
conn = await asyncpg.connect(
host="localhost",
port=5432,
database="testdb",
user="postgres",
password="postgres"
)
print("Connected to PostgreSQL")
# 1. 创建扩展和表
await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;")
await conn.execute("""
DROP TABLE IF EXISTS items CASCADE;
CREATE TABLE items (
id SERIAL PRIMARY KEY,
embedding VECTOR(128),
category_id INT,
created_at TIMESTAMP DEFAULT NOW()
);
""")
print("Table created")
# 2. 插入 10,000 条数据
print("Inserting 10,000 rows...")
batch_size = 1000
for i in range(10):
values = []
for _ in range(batch_size):
vec = await random_vector(128)
category = random.randint(1, 10)
values.append(f"('{vec}', {category})")
query = f"INSERT INTO items (embedding, category_id) VALUES {', '.join(values)}"
await conn.execute(query)
print("Data inserted")
# 3. 创建 HNSW 索引
await conn.execute("""
CREATE INDEX items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
""")
print("HNSW index created")
query_vec = await random_vector(128)
# 4. 精确搜索
start = time.time()
rows = await conn.fetch("""
SELECT id, category_id, embedding <=> $1 AS distance
FROM items
ORDER BY embedding <=> $1
LIMIT 5
""", query_vec)
print(f"Exact search ({int((time.time()-start)*1000)}ms):", [dict(r) for r in rows])
# 5. 带过滤条件的 ANN 搜索(迭代扫描)
await conn.execute("SET hnsw.ef_search = 100;")
await conn.execute("SET hnsw.iterative_scan = 'strict_order';")
start = time.time()
rows = await conn.fetch("""
SELECT id, category_id, embedding <=> $1 AS distance
FROM items
WHERE category_id = $2
ORDER BY embedding <=> $1
LIMIT 5
""", query_vec, 5)
print(f"Filtered ANN search ({int((time.time()-start)*1000)}ms):", [dict(r) for r in rows])
# 6. 索引大小
rows = await conn.fetch("""
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'items' AND indexname LIKE '%hnsw%';
""")
for r in rows:
print(f"Index {r['indexname']} size: {r['size']}")
await conn.close()
if __name__ == "__main__":
random.seed(time.time())
asyncio.run(main())
运行说明
bash
# 1. 安装 Python 3.8+ 和依赖
pip install asyncpg
# 2. 确保 PostgreSQL 已安装 pgvector,并创建数据库 testdb
createdb testdb
# 3. 运行
python main.py
代码说明
- 使用
asyncpg异步驱动,支持高并发和 await 语法。 random_vector返回 PostgreSQL 数组字符串格式。- 批量插入:每 1000 条一批,使用
', '.join(values)构建 SQL。 - 参数化查询:
$1占位符传递向量字符串,$2传递 category_id。 - 会话级参数通过
conn.execute("SET ...")设置。 - 使用
fetch获取所有结果,dict(r)转为字典便于打印。
技术点总结
| 技术点 | 说明 |
|---|---|
asyncpg 驱动 |
纯 Python 异步驱动,性能优秀,支持 pgvector |
| 异步 I/O | 使用 asyncio 提升并发能力 |
| 批量插入优化 | 减少事务开销,提高写入效率 |
| 参数化查询 | 防止 SQL 注入,自动类型转换 |
| 迭代扫描启用 | 通过 SET 语句开启 strict_order 模式 |
Java 示例
完整代码
java
// Main.java
import java.sql.*;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public class Main {
private static String randomVector(int dim) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < dim; i++) {
if (i > 0) sb.append(",");
sb.append(ThreadLocalRandom.current().nextDouble());
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) throws Exception {
String url = "jdbc:postgresql://localhost:5432/testdb";
Properties props = new Properties();
props.setProperty("user", "postgres");
props.setProperty("password", "postgres");
try (Connection conn = DriverManager.getConnection(url, props)) {
System.out.println("Connected to PostgreSQL");
// 1. 创建扩展和表
try (Statement st = conn.createStatement()) {
st.execute("CREATE EXTENSION IF NOT EXISTS vector;");
st.execute("DROP TABLE IF EXISTS items CASCADE;");
st.execute("""
CREATE TABLE items (
id SERIAL PRIMARY KEY,
embedding VECTOR(128),
category_id INT,
created_at TIMESTAMP DEFAULT NOW()
);
""");
}
System.out.println("Table created");
// 2. 插入 10,000 条数据
System.out.println("Inserting 10,000 rows...");
int batchSize = 1000;
for (int i = 0; i < 10; i++) {
StringBuilder values = new StringBuilder();
for (int j = 0; j < batchSize; j++) {
if (j > 0) values.append(",");
String vec = randomVector(128);
int category = ThreadLocalRandom.current().nextInt(1, 11);
values.append("('").append(vec).append("', ").append(category).append(")");
}
try (Statement st = conn.createStatement()) {
st.executeUpdate("INSERT INTO items (embedding, category_id) VALUES " + values.toString());
}
}
System.out.println("Data inserted");
// 3. 创建 HNSW 索引
try (Statement st = conn.createStatement()) {
st.execute("""
CREATE INDEX items_embedding_hnsw ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
""");
}
System.out.println("HNSW index created");
String queryVec = randomVector(128);
// 4. 精确搜索
long start = System.currentTimeMillis();
try (PreparedStatement ps = conn.prepareStatement("""
SELECT id, category_id, embedding <=> ? AS distance
FROM items
ORDER BY embedding <=> ?
LIMIT 5
""")) {
ps.setString(1, queryVec);
ps.setString(2, queryVec);
try (ResultSet rs = ps.executeQuery()) {
List<String> results = new ArrayList<>();
while (rs.next()) {
results.add(String.format("id=%d, category=%d, distance=%f",
rs.getInt("id"), rs.getInt("category_id"), rs.getDouble("distance")));
}
System.out.println("Exact search (" + (System.currentTimeMillis() - start) + "ms): " + results);
}
}
// 5. 带过滤条件的 ANN 搜索(迭代扫描)
try (Statement st = conn.createStatement()) {
st.execute("SET hnsw.ef_search = 100;");
st.execute("SET hnsw.iterative_scan = 'strict_order';");
}
start = System.currentTimeMillis();
try (PreparedStatement ps = conn.prepareStatement("""
SELECT id, category_id, embedding <=> ? AS distance
FROM items
WHERE category_id = ?
ORDER BY embedding <=> ?
LIMIT 5
""")) {
ps.setString(1, queryVec);
ps.setInt(2, 5);
ps.setString(3, queryVec);
try (ResultSet rs = ps.executeQuery()) {
List<String> results = new ArrayList<>();
while (rs.next()) {
results.add(String.format("id=%d, category=%d, distance=%f",
rs.getInt("id"), rs.getInt("category_id"), rs.getDouble("distance")));
}
System.out.println("Filtered ANN search (" + (System.currentTimeMillis() - start) + "ms): " + results);
}
}
// 6. 索引大小
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("""
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'items' AND indexname LIKE '%hnsw%';
""")) {
while (rs.next()) {
System.out.println("Index " + rs.getString("indexname") + " size: " + rs.getString("size"));
}
}
}
}
}
运行说明
bash
# 1. 安装 JDK 11+ 和 Maven(或直接下载 postgresql JDBC 驱动)
# 2. 创建项目并添加依赖(若使用 Maven)
# <dependency>
# <groupId>org.postgresql</groupId>
# <artifactId>postgresql</artifactId>
# <version>42.7.3</version>
# </dependency>
# 3. 编译
javac -cp "postgresql-42.7.3.jar" Main.java
# 4. 确保 PostgreSQL 已安装 pgvector,并创建数据库 testdb
createdb testdb
# 5. 运行
java -cp ".:postgresql-42.7.3.jar" Main
代码说明
- 使用 JDBC 驱动(
org.postgresql),通过DriverManager获取连接。 randomVector返回[0.1,0.2,...]格式字符串。- 批量插入:每 1000 条构建一个大的
INSERT语句,减少 SQL 解析次数。 - 参数化查询:使用
PreparedStatement的setString和setInt传递参数。 - 会话级参数通过
Statement.execute("SET ...")设置。 - 使用
try-with-resources自动关闭ResultSet、Statement和Connection。
技术点总结
| 技术点 | 说明 |
|---|---|
| JDBC 驱动 | 标准 Java 数据库连接,支持 pgvector 类型(作为字符串传递) |
| PreparedStatement | 防止 SQL 注入,预编译提升性能 |
| 批量插入优化 | 使用多行 VALUES 减少网络往返 |
| 连接管理 | try-with-resources 确保资源正确释放 |
| 会话参数设置 | 通过 Statement 执行 SET 命令 |
多语言对比表格
| 维度 | Go | Python | Java |
|---|---|---|---|
| 驱动/库 | github.com/jackc/pgx/v5 |
asyncpg |
org.postgresql:postgresql (JDBC) |
| 连接方式 | 连接池 pgxpool |
单连接 asyncpg.connect |
DriverManager.getConnection |
| 异步支持 | 原生协程(goroutine) | asyncio |
阻塞式,可配合线程池 |
| 参数化占位符 | $1, $2 |
$1, $2 |
?(JDBC 标准) |
| 批量插入方式 | 循环 Exec 或拼接多行 VALUES |
拼接多行 VALUES | 拼接多行 VALUES |
| 会话参数设置 | pool.Exec(ctx, "SET ...") |
conn.execute("SET ...") |
st.execute("SET ...") |
| 结果集处理 | rows.Scan 按列读取 |
await conn.fetch 返回字典列表 |
ResultSet 遍历,按列名获取 |
| 错误处理 | 显式 if err != nil |
try/except(但示例未展示) |
抛出 SQLException,需捕获 |
| 连接池配置 | 内置 pgxpool,可配置最大连接数 |
需第三方库(如 asyncpg.pool) |
需连接池实现(如 HikariCP) |
| 事务支持 | pool.Begin(ctx) |
conn.transaction() |
conn.setAutoCommit(false) |
| 适用场景 | 高并发微服务、云原生 | 快速原型、数据科学、异步 API | 企业级后端、Spring 生态 |
| 性能特点 | 极高吞吐,低内存占用 | 优秀异步性能,适合 I/O 密集型 | 稳定成熟,通过连接池提高吞吐 |
以上三种实现均与 Node.js 示例保持一致的逻辑流程,开发者可根据自身技术栈选择合适的语言版本。所有示例均要求 PostgreSQL 已安装 pgvector 扩展,并已在 testdb 数据库中启用。
项目难点与解决方案
核心难点
Post-filter 导致的结果不完整问题:当 ANN 索引扫描的 top-k 候选集中,满足业务过滤条件的记录不足 k 条时,最终返回结果集会缺失,且 PostgreSQL 优化器无法提前感知这一情况。
解决方案
- 迭代索引扫描 (
pgvector≥ 0.8.0):通过hnsw.iterative_scan/ivfflat.iterative_scan参数启用,自动扩大扫描范围直至获取足够结果。 - 提高
ef_search/probes:增大初始候选集规模,降低过滤后结果不足的概率。 - 部分索引:为高频过滤值单独建索引,缩小索引范围。
- 分区表:按过滤键分区,结合分区剪枝精确命中目标分区。
广度
涵盖暴力检索、HNSW、IVFFlat 三种检索算法,Pre-filter、Post-filter、迭代扫描三种过滤策略,以及降维、量化、部分索引、分区表、并行构建等多种优化手段,覆盖向量检索从算法到工程落地的全链路。
深度
深入剖析 HNSW 的 m、ef_construction、ef_search 参数含义与调优逻辑,IVFFlat 的 lists 与 probes 权衡关系,以及迭代扫描的 strict_order 与 relaxed_order 模式差异。
复杂度
涉及 PostgreSQL 索引机制、查询优化器行为、分区剪枝、并行构建、内存参数调优等多维度技术栈,需综合考量数据规模、读写负载、召回率要求与延迟预算。
官方文档
参考链接
总结
本文系统梳理了 pgvector 扩展中向量检索的核心技术体系,涵盖暴力检索与 ANN 索引的算法原理、HNSW 和 IVFFlat 的架构差异与关键参数调优、Pre-filter 与 Post-filter 过滤策略的取舍,以及 pgvector 0.8.0 版本引入的迭代索引扫描机制。
性能优化层面,从降维、量化、部分索引、分区表到并行构建与内存调优,提供了完整的工程化手段。在实际业务中,需根据数据规模、读写负载、召回率要求与延迟预算综合权衡,以 HNSW 为默认首选,在资源受限时考虑 IVFFlat,并结合迭代扫描与分区策略突破 Post-filter 的性能瓶颈。