一、 pgvector与toast设置

二、 HNSW索引

1. 索引选项

指定 HNSW 索引的参数:

  • m - 控制 HNSW 图索引中每个节点(向量)在每一层的最大连接数。(默认为 16

  • 作用

    • 值越大,图的连通性越强,搜索路径更多,召回率(Recall)更高。

    • 但会增加索引的内存占用和构建时间。

  • 想象 HNSW 是一张多层的社交网络:

    • m=16 表示每个人(向量)最多和 16 个朋友直接联系。

    • 朋友越多,找人时越容易通过朋友链找到目标,但维护朋友关系也更费内存和时间。

  • ef_construction - 在构建索引时,动态保留的候选向量数量(默认为 64

  • 作用

    • 值越大,构建的图质量越高,搜索精度更高,但索引构建速度越慢。

    • 影响索引的“粗调”阶段,决定如何连接节点。

  • 假设你要建一个城市地图(HNSW 索引):

    • ef_construction=64 表示画地图时,每次会考虑 64 个可能的路口来规划最优路线。

    • 考虑的路口越多,地图越精确,但画地图的时间也越长。

 

使用方法

CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);

2. 查询选项

  • ef_search :指定搜索时的动态候选列表大小(默认为 40
  • 作用

    • 值越大,搜索时考察的潜在相似向量越多,召回率越高,但查询速度越慢。

    • 仅影响查询阶段,与索引构建无关。

  • 想象你在图书馆(HNSW 索引)找一本书:

    • ef_search=40 表示你会先查看 40 本可能相关的书,再挑出最匹配的。

    • 检查的书越多(ef_search 越大),找到理想书的概率越高,但耗时也更长。

使用方法

SET hnsw.ef_search = 100;

如需在单个查询中临时设置,可在事务内使用 SET LOCAL

BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ...
COMMIT;

3. 加速索引构建

① 内存

如果图结构能完全放入 maintenance_work_mem,索引构建速度会显著提升:

SET maintenance_work_mem = '8GB';

如果内存不足,会显示提示:

NOTICE: HNSW 图结构在 100000 条元组后无法完全放入 maintenance_work_mem DETAIL: 构建时间将显著增加。

HINT: 增加 maintenance_work_mem 可加速构建。

注意:不要将maintenance_work_mem设置过高,以免耗尽服务器内存。与其他索引类型一样,初始数据加载完成后再创建索引会更快。

② 并行

还可通过增加并行工作线程数(默认为2)加速索引创建:

SET max_parallel_maintenance_workers = 7; -- 包含主线程

若工作线程数较多,可能需要调整 max_parallel_workers(默认为 8)。

③ 索引参数

如 m 和 ef_construction,也会显著影响构建时间(若无召回率问题,建议使用默认值)。

4. 索引构建进度

查看进度:

SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" 
FROM pg_stat_progress_create_index;

HNSW 索引的构建阶段(phase)包括:

  • initializing(初始化)
  • loading tuples(加载数据元组)

三、 IVFFlat索引

1. 查询选项

probes:控制搜索时需要检查的聚类数量(默认为 1

作用

  • 值越大,召回率(Recall)越高,但查询速度越慢
  • 若设置为聚类数(lists),则退化为精确最近邻搜索(此时规划器将不再使用索引)

类比

  • 将 IVFFlat 索引比作一个图书馆:

    • 聚类(lists:图书馆的书按主题分到不同区域(如“科幻区”“历史区”)

    • probes:你要搜索“科幻小说”时,决定检查几个主题区

参数影响

  • probes=1:只检查最相关的 1 个区(如“科幻区”),最快但可能漏掉其他区的书

  • probes=10:检查 10 个相关区(如“科幻区”“奇幻区”等),找到更多好书但速度变慢

  • probes=所有区:相当于全馆搜索,结果最全但和最慢

使用建议

  • 精度优先:增大 probes(如 10

  • 速度优先:减小 probes(如 1~3

SET ivfflat.probes = 10;

单次查询临时设置(使用事务内 SET LOCAL):

BEGIN;
SET LOCAL ivfflat.probes = 10;
SELECT ...;
COMMIT;


2. 加速索引构建

 可通过增加并行工作线程数(默认为2)加速索引创建:

SET max_parallel_maintenance_workers = 7; -- 包含主线程

若工作线程数较多,可能需要调整 max_parallel_workers(默认为 8)。

3. 索引构建进度

查看进度

SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" 
FROM pg_stat_progress_create_index;

IVFFlat 构建阶段

  • initializing(初始化)

  • performing k-means(执行 K-Means 聚类)

  • assigning tuples(分配元组)

  • loading tuples(加载元组),百分比(%)仅在 loading tuples 阶段显示。

四、 迭代式索引扫描(Iterative Index Scans)

自动扩展索引扫描范围,直至找到足够结果或达到 max_scan_tuples/max_probes 限制。

1. 模式选择

  • 严格排序(Strict Order):确保结果按距离精确排序
SET hnsw.iterative_scan = strict_order;
  • 宽松排序(Relaxed Order):允许轻微顺序偏差以提升召回率
SET hnsw.iterative_scan = relaxed_order;
-- 或
SET ivfflat.iterative_scan = relaxed_order;

2. 迭代扫描控制参数

HNSW
  • hnsw.max_scan_tuples:最大扫描元组数 (默认为 20000):

  • hnsw.scan_mem_multiplier:内存使用倍数(基于 work_mem,默认为 1):

    提示:若增大 max_scan_tuples 未提升召回率,可尝试调整此参数。

IVFFlat
  • ivfflat.max_probes:最大探测数:

    注意:若此值小于 ivfflat.probes,则以 probes 为准。

五、Q&A

1. 单表最多能存储多少向量?
  • 默认限制:PostgreSQL 非分区表默认支持 32 TB 数据,分区表可扩展至数千个同等大小的分区。


2. 是否支持数据复制?
  • 支持:pgvector 通过预写日志(WAL)实现,兼容主从复制和时间点恢复(PITR)。


3. 如何索引超过 2,000 维的向量?
  • 半精度索引(half-precision):支持最高 4,000 维。

  • 二进制量化(binary quantization):支持最高 64,000 维。
  • 降维处理:如 PCA 降低维度后再索引。

4. 同一列能否存储不同维度的向量?

存储:可使用无维度声明的 vector 类型:

CREATE TABLE embeddings (
    model_id bigint,
    item_id bigint,
    embedding vector,
    PRIMARY KEY (model_id, item_id)
);

索引限制:需对固定维度的子集创建索引(通过表达式索引+条件过滤):

CREATE INDEX ON embeddings USING hnsw ((embedding::vector(3)) vector_l2_ops) WHERE (model_id = 123);

查询示例

SELECT * FROM embeddings 
    WHERE model_id = 123 
    ORDER BY embedding::vector(3) <-> '[3,1,2]' 
    LIMIT 5;

5. 如何存储更高精度的向量?
  • 方法:使用 double precision[] 或 numeric[] 类型:

    CREATE TABLE items (
        id bigserial PRIMARY KEY,
        embedding double precision[]
    );
    -- 插入数据(Postgres 数组用 {})
    INSERT INTO items (embedding) VALUES ('{1,2,3}'), ('{4,5,6}');
  • 可选约束:确保数据可转换为向量且维度正确:

    ALTER TABLE items ADD CHECK (vector_dims(embedding::vector) = 3);
  • 表达式索引:以较低精度创建索引:

    CREATE INDEX ON items USING hnsw ((embedding::vector(3)) vector_l2_ops);
  • 查询示例

    SELECT * FROM items 
        ORDER BY embedding::vector(3) <-> '[3,1,2]' 
        LIMIT 5;

6. 索引是否需要全部装入内存?
  • :但与其他索引类型一样,内存足够时性能更优。

  • 查看索引大小

    SELECT pg_size_pretty(pg_relation_size('index_name'));

故障排查

1. 查询为何未使用索引?
  • 必要条件

    • 查询需包含 ORDER BY + LIMIT,且排序需直接使用距离运算符(升序):

      -- 使用索引
      ORDER BY embedding <=> '[3,1,2]' LIMIT 5;
      
      -- 不使用索引(表达式修饰)
      ORDER BY 1 - (embedding <=> '[3,1,2]') DESC LIMIT 5;
  • 强制使用索引

    BEGIN;
    SET LOCAL enable_seqscan = off;
    SELECT ...;
    COMMIT;
  • 小表提示:数据量少时,全表扫描可能更快。


2. 查询为何未启用并行扫描?
  • 原因:规划器可能低估了并行扫描成本。

  • 解决方案

    BEGIN;
    SET LOCAL min_parallel_table_scan_size = 1;
    SET LOCAL parallel_setup_cost = 1;
    SELECT ...;
    COMMIT;
  • 或改为行内存储

    ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN;

3. 添加 HNSW 索引后结果变少?
  • 动态候选列表限制:默认 hnsw.ef_search=40 可能限制结果数量。

  • 其他原因

    • 死元组(dead tuples)或查询中的过滤条件。

    • NULL 向量零向量(余弦距离时)不会被索引。

  • 解决方案:启用迭代式索引扫描或增大 ef_search


4. 添加 IVFFlat 索引后结果变少?
  • 常见原因

    • 创建索引时数据量不足(相对于 lists 参数)。

    • 探测数 ivfflat.probes 设置过低。

  • 解决方案

    • 数据量不足时删除索引,待数据增长后重建:

      DROP INDEX index_name;
    • 启用迭代式扫描或增大 probes

  • 注意NULL 向量零向量(余弦距离时)不会被索引。

六、 pgvector后续计划

参考:

 部分内容来自AI回答

https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw

Logo

一站式 AI 云服务平台

更多推荐