PostgreSQL笔记49:向量检索核心算法、索引调优与过滤策略深度解析

纲要

  • pgvector 扩展
  • 向量检索核心算法
    • 暴力检索(Brute Force / Exact KNN)
    • 近似最近邻(ANN):HNSWIVFFlat
  • 过滤策略
    • Pre-filter(预过滤)
    • Post-filter(后过滤)
    • 迭代索引扫描(Iterative Index Scan,pgvector ≥ 0.8.0)
      • strict_order 模式
      • relaxed_order 模式
  • 索引关键参数
    • HNSWmef_constructionhnsw.ef_search
    • IVFFlatlistsivfflat.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 索引类型:HNSWIVFFlat

过滤策略:Pre-filter、Post-filter 与迭代扫描

在实际业务场景中,向量相似度检索通常需要与业务属性过滤条件(如 category_idstatus 等)组合使用。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 构建多层图结构,查询时在图结构中导航而非全表扫描。它不需要训练步骤,可以在空表上创建。

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 kWHERE 过滤条件组合使用时,PostgreSQL 优化器无法预知 满足过滤条件的记录是否足以填满 k 条结果。若实际满足条件的记录不足 k 条,优化器可能扫描整个索引直至确认无法找到更多匹配记录,导致性能骤降

sql 复制代码
-- 危险查询:若 category_id = 999 只有 5 条记录,优化器可能全索引扫描
SELECT * FROM items
WHERE category_id = 999
ORDER BY embedding <=> '[3,1,2]'
LIMIT 10;

解决方案

  1. 迭代索引扫描pgvector ≥ 0.8.0):自动扩大扫描范围直至获取足够结果
  2. 提高 ef_searchprobes:增大初始候选集规模
  3. 部分索引:为高频过滤条件单独建索引
  4. 组合索引:在过滤列上建立 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=100hnsw.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 解析次数。
  • 参数化查询:使用 PreparedStatementsetStringsetInt 传递参数。
  • 会话级参数通过 Statement.execute("SET ...") 设置。
  • 使用 try-with-resources 自动关闭 ResultSetStatementConnection
技术点总结
技术点 说明
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 优化器无法提前感知这一情况。

解决方案

  1. 迭代索引扫描pgvector ≥ 0.8.0):通过 hnsw.iterative_scan / ivfflat.iterative_scan 参数启用,自动扩大扫描范围直至获取足够结果。
  2. 提高 ef_search / probes:增大初始候选集规模,降低过滤后结果不足的概率。
  3. 部分索引:为高频过滤值单独建索引,缩小索引范围。
  4. 分区表:按过滤键分区,结合分区剪枝精确命中目标分区。

广度

涵盖暴力检索、HNSW、IVFFlat 三种检索算法,Pre-filter、Post-filter、迭代扫描三种过滤策略,以及降维、量化、部分索引、分区表、并行构建等多种优化手段,覆盖向量检索从算法到工程落地的全链路。

深度

深入剖析 HNSW 的 mef_constructionef_search 参数含义与调优逻辑,IVFFlat 的 listsprobes 权衡关系,以及迭代扫描的 strict_orderrelaxed_order 模式差异。

复杂度

涉及 PostgreSQL 索引机制、查询优化器行为、分区剪枝、并行构建、内存参数调优等多维度技术栈,需综合考量数据规模、读写负载、召回率要求与延迟预算。

官方文档

参考链接

总结

本文系统梳理了 pgvector 扩展中向量检索的核心技术体系,涵盖暴力检索与 ANN 索引的算法原理、HNSW 和 IVFFlat 的架构差异与关键参数调优、Pre-filter 与 Post-filter 过滤策略的取舍,以及 pgvector 0.8.0 版本引入的迭代索引扫描机制。

性能优化层面,从降维、量化、部分索引、分区表到并行构建与内存调优,提供了完整的工程化手段。在实际业务中,需根据数据规模、读写负载、召回率要求与延迟预算综合权衡,以 HNSW 为默认首选,在资源受限时考虑 IVFFlat,并结合迭代扫描与分区策略突破 Post-filter 的性能瓶颈。

相关推荐
疯狂打码的少年1 小时前
【数据结构】交换类排序:冒泡与快速排序
数据结构·笔记·算法·排序算法
chnyi6_ya2 小时前
VideoHallu 论文阅读笔记
论文阅读·笔记
Nil2082 小时前
leetcode 108有序数组转换为二叉搜索树
数据结构·算法·leetcode
高频因子挖掘机2 小时前
QuantDash 成交量单位统一实战:从“手”到“股”的跨市场量化数据清洗全流程
后端·算法·github
Wang's Blog2 小时前
PostgreSQL笔记51: 基于 pgvector 构建企业级智能问答系统
数据库·笔记·postgresql
Escalating_xu2 小时前
【C++ STL简介】从六大组件到容器、迭代器与算法协作
java·c++·算法
自小吃多3 小时前
Capture软件原理图添加差分属性笔记
笔记·嵌入式硬件
纪念 2293 小时前
算法二叉树(一)
算法
日常筹谋记3 小时前
ATS选不对运维两行泪:数据中心市电与柴发切换场景的双电源自动转换开关选型笔记
运维·数据库·笔记