土法炼钢兴趣小组的算法知识备份

【向量检索引擎】pgvector 内核对照:同进程 SQL 扩展与专用引擎差在哪一层

文章导航

分类入口
databasestorage
标签入口
#pgvector#postgresql#hnsw#index-am#toast#milvus#vector-engine#hybrid-filter

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

第 18 篇决策树把「必须留在 SQL 同进程」标成 sqlGate,但停在选型口头禅。同一条业务行上的 vector(768),在 PostgreSQL 里怎么进堆、怎么进 HNSW 索引页、过滤发生在扫描前还是扫描后——这些才是和 Milvus Growing/Sealed + Knowhere 差在哪一层的可核对答案。

本文是「向量检索引擎」系列第 19 篇(共 19 篇)。→ 系列目录

文章 分工
01 全景 / 18 选型 四类库与 sqlGate
03 / 05 / 08 / 11 Milvus 生命周期与 bitset
15 / 16 另两极对照
本文 pgvector v0.8.0:Index AM + 8KB 页 + post-filter
PG 页面 / 扩展 8KB 页与 AM 共性
storage/39 ANN 通识入口

版本锚定:pgvector v0.8.0;Milvus 对照 v2.6.21。本机无 Postgres:页装箱为宏公式复现,不伪造 pg_relation_size


一、最小故事:同一行向量,两套系统怎么落地

业务表要保存文档元数据与 768 维嵌入,并且「插入成功后同一事务里就能按相似度查出刚写入的行」。

pgvector 路径(同进程):

BEGIN;
INSERT INTO documents (id, tenant_id, embedding)
VALUES (42, 7, '[...]'::vector(768));
-- 同事务内可见(MVCC);近似索引路径见下文 Index AM
SELECT id FROM documents
ORDER BY embedding <-> $1 LIMIT 10;
COMMIT;

堆元组与 HNSW 索引页变更进入 同一套 WAL;提交后对本快照可见。代价是:建索、ANN 距离计算、OLTP 查询、autovacuum 抢 同一实例 的 CPU 与 shared_buffers

Milvus 路径(本系列主线):客户端 insert 经 Proxy → Streaming Node → WAL → Growing;高质量 Sealed 索引要等 flush / Data Node 建索 / handoff(030510)。可见性由一致性级别裁剪(12),与业务库事务通常跨系统,靠业务主键对齐。

差分不在「会不会 HNSW」,而在:页模型是否绑死 8KB Index AM、索引生命周期能否与在线查询物理分流、过滤是否进图内 bitset

flowchart TB
  subgraph pg ["PostgreSQL process"]
    sql["SQL / Planner / Executor"]
    heap["Heap + TOAST"]
    idx["pgvector HNSW index pages"]
    buf["shared_buffers"]
    sql --> heap
    sql --> idx
    heap --> buf
    idx --> buf
  end
  subgraph milvus ["Milvus disaggregated path"]
    proxy["Proxy"]
    stream["Streaming + WAL"]
    growing["Growing segment"]
    sealed["Sealed + Knowhere"]
    proxy --> stream --> growing
    stream --> sealed
  end

二、谱系、争论与工程映射

阶段 代表 work 与本文关系
图 ANN Malkov & Yashunin, HNSW, IEEE TPAMI 42(4), 2020(arXiv:1603.09320) 与 Knowhere/Hnswlib 同源算法族
专用向量系统 Wang et al., Milvus, SIGMOD 2021 「动态向量需要独立数据管理」
PG 扩展面 Index Access Method;20 扩展 插入点是 IndexAmRoutine
同进程落地 pgvector v0.8.0 vector + hnsw/ivfflat;iterative index scan

争论(双方可核对)

立场 主张 代表论据
A:向量应成通用 DB 一等类型 事务、JOIN、备份同库 pgvector README;第 18 篇 sqlGate
B:与 OLTP 页模型不兼容 持续写入、建索、过滤、对象存储应分流 Wang et al., SIGMOD 2021;本系列 03–08、10

下文只钉可核对差分:页上装得下什么、过滤发生在哪、与谁争 Buffer。


三、接入点:IndexAmRoutine

HNSW 入口 hnswhandler 向 Postgres 交标准索引 AM(摘录保留与正文相关的字段):

// pgvector v0.8.0 — src/hnsw.c
Datum
hnswhandler(PG_FUNCTION_ARGS)
{
    IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
    amroutine->amcanorder = false;
    amroutine->amcanorderbyop = true;   /* ORDER BY dist_op */
    amroutine->amcanparallel = false;
    amroutine->ambuild = hnswbuild;
    amroutine->aminsert = hnswinsert;
    amroutine->ambulkdelete = hnswbulkdelete;
    amroutine->amvacuumcleanup = hnswvacuumcleanup;
    amroutine->amcostestimate = hnswcostestimate;
    amroutine->ambeginscan = hnswbeginscan;
    amroutine->amgettuple = hnswgettuple;
    amroutine->amendscan = hnswendscan;
    PG_RETURN_POINTER(amroutine);
}

四、堆上的 vector:varlena + TOAST

// pgvector v0.8.0 — src/vector.h
#define VECTOR_MAX_DIM 16000
#define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim))

typedef struct Vector {
    int32  vl_len_;
    int16  dim;
    int16  unused;
    float  x[FLEXIBLE_ARRAY_MEMBER];
} Vector;

#define DatumGetVector(x) ((Vector *) PG_DETOAST_DATUM(x))

\(d\) 维 float32 载荷约 \(8+4d\) 字节(另加 HeapTuple 头)。\(d=768\) 约 3080 字节,常仍可进单页;更高维或宽行更易 TOAST。pgvector README 写明 planner 不把 out-of-line 计入 seqscan 代价,可能让串行扫看起来偏便宜——同进程特有的「优化器 vs 物理布局」错位。

HNSW 索引页内再存一份向量副本(下节 data),故「表 + HNSW」存储接近 两份稠密向量 + 邻接 tid。


五、HNSW 落在 8KB 页上

索引页同样是 8KB(02 页面)。block 0 为元页,其后为图页:

// pgvector v0.8.0 — src/hnsw.h
/* Make graph robust against non-HOT updates */
#define HNSW_HEAPTIDS 10

typedef struct HnswMetaPageData {
    uint32 magicNumber; uint32 version; uint32 dimensions;
    uint16 m; uint16 efConstruction;
    BlockNumber entryBlkno; OffsetNumber entryOffno;
    int16 entryLevel; BlockNumber insertPage;
} HnswMetaPageData;

typedef struct HnswElementTupleData {
    uint8 type;      /* HNSW_ELEMENT_TUPLE_TYPE = 1 */
    uint8 level; uint8 deleted; uint8 version;
    ItemPointerData heaptids[HNSW_HEAPTIDS];
    ItemPointerData neighbortid;
    uint16 unused;
    Vector data;
} HnswElementTupleData;

typedef struct HnswNeighborTupleData {
    uint8 type;      /* HNSW_NEIGHBOR_TUPLE_TYPE = 2 */
    uint8 version; uint16 count;
    ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
} HnswNeighborTupleData;

\[ \begin{aligned} \texttt{HNSW\_ELEMENT\_TUPLE\_SIZE}(s) &= \mathrm{MAXALIGN}\bigl(\mathrm{offsetof}(\texttt{data}) + s\bigr), \\ \texttt{HNSW\_NEIGHBOR\_TUPLE\_SIZE}(\ell, m) &= \mathrm{MAXALIGN}\bigl(\mathrm{offsetof}(\texttt{indextids}) + (\ell+2)\,m\cdot |\texttt{ItemPointerData}|\bigr). \end{aligned} \]

hnswbuild.c 注释与逻辑要求:尽量把 element 与 neighbor 放同一页,降低遍历随机读。

HNSW element and neighbor tuples on an 8KB PostgreSQL index page

heaptids[10] 落点:非 HOT 更新可能产生新堆元组。hnswinsert.c 在已有 element 上找空槽追加 heap TID;槽满(i == HNSW_HEAPTIDS)或正在删除则放弃本次合并:

// pgvector v0.8.0 — src/hnswinsert.c(摘录)
for (i = 0; i < HNSW_HEAPTIDS; i++) {
    if (!ItemPointerIsValid(&etup->heaptids[i]))
        break;
}
/* Either being deleted or we lost our chance to another backend */
if (i == 0 || i == HNSW_HEAPTIDS)
    return false;
etup->heaptids[i] = element->heaptids[0];

宏旁注释 Make graph robust against non-HOT updates 与此对应:不是「无限抗更新」,而是 有上限的多 TID 槽

5.1 本机可复现:页装箱

写作机无 PostgreSQL。用 v0.8.0 宏估算 level 0、\(m=16\) 时每页大约几对 element+neighbor(exp_19_page_packing.py,本机已跑通):

\(d\) VECTOR_SIZE etup ntup 约 pairs/page
128 520 592 200 10
384 1544 1616 200 4
768 3080 3152 200 2
1536 6152 6224 200 1

pgvector#690 维护者说明同构。这是 稠密向量 × 8KB 页 的几何事实,不是「HNSW 算法慢」。Milvus Sealed 上 Knowhere 按段文件/mmap 布局,不受此装箱约束(0608)。

python3 post/db/vector-engine/reproduce/exp_19_page_packing.py

六、写路径与生命周期对照

步骤 pgvector Milvus 2.6.x
持久化 堆 + 索引页 → 同一 WAL Streaming → Woodpecker/WAL → Growing
事务 可与业务表同事务(MVCC) 通常跨系统;业务 id 对齐
可搜时机 提交后对本快照可见 Growing 可搜;Sealed+Index 需 handoff
建索隔离 与查询争同一实例 Data Node 可独立池
删除 hnswbulkdelete + vacuum;deleted 标记 Delete/Upsert 与 segment 版本(13

hnswgettuple 要求 MVCC snapshot(非 MVCC 直接报错),搜索前对扫描锁页加共享锁,以便 vacuum 在无 in-flight scan 时标记删除(src/hnswscan.c)——图索引住在 Buffer Manager 里的直接后果。


七、过滤:默认 post-filter 与 iterative scan

README(v0.8.0):近似索引下过滤在 索引扫描之后。谓词命中 10% 行、默认 hnsw.ef_search=40 时,平均大约 4 行通过过滤。与 11 / frontier/09 的 post-filter 失效同构;专用引擎常先编 bitset 再搜图。

0.8.0 起可用 iterative scan 扩扫,直至凑够结果或触达 hnsw.max_scan_tuples / 内存上限:

SET hnsw.iterative_scan = strict_order;  -- 或 relaxed_order

实现落在 hnswgettuple:候选空且未关闭 iterative 时,从 discarded 恢复或 ResumeScanItems

// pgvector v0.8.0 — src/hnswscan.c(摘录)
if (list_length(so->w) == 0) {
    if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_OFF)
        break;
    if (so->tuples >= hnsw_max_scan_tuples
        || MemoryContextMemAllocated(so->tmpCtx, false) > so->maxMemory) {
        /* return remaining from discarded heap */
        so->w = lappend(so->w,
            HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
    } else {
        LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
        so->w = ResumeScanItems(scan);
        UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
    }
}

这是对召回塌陷的工程补丁,不是 ACORN 式图内过滤。README 仍建议:过滤列 B-Tree/GIN、低基数 partial index、高基数分区——Postgres 既有工具,不是 Knowhere 旋钮。


八、资源边界与可观测入口

同进程意味着 ANN 与 OLTP 共享核、shared_buffers、复制流与扩展模型(分区 / Citus 等,而非 Channel+Worker)。第 18 篇「阶段一→迁出」是 sqlGate 与争用同时翻转,不是品味。

本机无 Postgres,下列为 排障入口(工程清单,非实测数字)

怀疑 先看
缓冲争用 pg_stat_io / pg_buffercache(若装);比较向量索引关系与热 OLTP 表的 hit 形态
建索打满 pg_stat_progress_create_index;避开高峰 CREATE INDEX
复制放大 大表建索/重建期间的 pg_stat_replication 延迟
过滤召回不足 EXPLAIN (ANALYZE, BUFFERS);提高 hnsw.ef_search 或打开 iterative_scan,并给过滤列建索引

有数字再说快慢;未标注硬件与 QPS 的「pgvector 一定更慢」本站不写。


九、工程间隙

  1. 算法同源,外壳不同:HNSW 论文假设与 ann-benchmarks 多为静态集;pgvector 把图塞进 OLTP 页与 MVCC,Milvus 把图挂在段文件上——比较 QPS 前先对齐外壳。
  2. 文档「同进程即简单」掩盖装箱\(d=768\) 约 2 节点/页时,随机遍历对 buffer 更敏感;半精度/halfvec 改变的是装箱,不是换成另一套引擎。
  3. iterative_scan ≠ mixed-filter 索引:扩扫提高召回上限,仍可能放大延迟与锁持有时间;ACORN 等见 frontier/09。
  4. TOAST ≠ 对象存储段:仍在同一备份域与实例内,无独立 Data Node 建索池。

十、常见误解

  1. 「pgvector 的 HNSW 和 Milvus 不是一种东西」 —— 算法谱系同源;差分在 Index AM / 8KB 页 vs Segcore/Knowhere/对象存储。
  2. 「有了 iterative_scan 就等于图内过滤」 —— 仍是扩扫 + 事后过滤。
  3. 「进了 TOAST 就等于进了对象存储」 —— 备份域与调度模型都不同。
  4. 「同进程一定更慢」 —— 小规模少一跳网络往往更简单;大规模迁的是隔离与生命周期。

十一、开放问题

  1. 页装箱与量化halfvec / binary 能否在不破坏 AM 语义下显著提高每页节点数?
  2. 过滤下推深度:iterative scan + partial index/分区,能否覆盖专用引擎 bitset 的多数生产谓词?缺同 workload 公开对照。
  3. 分布式 Postgres 上的图分割:Citus 等跨分片 top-k 的召回责任边界仍薄。
  4. 双写协同:热 pgvector + 冷 Lance/Milvus 的一致性,产品案例多、形式化少。

十二、三句话小结

  1. pgvector 是 PostgreSQL Index AM 上的 HNSW/IVFFlat:向量为 varlena,图落在 8KB 页,共享 WAL/Buffer/VACUUM。
  2. 相对本系列 Milvus,它强在 同进程事务与 SQL,弱在 生命周期分流、页装箱、默认 post-filter
  3. 选型回到第 18 篇 sqlGate 与资源争用,而不是比较「谁更会做 ANN」。

参考资料

规范 / 官方文档

源码

核心论文

  1. Malkov, Y. A., & Yashunin, D. A. (2020). Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. IEEE TPAMI, 42(4), 824–836. (arXiv:1603.09320)
  2. Wang et al. (2021). Milvus: A Purpose-Built Vector Data Management System. SIGMOD.

实验 / 线索

站内交叉阅读

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-12 · database / storage

【向量检索引擎】Knowhere:向量索引执行引擎与插件契约

按官方 Knowhere 文档说明其在 Milvus 中的位置、相对 Faiss 的扩展(bitset、SIMD 选择、二进制度量)、VecIndex 类层次与 IDMAP/IVF/HNSW 等类型,用插件注册、CPU/GPU 分发与 bitset 进查询三张图钉住工程契约,并与 db-frontier/08 的算法细节分工。


By .