1. 向量概念:从 PostgreSQL 视角理解

1.1 embedding 是什么

embedding 是模型把对象映射到高维空间后的坐标。例如:

  • 文本 embedding:语义相近的文本向量距离更近。
  • 图片 embedding:视觉相似的图片距离更近。
  • 商品/用户 embedding:行为相近、属性相近的实体距离更近。

在数据库中,embedding 就是一列数据;差别是查询时不是 =<LIKE,而是“离查询向量最近”。

1.2 距离、相似度、召回率

常用术语:

  • Top-K 最近邻:找距离最小的 K 条记录。
  • 距离 distance:越小越近。
  • 相似度 similarity:越大越像。余弦相似度常写成 1 - cosine_distance
  • 召回率 recall:近似索引返回结果与精确搜索结果的重合程度。
  • re-rank:先用便宜的近似/量化方式取较多候选,再用原始向量精排。

pgvector 的 <#> 返回的是负内积,不是内积本身,因为 PostgreSQL 的索引扫描主要支持按操作符升序取结果。想要内积本身:

SELECT ('[1,2,3]'::vector <#> '[4,5,6]'::vector) * -1 AS inner_product;

1.3 选哪种距离

  • L2 / Euclidean:几何直线距离,通用。
  • Inner product:向量已归一化时常很快;许多 embedding 文档建议归一化向量可用内积。
  • Cosine distance:关注方向而非长度,cosine_similarity = 1 - cosine_distance
  • L1 / taxicab:坐标差绝对值之和。
  • Hamming:二进制位不同的个数。
  • Jaccard:二进制集合差异,适合集合式 bit 表示。

2. 存储类型与可执行示例

2.1 vector(n):单精度 dense vector

vector 每个元素是类似 PostgreSQL real 的单精度浮点。存储约为 4 * dimensions + 8 字节。vector 类型最多可存 16000 维,但 HNSW/IVFFlat 索引对 vector 支持到 2000 维。

SET search_path = vec_lab, public;

DROP TABLE IF EXISTS dense_items;
CREATE TABLE dense_items (
    id bigserial PRIMARY KEY,
    embedding vector(3)
);

INSERT INTO dense_items (embedding)
VALUES ('[1,2,3]'), ('[4,5,6]');

ALTER TABLE dense_items ADD COLUMN note text;
UPDATE dense_items SET note = 'first' WHERE id = 1;
DELETE FROM dense_items WHERE id = 999;

-- upsert
INSERT INTO dense_items (id, embedding, note)
VALUES (1, '[1,2,3]', 'upserted'), (3, '[7,8,9]', 'new')
ON CONFLICT (id) DO UPDATE
SET embedding = EXCLUDED.embedding,
    note = EXCLUDED.note;

批量加载推荐 COPY。README 给的是二进制 COPY;日常也可先用文本 COPY。示例:

DROP TABLE IF EXISTS copy_items;
CREATE TABLE copy_items (id int PRIMARY KEY, embedding vector(3));

COPY copy_items (id, embedding) FROM STDIN WITH (FORMAT text); -- tab seq
1	[1,2,3]
2	[4,5,6]
\.

SELECT * FROM copy_items;

 id | embedding
----+-----------
  1 | [1,2,3]
  2 | [4,5,6]

2.2 halfvec(n):半精度 dense vector

halfvec 每元素半精度,存储约 2 * dimensions + 8 字节;类型最多 16000 维,索引支持到 4000 维。适合减少存储、缓存、索引尺寸,但精度更低。

DROP TABLE IF EXISTS half_items;
CREATE TABLE half_items (
    id bigserial PRIMARY KEY,
    embedding halfvec(3)
);

INSERT INTO half_items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]'), ('[1,1,1]');

SELECT id, embedding, embedding <-> '[1,2,4]' AS l2_distance
FROM half_items
ORDER BY embedding <-> '[1,2,4]'
LIMIT 3;

 id | embedding |    l2_distance
----+-----------+--------------------
  1 | [1,2,3]   |                  1
  3 | [1,1,1]   | 3.1622776601683795
  2 | [4,5,6]   |   4.69041575982343

2.3 bit(n):二进制向量

bit 是 PostgreSQL 原生定长位串类型,pgvector 为它提供 Hamming / Jaccard 距离和 ANN 索引 opclass。索引支持到 64000 维。

DROP TABLE IF EXISTS bit_items;
CREATE TABLE bit_items (
    id bigserial PRIMARY KEY,
    embedding bit(6)
);

INSERT INTO bit_items (embedding) VALUES (B'000000'), (B'111111'), (B'101010'), (B'101011');

SELECT id, embedding,
       embedding <~> B'101000' AS hamming_distance,
       embedding <%> B'101000' AS jaccard_distance
FROM bit_items
ORDER BY embedding <~> B'101000'
LIMIT 4;

 id | embedding | hamming_distance |  jaccard_distance
----+-----------+------------------+---------------------
  3 | 101010    |                1 | 0.33333333333333337
  1 | 000000    |                2 |                   1
  4 | 101011    |                2 |                 0.5
  2 | 111111    |                4 |  0.6666666666666667

2.4 sparsevec(n):稀疏向量

sparsevec 只存非零元素,格式:{index:value,index:value}/dimensions,下标从 1 开始。每个非零元素约 8 字节,加固定开销。类型最多 16000 个非零元素,HNSW 索引支持到 1000 个非零元素。

DROP TABLE IF EXISTS sparse_items;
CREATE TABLE sparse_items (
    id bigserial PRIMARY KEY,
    embedding sparsevec(5)
);

INSERT INTO sparse_items (embedding)
VALUES ('{1:1,3:2,5:3}/5'), ('{1:4,3:5,5:6}/5'), ('{2:1,4:2}/5');

SELECT id, embedding, embedding <-> '{1:3,3:1,5:2}/5' AS l2_distance
FROM sparse_items
ORDER BY embedding <-> '{1:3,3:1,5:2}/5'
LIMIT 3;

 id |    embedding    |    l2_distance
----+-----------------+-------------------
  1 | {1:1,3:2,5:3}/5 | 2.449489742783178
  3 | {2:1,4:2}/5     | 4.358898943540674
  2 | {1:4,3:5,5:6}/5 | 5.744562646538029

2.5 不同维度共存

如果列声明为 vector 而不是 vector(n),可以存不同维度。但距离计算要求两边维度一致,索引也只能建在同维度子集上。

DROP TABLE IF EXISTS embeddings_mixed;
CREATE TABLE embeddings_mixed (
    model_id bigint NOT NULL,
    item_id  bigint NOT NULL,
    embedding vector NOT NULL,
    PRIMARY KEY (model_id, item_id)
);

INSERT INTO embeddings_mixed VALUES
(123, 1, '[1,2,3]'),
(123, 2, '[3,2,1]'),
(456, 1, '[1,2,3,4]');

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

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

 model_id | item_id | embedding
----------+---------+-----------
      123 |       2 | [3,2,1]
      123 |       1 | [1,2,3]

2.6 更高精度存储,低精度索引

可以用 double precision[]numeric[] 保留更高精度,查询/索引时 cast 成 vector

DROP TABLE IF EXISTS high_precision_items;
CREATE TABLE high_precision_items (
    id bigserial PRIMARY KEY,
    embedding double precision[] NOT NULL,
    CHECK (vector_dims(embedding::vector) = 3)
);

INSERT INTO high_precision_items (embedding)
VALUES ('{1.000000001,2,3}'), ('{4,5,6}');

CREATE INDEX high_precision_items_hnsw
ON high_precision_items
USING hnsw ((embedding::vector(3)) vector_l2_ops);

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

 id |     embedding
----+-------------------
  1 | {1.000000001,2,3}
  2 | {4,5,6}

3. 查询模式

3.1 最近邻

SELECT id, name, embedding
FROM items
ORDER BY embedding <-> '[3,1,2]'
LIMIT 5;

 id |      name       |   embedding
----+-----------------+---------------
  2 | android phone   | [1,0.8,0.1]
  1 | apple phone     | [1,1,0]
  3 | dslr camera     | [0.9,0.1,0.1]
  8 | laptop computer | [0.8,0.7,0]
  9 | office chair    | [0.3,0.7,0.6]

3.2 查询某行的近邻

SELECT i.*
FROM items i
WHERE i.id <> 1
ORDER BY i.embedding <-> (SELECT embedding FROM items WHERE id = 1)
LIMIT 5;

 id |      name       | category_id | location_id |        content         |   embedding   |             textsearch
----+-----------------+-------------+-------------+------------------------+---------------+------------------------------------
  2 | android phone   |           1 |          10 | phone mobile android   | [1,0.8,0.1]   | 'android':3 'mobile':2 'phone':1
  8 | laptop computer |           1 |          30 | computer laptop work   | [0.8,0.7,0]   | 'computer':1 'laptop':2 'work':3
  3 | dslr camera     |           1 |          20 | camera photo lens      | [0.9,0.1,0.1] | 'camera':1 'lens':3 'photo':2
  9 | office chair    |           4 |          30 | chair office furniture | [0.3,0.7,0.6] | 'chair':1 'furniture':3 'office':2
 10 | yoga mat        |           2 |          30 | sport yoga fitness     | [0.1,1,0.8]   | 'fitness':3 'sport':1 'yoga':2
(5 rows)

3.3 距离阈值

SELECT id, name, embedding <-> '[3,1,2]' AS distance
FROM items
WHERE embedding <-> '[3,1,2]' < 5
ORDER BY embedding <-> '[3,1,2]'
LIMIT 5;

 id |      name       |      distance
----+-----------------+--------------------
  2 | android phone   |   2.76586326822753
  1 | apple phone     | 2.8284271247461903
  3 | dslr camera     | 2.9715314183147616
  8 | laptop computer | 2.9883106105583774
  9 | office chair    | 3.0561413829513664

注意:README 特别提醒,距离过滤最好结合 ORDER BYLIMIT,这样才是典型可索引的 KNN 形态。

3.4 聚合

SELECT AVG(embedding) FROM items;

          avg
------------------------
 [0.45,0.65,0.53000003]


SELECT category_id, AVG(embedding)
FROM items
GROUP BY category_id;

 category_id |                avg
-------------+------------------------------------
           3 | [0.15,0.15,0.85]
           4 | [0.3,0.7,0.6]
           2 | [0.06666667,0.96666664,0.93333334]
           1 | [0.925,0.65,0.05]

均值向量常用于类别中心、用户画像中心、聚类中心等。

4. 索引:精确 vs 近似

默认没有 ANN 索引时,pgvector 做精确搜索:扫描候选行并计算距离,召回率 100%,但大表慢。

pgvector 支持两类 ANN 索引:

  • HNSW:多层近邻图。查询性能/召回折中通常优于 IVFFlat,但建索引慢、内存占用高。可以空表建索引,因为不需要训练。
  • IVFFlat:把向量聚成多个 lists,查询时只探测部分 list。建索引快、内存少,但速度-召回折中通常不如 HNSW。需要表中已有代表性数据后再建。

近似索引的关键现实:建索引后,结果可能与精确搜索不同

4.1 HNSW 索引

SET search_path = vec_lab, public;

DROP INDEX IF EXISTS items_embedding_hnsw_l2;
CREATE INDEX items_embedding_hnsw_l2
ON items USING hnsw (embedding vector_l2_ops);

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, name
FROM items
ORDER BY embedding <-> '[1,1,0]'
LIMIT 5;

                                                               QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=8.07..10.14 rows=5 width=28) (actual time=0.058..0.065 rows=5 loops=1)
   Buffers: shared hit=22
   ->  Index Scan using items_embedding_hnsw_l2 on items  (cost=8.07..12.20 rows=10 width=28) (actual time=0.056..0.061 rows=5 loops=1)
         Order By: (embedding <-> '[1,1,0]'::vector)
         Buffers: shared hit=22
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.148 ms
 Execution Time: 0.100 ms

不同距离要建不同 opclass:

CREATE INDEX items_embedding_hnsw_ip     ON items USING hnsw (embedding vector_ip_ops);
CREATE INDEX items_embedding_hnsw_cosine ON items USING hnsw (embedding vector_cosine_ops);
CREATE INDEX items_embedding_hnsw_l1     ON items USING hnsw (embedding vector_l1_ops);

halfvechalfvec_l2_ops 等,sparsevecsparsevec_l2_ops 等,bitbit_hamming_ops / bit_jaccard_ops

HNSW 参数:

CREATE INDEX items_embedding_hnsw_l2_custom
ON items USING hnsw (embedding vector_l2_ops)
WITH (m = 16, ef_construction = 64);
  • m:每层最大连接数,默认 16。
  • ef_construction:构图候选列表大小,默认 64;越大召回通常越好,但建索引/插入更慢。

查询参数:

SET hnsw.ef_search = 100;

ef_search 默认 40;越大召回通常越好,但查询更慢。只对单个事务生效:

BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, name FROM items ORDER BY embedding <-> '[1,1,0]' LIMIT 5;
COMMIT;

HNSW 建索引调优:

SET maintenance_work_mem = '8GB';
SET max_parallel_maintenance_workers = 7; -- 外加 leader
-- 如果 worker 很多,可能还需调大 max_parallel_workers

不要把 maintenance_work_mem 设到让系统内存耗尽。若构图不再适配该内存,会出现类似 notice:graph no longer fits into maintenance_work_mem。

查看建索引进度:

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

HNSW phases:initializingloading tuples

4.2 IVFFlat 索引

IVFFlat 三个关键点:

  1. 表里先有数据再建索引。
  2. lists 选择:100 万行以内可从 rows / 1000 起步;超过 100 万可从 sqrt(rows) 起步。
  3. 查询 probes:可从 sqrt(lists) 起步;越高召回越好、越慢。
DROP INDEX IF EXISTS items_embedding_ivfflat_l2;
CREATE INDEX items_embedding_ivfflat_l2
ON items USING ivfflat (embedding vector_l2_ops)
WITH (lists = 10);

SET ivfflat.probes = 3;

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, name
FROM items
ORDER BY embedding <-> '[1,1,0]'
LIMIT 5;

                                                                  QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=8.07..10.14 rows=5 width=28) (actual time=0.038..0.044 rows=5 loops=1)
   Buffers: shared hit=22
   ->  Index Scan using items_embedding_hnsw_l2_custom on items  (cost=8.07..12.20 rows=10 width=28) (actual time=0.036..0.041 rows=5 loops=1)
         Order By: (embedding <-> '[1,1,0]'::vector)
         Buffers: shared hit=22
 Planning:
   Buffers: shared hit=21
 Planning Time: 0.230 ms
 Execution Time: 0.088 ms
(9 rows)

不同距离:

CREATE INDEX items_embedding_ivfflat_ip
ON items USING ivfflat (embedding vector_ip_ops) WITH (lists = 10);

CREATE INDEX items_embedding_ivfflat_cosine
ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 10);

CREATE INDEX bit_items_ivfflat_hamming
ON bit_items USING ivfflat (embedding bit_hamming_ops) WITH (lists = 10);

IVFFlat 支持 vector 到 2000 维、halfvec 到 4000 维、bit 到 64000 维。

查询参数:

SET ivfflat.probes = 10;

BEGIN;
SET LOCAL ivfflat.probes = 10;
SELECT id, name FROM items ORDER BY embedding <-> '[1,1,0]' LIMIT 5;
COMMIT;

当 probes 等于 lists 时接近精确搜索,此时 planner 可能不再使用 IVFFlat 索引。

建索引进度:

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

IVFFlat phases:initializingperforming k-meansassigning tuplesloading tuples;百分比主要在 loading tuples 阶段有效。

4.3 向量个数、数量级与索引选择

pgvector 对“向量个数”没有类似“最多只能存 N 条向量”的插件级硬限制。它首先是 PostgreSQL 表数据,因此行数上限主要受 PostgreSQL 表大小、磁盘、内存、WAL、vacuum、备份恢复、查询延迟和索引维护成本影响。README FAQ 中提到,非分区表默认受 PostgreSQL 单表大小限制,默认可到 32 TB;分区表可以有很多这样的分区。

更明确的硬限制通常是“单个向量的维度”以及“索引支持的维度”,而不是表中有多少个向量:

类型 类型可存储上限 HNSW 索引支持 IVFFlat 索引支持 典型用途
vector 16000 维 2000 维 2000 维 常规单精度 dense embedding
halfvec 16000 维 4000 维 4000 维 更小存储/索引,牺牲精度
bit PostgreSQL bit 串 64000 维 64000 维 二进制向量、binary quantization
sparsevec 16000 个非零元素 1000 个非零元素 README 未列为 IVFFlat 支持 高维稀疏特征

因此,常见 384、768、1024、1536 维 embedding 通常可直接使用 vector(n) 并建立 HNSW/IVFFlat 索引;超过 2000 维时,要考虑 halfvec、binary quantization、subvector indexing 或降维。

4.3.1 README 明确给出的选择原则

README 没有给“多少行必须用哪种索引”的硬性分档,但给了几个重要原则:

  1. 默认精确搜索:不建 ANN 索引时,pgvector 做 exact nearest neighbor search,召回率 100%,但大表会慢。
  2. HNSW:查询性能和速度-召回率折中通常优于 IVFFlat;缺点是建索引慢、内存占用更高;优点是不需要训练,空表也能建。
  3. IVFFlat:建索引更快、内存更少;缺点是查询性能/召回折中通常不如 HNSW,并且应该在表中已有代表性数据后再建索引。
  4. IVFFlat lists 建议:100 万行以内可从 rows / 1000 起步;超过 100 万行可从 sqrt(rows) 起步。
  5. IVFFlat probes 建议:可从 sqrt(lists) 起步;probes 越高召回越好,但查询越慢。
4.3.2 实践数量级建议

下面是实践起步建议,不是 pgvector 的硬规则。真实选择要用你的数据、维度、过滤条件、机器配置、延迟目标和召回目标压测验证。

向量行数 起步方案 说明
小于 1 万 精确搜索,必要时给过滤列建普通索引 小表顺序扫描可能最快,ANN 索引收益不明显
1 万 - 10 万 先测精确搜索;延迟不满足再用 HNSW 这个阶段 HNSW 通常简单有效
10 万 - 100 万 优先 HNSW;内存紧或建索引时间敏感可试 IVFFlat HNSW 召回/速度折中通常更好
100 万 - 1000 万 HNSW 或 IVFFlat 都要压测;IVFFlat 按 lists/probes 公式起步 HNSW 更吃内存,IVFFlat 更依赖参数
1000 万以上 分区/分片 + HNSW/IVFFlat;考虑 halfvec、binary quantization、re-rank 单个大索引的构建、vacuum、缓存命中、召回都需要工程化
亿级以上 通常需要分区、分片、只读副本、量化、两阶段召回/精排 这已经是系统架构问题,不只是索引语法问题

简单决策可以这样记:

数据少、要绝对准确:精确搜索
想省心且查询快:HNSW
想建索引快、内存省、能接受更多调参:IVFFlat
数据巨大:分区/分片 + 量化 + re-rank + 监控召回率
4.3.3 IVFFlat lists/probes 计算例子

10 万行:

rows = 100000
lists = rows / 1000 = 100
probes = sqrt(lists) = 10
CREATE INDEX items_embedding_ivfflat_l2_100k
ON items USING ivfflat (embedding vector_l2_ops)
WITH (lists = 100);

SET ivfflat.probes = 10;

100 万行:

rows = 1000000
lists = rows / 1000 = 1000
probes = sqrt(1000) ≈ 32
CREATE INDEX items_embedding_ivfflat_l2_1m
ON items USING ivfflat (embedding vector_l2_ops)
WITH (lists = 1000);

SET ivfflat.probes = 32;

1000 万行:

rows = 10000000
lists = sqrt(rows) ≈ 3162
probes = sqrt(lists) ≈ 56
CREATE INDEX items_embedding_ivfflat_l2_10m
ON items USING ivfflat (embedding vector_l2_ops)
WITH (lists = 3162);

SET ivfflat.probes = 56;

如果把 ivfflat.probes 设置为 lists 的数量,搜索会接近精确搜索,但 planner 可能不再选择 IVFFlat 索引,因为此时扫描范围已经很大。

4.3.4 看“过滤后的候选数”,不是只看总行数

业务查询通常带过滤条件,例如租户、类目、地区、时间:

SELECT *
FROM items
WHERE category_id = 1
ORDER BY embedding <-> '[1,1,0]'
LIMIT 10;

如果总表 1 亿行,但 category_id = 1 只剩 5 万行,那么向量搜索面对的候选规模更接近 5 万,而不是 1 亿。此时普通 B-tree 过滤索引 + 精确排序可能已经很好:

CREATE INDEX items_category_id_idx2 ON items (category_id);

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM items
WHERE category_id = 1
ORDER BY embedding <-> '[1,1,0]'
LIMIT 10;

如果某些过滤值是热点,可以用 partial ANN index:

CREATE INDEX items_category_1_hnsw2
ON items USING hnsw (embedding vector_l2_ops)
WHERE category_id = 1;

如果过滤值很多且天然隔离,例如 tenant、category、region,可以考虑分区,让每个分区维护较小的向量索引。

4.3.5 大规模时的组合策略

当单个 ANN 索引过大或查询延迟/召回不稳定时,常见组合是:

  1. 缩小向量或索引:使用 halfvec、half-precision expression index、binary quantization。
  2. 两阶段检索:先用便宜索引召回较多候选,再用原始向量精排。
  3. 分区/分片:按租户、类目、时间、地域或 hash 分片降低单索引规模。
  4. 读扩展:用 replica 承担读查询。
  5. 召回率监控:抽样对比 ANN 结果和禁用索引后的精确结果。

二阶段 binary quantization 示例:

CREATE INDEX items_embedding_bq_hnsw_large
ON items USING hnsw ((binary_quantize(embedding)::bit(3)) bit_hamming_ops);

SELECT *
FROM (
    SELECT id, name, embedding
    FROM items
    ORDER BY binary_quantize(embedding)::bit(3) <~> binary_quantize('[1,-2,3]'::vector)
    LIMIT 100
) candidates
ORDER BY embedding <=> '[1,-2,3]'
LIMIT 10;

这个例子中,内层用较小的 bit 索引快速召回 100 条候选,外层用原始 vector 做 cosine 精排取前 10 条。候选数 100 不是固定值,要根据召回率和延迟压测调整。

5. 过滤条件与 ANN 索引

典型业务查询往往有过滤条件:

SELECT *
FROM items
WHERE category_id = 1
ORDER BY embedding <-> '[1,1,0]'
LIMIT 5;

 id |      name       | category_id | location_id |          content          |   embedding   |                textsearch
----+-----------------+-------------+-------------+---------------------------+---------------+-------------------------------------------
  1 | apple phone     |           1 |          10 | phone mobile camera apple | [1,1,0]       | 'apple':4 'camera':3 'mobile':2 'phone':1
  2 | android phone   |           1 |          10 | phone mobile android      | [1,0.8,0.1]   | 'android':3 'mobile':2 'phone':1
  8 | laptop computer |           1 |          30 | computer laptop work      | [0.8,0.7,0]   | 'computer':1 'laptop':2 'work':3
  3 | dslr camera     |           1 |          20 | camera photo lens         | [0.9,0.1,0.1] | 'camera':1 'lens':3 'photo':2

5.1 先给过滤列建普通索引

如果过滤选择性好,普通索引 + 精确距离排序很有效:

CREATE INDEX items_category_id_idx ON items (category_id);
CREATE INDEX items_location_category_idx ON items (location_id, category_id);

PostgreSQL 仍可使用 B-tree/hash/GiST/SP-GiST/GIN/BRIN 等适合过滤条件的索引,再对较少候选做距离排序。

5.2 ANN 过滤是“扫描后过滤”

对 HNSW/IVFFlat,过滤通常在 ANN 索引扫描后应用。假如 category_id = 1 只匹配 10% 行,HNSW 默认 ef_search = 40,平均可能只有约 4 条通过过滤。解决方法:

SET hnsw.ef_search = 200;

或者启用 iterative scan。

5.3 Partial index

如果只有少数离散过滤值,可为热点值建 partial ANN 索引:

CREATE INDEX items_category_1_hnsw
ON items USING hnsw (embedding vector_l2_ops)
WHERE category_id = 1;

5.4 Partitioning

如果过滤值很多,考虑分区:

DROP TABLE IF EXISTS items_part CASCADE;
CREATE TABLE items_part (
    id bigserial,
    embedding vector(3),
    category_id int NOT NULL,
    PRIMARY KEY (category_id, id)
) PARTITION BY LIST (category_id);

CREATE TABLE items_part_1 PARTITION OF items_part FOR VALUES IN (1);
CREATE TABLE items_part_2 PARTITION OF items_part FOR VALUES IN (2);

6. Iterative Index Scans(0.8.0+)

近似索引配合过滤可能返回不足 K 条。Iterative scan 会自动继续扫描更多索引内容,直到结果足够或达到上限。

严格排序:

SET hnsw.iterative_scan = strict_order;

宽松排序:结果距离顺序可能略有偏差,但召回更好。

SET hnsw.iterative_scan = relaxed_order;
SET ivfflat.iterative_scan = relaxed_order;

宽松结果再排序:

WITH relaxed_results AS MATERIALIZED (
    SELECT id, name, embedding <-> '[1,1,0]' AS distance
    FROM items
    WHERE category_id = 1
    ORDER BY distance
    LIMIT 5
)
SELECT *
FROM relaxed_results
ORDER BY distance + 0;

 id |      name       |      distance
----+-----------------+--------------------
  1 | apple phone     |                  0
  2 | android phone   |  0.223606791085977
  8 | laptop computer | 0.3605551209338572
  3 | dslr camera     | 0.9110433160426867

+ 0 是 README 对 PostgreSQL 17+ 的提示,用于确保外层排序表达式被正确处理。

距离过滤推荐把距离条件放在 materialized CTE 外层:

WITH nearest_results AS MATERIALIZED (
    SELECT id, name, embedding <-> '[1,1,0]' AS distance
    FROM items
    ORDER BY distance
    LIMIT 5
)
SELECT *
FROM nearest_results
WHERE distance < 0.6
ORDER BY distance;

 id |      name       |      distance
----+-----------------+--------------------
  1 | apple phone     |                  0
  2 | android phone   |  0.223606791085977
  8 | laptop computer | 0.3605551209338572

HNSW 上限参数:

SET hnsw.max_scan_tuples = 20000;
SET hnsw.scan_mem_multiplier = 2;
  • hnsw.max_scan_tuples 默认 20000,近似控制最多访问 tuple 数,不影响初始扫描。
  • hnsw.scan_mem_multiplierwork_mem 的倍数,默认 1;如果调大 max_scan_tuples 不改善召回,可试着增大它。

IVFFlat 上限:

SET ivfflat.max_probes = 100;

如果 ivfflat.max_probes < ivfflat.probes,仍会使用 ivfflat.probes

7. 半精度索引、二进制量化、子向量索引

7.1 用 halfvec 表达式索引降低索引尺寸

即使表列是 vector,也可以把索引建在 embedding::halfvec(n) 上。

CREATE INDEX items_embedding_half_hnsw
ON items USING hnsw ((embedding::halfvec(3)) halfvec_l2_ops);

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

7.2 Binary quantization

binary_quantize(vector) 把向量按符号等规则量化成 bit,索引更小、搜索更快,但信息损失更大。常见做法是先用 bit 召回较多候选,再用原向量精排。

CREATE INDEX items_embedding_bq_hnsw
ON items USING hnsw ((binary_quantize(embedding)::bit(3)) bit_hamming_ops);

-- 只用量化向量搜索
SELECT id, name
FROM items
ORDER BY binary_quantize(embedding)::bit(3) <~> binary_quantize('[1,-2,3]'::vector)
LIMIT 5;

-- 先取 20 个候选,再用原向量 cosine 精排
SELECT *
FROM (
    SELECT id, name, embedding
    FROM items
    ORDER BY binary_quantize(embedding)::bit(3) <~> binary_quantize('[1,-2,3]'::vector)
    LIMIT 20
) candidates
ORDER BY embedding <=> '[1,-2,3]'
LIMIT 5;

7.3 Subvector indexing

某些模型向量前若干维可作为粗召回信号,可对子向量建表达式索引,再全向量 re-rank。

DROP TABLE IF EXISTS long_items;
CREATE TABLE long_items (id bigserial PRIMARY KEY, embedding vector(5));
INSERT INTO long_items (embedding) VALUES
('[1,2,3,4,5]'), ('[1,2,2,4,4]'), ('[5,4,3,2,1]'), ('[0,1,0,1,0]');

CREATE INDEX long_items_subvec_hnsw
ON long_items USING hnsw ((subvector(embedding, 1, 3)::vector(3)) vector_cosine_ops);

SELECT *
FROM long_items
ORDER BY subvector(embedding, 1, 3)::vector(3) <=> subvector('[1,2,3,4,5]'::vector, 1, 3)
LIMIT 5;

SELECT *
FROM (
    SELECT *
    FROM long_items
    ORDER BY subvector(embedding, 1, 3)::vector(3) <=> subvector('[1,2,3,4,5]'::vector, 1, 3)
    LIMIT 20
) candidates
ORDER BY embedding <=> '[1,2,3,4,5]'
LIMIT 5;

8. Hybrid Search:向量 + 全文检索

向量搜索擅长语义相似,全文检索擅长关键词精确匹配。混合搜索常用于 RAG、商品搜索、文档搜索。

SET search_path = vec_lab, public;

-- 关键词全文搜索
SELECT id, content
FROM items, plainto_tsquery('simple', 'phone camera') query
WHERE textsearch @@ query
ORDER BY ts_rank_cd(textsearch, query) DESC
LIMIT 5;

-- 一个简单融合:分别算文本分和向量距离,按手工公式排序
WITH q AS (
    SELECT plainto_tsquery('simple', 'phone camera') AS tsq,
           '[1,1,0]'::vector AS vec
)
SELECT i.id, i.name,
       ts_rank_cd(i.textsearch, q.tsq) AS text_rank,
       i.embedding <=> q.vec AS cosine_distance,
       ts_rank_cd(i.textsearch, q.tsq) - (i.embedding <=> q.vec) AS fused_score
FROM items i, q
WHERE i.textsearch @@ q.tsq
ORDER BY fused_score DESC
LIMIT 5;

README 提到生产中可用 Reciprocal Rank Fusion(RRF)或 cross-encoder 融合结果。简单公式不一定稳定,实际需要离线评估。

9. 常见排障

9.1 为什么没有用向量索引?

KNN 索引查询一般必须是:距离操作符结果本身、升序、带 LIMIT。

-- 可用索引的形态
SELECT * FROM items
ORDER BY embedding <=> '[1,1,0]'
LIMIT 5;

-- 通常不能用向量索引,因为 ORDER BY 是表达式且 DESC
SELECT * FROM items
ORDER BY 1 - (embedding <=> '[1,1,0]') DESC
LIMIT 5;

可临时鼓励 planner:

BEGIN;
SET LOCAL enable_seqscan = off;
SELECT * FROM items ORDER BY embedding <-> '[1,1,0]' LIMIT 5;
COMMIT;

小表顺序扫描可能本来就更快。

9.2 加 HNSW 后结果少了?

可能原因:

  • hnsw.ef_search 默认 40,候选不足。
  • dead tuples。
  • WHERE 过滤在 ANN 扫描后应用。
  • NULL 向量不会被索引;cosine 距离下零向量也不会被索引。

处理:调大 hnsw.ef_search,启用 iterative scan,VACUUM/REINDEX,检查过滤选择性。

9.3 加 IVFFlat 后结果少了?

可能原因:

  • 建索引时数据太少或 lists 太多,聚类质量差。
  • ivfflat.probes 太小。
  • 同样存在 NULL/cosine 零向量不索引问题。

可先 drop index,等数据更多后重建:

DROP INDEX IF EXISTS items_embedding_ivfflat_l2;

9.4 索引是否必须放进内存?

不是必须,但像其它索引一样,能放进内存通常更快。查看大小:

SELECT pg_size_pretty(pg_relation_size('items_embedding_hnsw_l2'));

可用 half-precision indexing 或 binary quantization 缩小索引。


10. SQL/API 参考速查

10.1 vector

  • 存储:4 * dimensions + 8 字节。
  • 元素:单精度浮点,必须 finite,不能 NaN/Infinity。
  • 类型最多 16000 维;ANN 索引支持到 2000 维。

操作符:

操作符 含义
+ 元素级加法
- 元素级减法
* 元素级乘法
`
<-> L2 / Euclidean distance
<#> negative inner product
<=> cosine distance
<+> L1 / taxicab distance

函数:

SELECT
  l2_distance('[1,2,3]'::vector, '[4,5,6]'::vector),
  inner_product('[1,2,3]'::vector, '[4,5,6]'::vector),
  cosine_distance('[1,2,3]'::vector, '[4,5,6]'::vector),
  l1_distance('[1,2,3]'::vector, '[4,5,6]'::vector),
  l2_normalize('[3,4]'::vector),
  binary_quantize('[1,-1,2]'::vector),
  subvector('[1,2,3,4]'::vector, 2, 2),
  vector_dims('[1,2,3]'::vector),
  vector_norm('[3,4]'::vector);

聚合:avg(vector)sum(vector)

10.2 halfvec

  • 存储:2 * dimensions + 8 字节。
  • 类型最多 16000 维;ANN 索引支持到 4000 维。
  • 支持 + - * || <-> <#> <=> <+>
  • 支持 binary_quantizecosine_distanceinner_productl1_distancel2_distancel2_norml2_normalizesubvectorvector_dims
  • 聚合:avg(halfvec)sum(halfvec)

10.3 bit

  • PostgreSQL 原生 bit 类型,pgvector 增加距离函数/索引。
  • HNSW/IVFFlat 索引支持到 64000 维。
SELECT
  B'1010' <~> B'1001' AS hamming,
  B'1010' <%> B'1001' AS jaccard,
  hamming_distance(B'1010', B'1001'),
  jaccard_distance(B'1010', B'1001');

10.4 sparsevec

  • 存储:8 * non-zero elements + 16 字节。
  • 元素:单精度浮点,必须 finite。
  • 类型最多 16000 个非零元素;HNSW 索引支持到 1000 个非零元素。
  • 支持 <-> <#> <=> <+>
  • 支持 cosine_distanceinner_productl1_distancel2_distancel2_norml2_normalize

全量脚本

CREATE EXTENSION IF NOT EXISTS vector;

DROP SCHEMA IF EXISTS vec_lab CASCADE;
CREATE SCHEMA vec_lab;
SET search_path = vec_lab, public;

-- 商品表:embedding 用 3 维向量,便于手算距离
CREATE TABLE items (
    id          bigserial PRIMARY KEY,
    name        text NOT NULL,
    category_id int  NOT NULL,
    location_id int  NOT NULL,
    content     text NOT NULL,
    embedding   vector(3) NOT NULL,
    textsearch   tsvector GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED
);

INSERT INTO items (name, category_id, location_id, content, embedding) VALUES
('apple phone',       1, 10, 'phone mobile camera apple',        '[1, 1, 0]'),
('android phone',     1, 10, 'phone mobile android',             '[1, 0.8, 0.1]'),
('dslr camera',       1, 20, 'camera photo lens',                '[0.9, 0.1, 0.1]'),
('running shoes',     2, 10, 'sport shoes running',              '[0, 1, 1]'),
('hiking boots',      2, 20, 'outdoor shoes hiking',             '[0.1, 0.9, 1]'),
('coffee beans',      3, 10, 'coffee beans drink',               '[0.2, 0.1, 0.9]'),
('green tea',         3, 20, 'tea drink leaf',                   '[0.1, 0.2, 0.8]'),
('laptop computer',   1, 30, 'computer laptop work',             '[0.8, 0.7, 0]'),
('office chair',      4, 30, 'chair office furniture',           '[0.3, 0.7, 0.6]'),
('yoga mat',          2, 30, 'sport yoga fitness',               '[0.1, 1, 0.8]');

-- 基础最近邻:按 L2 欧氏距离排序
SELECT id, name, embedding <-> '[1,1,0]' AS l2_distance
FROM items
ORDER BY embedding <-> '[1,1,0]'
LIMIT 5;

-- 四种 dense vector 距离/相似度对比
SELECT
    name,
    embedding <-> '[1,1,0]' AS l2_distance,
    (embedding <#> '[1,1,0]') * -1 AS inner_product,
    embedding <=> '[1,1,0]' AS cosine_distance,
    1 - (embedding <=> '[1,1,0]') AS cosine_similarity,
    embedding <+> '[1,1,0]' AS l1_distance
FROM items
ORDER BY embedding <-> '[1,1,0]'
LIMIT 5;

-- 找与某一行最相似的其它行
SELECT q.name AS query_item, i.id, i.name, i.embedding <-> q.embedding AS distance
FROM items q
JOIN items i ON i.id <> q.id
WHERE q.name = 'apple phone'
ORDER BY i.embedding <-> q.embedding
LIMIT 5;

-- 距离过滤:生产中通常和 ORDER BY + LIMIT 一起使用,才利于索引
SELECT id, name, embedding <-> '[1,1,0]' AS distance
FROM items
WHERE embedding <-> '[1,1,0]' < 0.6
ORDER BY embedding <-> '[1,1,0]'
LIMIT 10;

-- 聚合:向量均值可作为某个类别的中心向量/原型向量
SELECT category_id, AVG(embedding) AS centroid, SUM(embedding) AS sum_embedding
FROM items
GROUP BY category_id
ORDER BY category_id;

-- 向量函数
SELECT
    vector_dims('[1,2,3]'::vector) AS dims,
    vector_norm('[3,4]'::vector) AS norm_3_4,
    l2_normalize('[3,4]'::vector) AS normalized,
    binary_quantize('[1,-2,0,3]'::vector) AS quantized,
    subvector('[1,2,3,4,5]'::vector, 2, 3) AS subvec;

-- 元素级运算
SELECT
    '[1,2,3]'::vector + '[4,5,6]'::vector AS add_v,
    '[4,5,6]'::vector - '[1,2,3]'::vector AS sub_v,
    '[1,2,3]'::vector * '[4,5,6]'::vector AS mul_v,
    '[1,2]'::vector || '[3,4]'::vector AS concat_v;
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐