pgvector onepage
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 BY 和 LIMIT,这样才是典型可索引的 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);
halfvec 用 halfvec_l2_ops 等,sparsevec 用 sparsevec_l2_ops 等,bit 用 bit_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:initializing、loading tuples。
4.2 IVFFlat 索引
IVFFlat 三个关键点:
- 表里先有数据再建索引。
- lists 选择:100 万行以内可从
rows / 1000起步;超过 100 万可从sqrt(rows)起步。 - 查询 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:initializing、performing k-means、assigning tuples、loading 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 没有给“多少行必须用哪种索引”的硬性分档,但给了几个重要原则:
- 默认精确搜索:不建 ANN 索引时,pgvector 做 exact nearest neighbor search,召回率 100%,但大表会慢。
- HNSW:查询性能和速度-召回率折中通常优于 IVFFlat;缺点是建索引慢、内存占用更高;优点是不需要训练,空表也能建。
- IVFFlat:建索引更快、内存更少;缺点是查询性能/召回折中通常不如 HNSW,并且应该在表中已有代表性数据后再建索引。
- IVFFlat lists 建议:100 万行以内可从
rows / 1000起步;超过 100 万行可从sqrt(rows)起步。 - 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 索引过大或查询延迟/召回不稳定时,常见组合是:
- 缩小向量或索引:使用
halfvec、half-precision expression index、binary quantization。 - 两阶段检索:先用便宜索引召回较多候选,再用原始向量精排。
- 分区/分片:按租户、类目、时间、地域或 hash 分片降低单索引规模。
- 读扩展:用 replica 承担读查询。
- 召回率监控:抽样对比 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_multiplier是work_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_quantize、cosine_distance、inner_product、l1_distance、l2_distance、l2_norm、l2_normalize、subvector、vector_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_distance、inner_product、l1_distance、l2_distance、l2_norm、l2_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;
更多推荐




所有评论(0)