ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux 下 Docker 部署 PostgreSQL 16 与 pgvector 向量检索

Linux 下 Docker 部署 PostgreSQL 16 与 pgvector 向量检索

上周有人丢给我一个问题:手上堆了几百万条 1536 维的 embedding,究竟该塞进哪个库。聊天工具里聊了半小时,我给的答案还是那套:在 Linux 机器上拉一个 Docker 镜像,镜像名pgvector/pgvector:pg16,把pgvector扩展建起来,剩下的读写全交给 SQL。这套组合我从做语义检索的小项目一路用到几十亿 token 的语料库,中间换过机器、换过云、换过存储引擎,唯独这个底座一直没动过。

这篇文章讲的就是这件事:在 Linux 环境下用 Docker 安装 PostgreSQL 16 加 pgvector 向量扩展,从镜像怎么选、容器怎么起、参数怎么调,到表怎么建、索引怎么选、查询为什么没走索引,一条线写下来。它解决的核心问题是——不想额外维护一套专用向量数据库,又需要在一个熟悉的关系库里做近邻检索。适合三类人看:刚接触向量检索、手里只有几万条数据的新手;已经在跑 PostgreSQL、想原地加一个向量能力的后端;以及被专用向量库的运维成本劝退、想回退到单一存储的团队。

我把踩过的坑基本都写进去了,包括几个官方文档里一句话带过、但实际能让人卡半天的地方。

1. 先把选型讲透:为什么是 pgvector,而不是另一个专用库

1.1 三类向量存储方案的真实差距

向量检索这个领域,方案大致分三层。最上面是专用向量数据库,天生为 ANN 检索设计,分片、副本、量化压缩一应俱全;中间是搜索引擎派的实现,靠倒排加向量字段拼出来;最下面就是关系库扩展,典型代表就是 PostgreSQL 加pgvector。

很多人默认认为“专用的一定强”,这句话只在数据量到千万级、QPS 到几千、还需要多副本横向扩展的时候成立。真实项目里,绝大多数场景的向量规模在十万到几百万之间,QPS 几十到几百,还要求跟业务数据做 join、做事务、做条件过滤。这种负载下,上一套专用库意味着你要多维护一个集群、多写一套数据同步逻辑、多处理一次一致性对账。多出来的复杂度是实打实的成本,而性能收益可能只有几十毫秒。

pgvector的定位非常清晰:让 Postgres 具备 ANN 检索能力,代价是接受它在极端规模下不如专用库。它把向量当成一种普通列类型,索引走 HNSW 或 IVFFlat,查询就是一条带ORDER BY的 SQL。事务、备份、权限、CDC、BI 工具这些生态能力全部自动继承,这才是它最大的价值——不是快,是省事。

1.2 pgvector 的能力边界在哪里

用之前得知道它哪里不行,不然一定会在某个深夜踩坑。三个限制最值得记:

  • 可建索引的维度有上限。vector类型走 HNSW / IVFFlat 索引时最多 2000 维;halfvec(半精度)能到 4000 维;bit类型能到 64000 维。主流 embedding 模型输出 768、1024、1536、3072 维,1536 没问题,3072 就得转halfvec或者降到 2000 维以下。这一点很多人第一次撞上会懵,因为建表时不报错,建索引时才报。
  • 索引构建是内存密集型操作。HNSW 建索引会把图结构往内存里堆,几百万行数据配上maintenance_work_mem太小,直接 OOM 或者慢到怀疑人生。
  • 过滤加检索的组合需要动脑子。带WHERE条件的近邻查询,Postgres 有两种执行路径:先过滤再排序,或者先 ANN 再过滤。后者会导致召回数量不足,你得用iterative scan(0.8 版本引入)或者调大hnsw.ef_search来兜。

这三条如果事先不知道,后面一定会返工。我在第二个项目上就因为 3072 维的模型直接建 HNSW 索引失败,白等了一小时。

1.3 为什么用 Docker 装,而不是 apt 装

apt install postgresql-16再自己编译pgvector当然可行,我早期也这么干过。问题是编译需要postgresql-server-dev-16、build-essential,还得保证pg_config指向正确的版本,机器上如果同时装了 PG 14/15/16,编译出来的.so很容易装错目录,然后CREATE EXTENSION报一句could not access file "$libdir/vector",排查半小时。

Docker 方案把这些问题一次性抹平。官方维护的pgvector/pgvector镜像里,PostgreSQL 和扩展都是预编译好、路径对齐的,版本组合固定。换机器、换系统、迁移到另一台测试机,拉同一个 tag 就是同一个环境。另外,容器里的 PG 你可以随便折腾参数、随便删库重建,不污染宿主机。

代价是数据持久化和资源限制要自己做,后面几节会讲怎么处理。总体算下来,省的时间远多于花的时间。

2. 环境准备与镜像 tag 的门道

2.1 宿主机的最低要求和检查清单

先明确硬件底线。跑pgvector我建议至少 2 核 4G 起步,如果数据量超过 500 万条向量,内存直接按 16G 起,原因在参数那一节讲。磁盘方面,向量是浮点数组,1536 维的vector大约占 6KB 左右(每维 4 字节加头部开销),一千万条就是 60GB 裸数据,索引还要再翻一倍左右,所以磁盘规划要留余量。

开始之前先在宿主机上确认几件事:

# 确认 docker 和 compose 都在 docker --version docker compose version # 确认内核的共享内存够用(后面 --shm-size 依赖) df -h /dev/shm # 确认 5432 端口没被占用 ss -tlnp | grep 5432 # 确认数据目录所在磁盘的剩余空间 df -h /data

提醒:如果宿主机上已经有别的 PostgreSQL 在跑,要么换端口映射,要么改容器内监听端口。直接-p 5432:5432会起不来,而且报错信息里只写bind: address already in use,不看日志不容易反应过来。

2.2 镜像 tag 到底该怎么选

pgvector/pgvector这个仓库的 tag 命名规则是<pgvector版本>-pg<PG大版本>或者直接pg<PG大版本>。比如pg16表示跟随 PostgreSQL 16 的最新补丁版、内置当前最新的pgvector;0.8.0-pg16这种则把扩展版本也钉死。

我的建议分两种场景:

场景推荐 tag理由
学习、测试、内部 demopg16跟随最新补丁,省心
生产环境0.8.0-pg16这类钉死版本避免某次docker pull之后扩展行为变化
需要特定系统库pg16-bookworm基于 Debian 12,glibc 版本较新
极小镜像pg16-alpine体积小,但部分扩展和字符集支持受限

alpine版本要谨慎,某些依赖 ICU 排序规则或者需要编译额外扩展的场景会出问题。我第一次用 alpine 时,中文排序行为和 bookworm 不一致,查了半天才发现是 locale 差异。生产环境我一般直接选默认的 Debian 版本,稳。

2.3 拉取镜像与本地验证

docker pull pgvector/pgvector:pg16

拉完之后别急着起容器,先确认里面确实带扩展:

docker run --rm pgvector/pgvector:pg16 \ bash -c "ls /usr/share/postgresql/16/extension/ | grep vector"

正常能看到vector.control、vector--0.8.0.sql之类的文件。如果这一行什么都没有,说明拉错了镜像——大概率是拉成了官方的postgres:16,那个镜像里没有pgvector,后面CREATE EXTENSION vector会直接告诉你extension "vector" is not available。

顺带看一眼容器里的 PG 版本和扩展版本:

docker run --rm pgvector/pgvector:pg16 \ bash -c "postgres --version; ls /usr/share/postgresql/16/extension/vector.control"

记下这个版本号,后面写文档、排查兼容性问题都用得上。

3. 容器启动:从一条命令到可复现的编排

3.1 一条能跑起来的 docker run 命令拆解

先给能直接用的版本,再逐个参数说原因:

mkdir -p /data/pgvector/{pgdata,init} docker run -d \ --name pgvector16 \ --restart unless-stopped \ -p 5432:5432 \ -e POSTGRES_USER=vecadmin \ -e POSTGRES_PASSWORD='换成你自己的强密码' \ -e POSTGRES_DB=vectordb \ -e TZ=Asia/Shanghai \ -e PGDATA=/var/lib/postgresql/data/pgdata \ -v /data/pgvector/pgdata:/var/lib/postgresql/data \ -v /data/pgvector/init:/docker-entrypoint-initdb.d:ro \ --shm-size=1g \ --memory=8g \ pgvector/pgvector:pg16

逐个说关键点:

  • -v /data/pgvector/pgdata:/var/lib/postgresql/data是数据持久化,这条不写等于白装,容器一删数据全没。
  • PGDATA指向挂载目录下的子目录,是官方镜像推荐的写法。原因是挂载点本身被 Docker 接管后权限和lost+found目录会带来麻烦,放进子目录更干净。这一点在升级 PG 大版本时尤其重要。
  • --shm-size=1g是最容易被漏掉的一条。PostgreSQL 并行查询、建索引都用共享内存,容器默认/dev/shm只有 64MB,跑大表建索引时会报could not resize shared memory segment ... No space left on device。这个报错跟磁盘无关,纯粹是 shm 太小,我第一次遇到时对着磁盘剩余空间看了半天。
  • --memory=8g是给容器设硬上限,防止某个失控查询把宿主机拖死。
  • TZ设时区,不设的话时间戳全按 UTC 存,日志和业务时间对不上,排查问题会很难受。

3.2 用 compose 固化配置,别手敲命令

docker run适合试水,长期用一定得上 compose。理由很实际:参数记不住、迁移时容易漏、团队里每个人的命令不一样。

services: pgvector: image: pgvector/pgvector:pg16 container_name: pgvector16 restart: unless-stopped environment: POSTGRES_USER: vecadmin POSTGRES_PASSWORD: ${PG_PASSWORD} POSTGRES_DB: vectordb TZ: Asia/Shanghai PGDATA: /var/lib/postgresql/data/pgdata ports: - "5432:5432" volumes: - ./pgdata:/var/lib/postgresql/data - ./init:/docker-entrypoint-initdb.d:ro - ./conf/postgresql.conf:/etc/postgresql/postgresql.conf:ro shm_size: 1g mem_limit: 8g command: - postgres - -c - config_file=/etc/postgresql/postgresql.conf healthcheck: test: ["CMD-SHELL", "pg_isready -U vecadmin -d vectordb"] interval: 10s timeout: 5s retries: 5

密码放在同目录的.env里,PG_PASSWORD=xxx,别写进 yaml 提交到仓库。healthcheck加上之后,docker compose ps能直接看到healthy状态,脚本里等依赖服务启动时很好用。

3.3 初始化脚本让扩展自动就位

/docker-entrypoint-initdb.d这个目录里的.sql、.sh文件,只会在数据目录为空时执行一次。这个特性特别适合做扩展自举:

-- init/01-init.sql CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

这里有两个坑得说清楚:

第一个坑,初始化脚本只在首次启动生效。你的pgdata目录里一旦有了数据,改init/01-init.sql重启也不会执行。想重新触发,只能把pgdata清空——生产环境千万别这么干。后续要加扩展,老老实实手动连上去执行CREATE EXTENSION。

第二个坑,初始化脚本里如果写了CREATE DATABASE或者复杂的建表逻辑,出错会导致容器启动失败,而且日志藏在docker logs里。建议初始化脚本保持极简,只放扩展创建,业务建表交给应用侧的迁移工具。

启动之后验证一下:

docker compose up -d docker compose logs -f pgvector # 连进去看扩展 docker exec -it pgvector16 psql -U vecadmin -d vectordb -c "\dx" docker exec -it pgvector16 psql -U vecadmin -d vectordb -c "SELECT extversion FROM pg_extension WHERE extname='vector';"

看到vector那一行,基础环境就算成了。

4. 数据库参数调优:让向量检索真的跑得起来

4.1 内存类参数怎么算

默认的postgresql.conf是按小机器配的,直接拿来跑向量检索会很憋屈。核心几个参数按下面这个逻辑算:

参数建议值计算依据
shared_buffers物理内存 25%,上限 8GB再大热数据命中率提升有限
effective_cache_size物理内存 50%~75%给优化器参考,不实际分配
work_mem64MB~256MB排序、哈希、召回过程中的临时内存
maintenance_work_mem2GB~8GB建索引专用,直接影响 HNSW 构建
max_parallel_workers_per_gather2~4并行扫描
max_connections100~200别设太大,每连接有固定开销

举例:一台 16GB 内存、专门跑向量库的机器,可以用这套:

shared_buffers = 4GB effective_cache_size = 10GB work_mem = 128MB maintenance_work_mem = 4GB max_parallel_workers_per_gather = 4 max_parallel_maintenance_workers = 4 random_page_cost = 1.1

random_page_cost改成 1.1 是因为现在基本都是 SSD,默认的 4.0 会让优化器过度回避索引扫描,向量查询特别容易因此走到全表扫描。

一个反直觉的点:work_mem不是越大越好。它是每个排序/哈希操作单独分配的,一条复杂查询里可能同时开好几个。设成 1GB 配上 100 个连接,理论峰值能吃掉 100GB。宁可按需在会话里SET work_mem,也别全局设太大。

4.2 建索引期间的临时参数

HNSW 建索引这一步是整个流程里最吃资源的。索引构建的内存需求大致跟行数 × (维度 × 4字节 + m × 2 × 8字节)成正比,粗算下来一百万行 1536 维、m=16,光图结构就要 1.5GB 以上,还不算排序和临时文件。

实际做法不是把全局maintenance_work_mem调得极高,而是针对性调整:

-- 建索引前 SET maintenance_work_mem = '8GB'; SET max_parallel_maintenance_workers = 4; CREATE INDEX CONCURRENTLY idx_items_embedding_hnsw ON items USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 128); -- 建完恢复 RESET maintenance_work_mem; RESET max_parallel_maintenance_workers;

CONCURRENTLY的好处是不锁表,代价是构建时间更长、内存占用更高。线上表已经有人在读的时候必须加这个关键字,否则建索引期间写入全被阻塞。

4.3 连接数与运维侧的参数

容器环境下还有一个常被忽视的参数:listen_addresses。官方镜像默认是*,但如果你挂载了自定义postgresql.conf,记得确认这一项,不然容器起来了外部连不进去,docker logs里一片安静。

日志方面建议开log_min_duration_statement = 1000,慢查询超过 1 秒就记下来。向量检索的慢查询和普通 SQL 的慢查询形态不一样,前者往往是“索引没命中”或者“过滤后召回不足”,看执行计划比看耗时更有用。

另外把pg_stat_statements加上,采样向量查询的真实分布。这玩意儿在建扩展那一步顺手就建了,shared_preload_libraries里加上pg_stat_statements即可。注意这个参数必须重启容器才生效,reload不管用。

5. 建表、建索引与查询实战

5.1 向量类型的选型表

从 0.7 版本起,pgvector提供四种向量类型,选错了要么精度掉、要么索引建不上:

类型单维占用可建索引维度上限适用场景
vector(n)4 字节(float4)2000常规 embedding,精度要求高
halfvec(n)2 字节(float2)4000高维模型、省内存省磁盘
bit(n)1 bit64000二值化 embedding、哈希签名
sparsevec(n)非零元素存储1000 非零项稀疏检索(如 BM25 向量化)

建表示例,用vector存 1536 维,同时保留业务字段和过滤字段:

CREATE TABLE IF NOT EXISTS doc_chunks ( id BIGSERIAL PRIMARY KEY, doc_id BIGINT NOT NULL, chunk_no INT NOT NULL, content TEXT NOT NULL, metadata JSONB DEFAULT '{}'::jsonb, embedding vector(1536), created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_doc_chunks_doc_id ON doc_chunks (doc_id); CREATE INDEX idx_doc_chunks_created ON doc_chunks (created_at DESC);

注意embedding没有加NOT NULL,因为向量入库常常是异步的:先落文本,再补向量。如果你的流程里向量一定存在,加上NOT NULL能让优化器有更多信息。

5.2 HNSW 和 IVFFlat 到底选哪个

这是最高频的问题,直接上对比:

维度HNSWIVFFlat
构建速度慢快
索引体积大(约数据量 1.5~2 倍)小
查询速度快,召回高中,依赖 probes
增量插入友好,直接插会退化,需重建
内存需求高低
参数m、ef_construction、ef_searchlists、probes
空表能建吗能不能(需要数据)

结论很直白:数据会持续增量写入的,选 HNSW;数据一次性导入、之后只读的,IVFFlat 更省资源。

HNSW 参数怎么定:

  • m:每个节点的最大连接数,默认 16。召回要求高就调到 32,索引体积和构建时间都会跟着涨。一般 16~32 之间。
  • ef_construction:构建时的候选队列长度,默认 64。调到 128 或 200 能明显提升召回,代价是构建变慢。100 万行以内建议 128 起步。
  • ef_search:查询期参数,默认 40。这个可以运行时改,不用重建索引。
-- 会话级调高召回 SET hnsw.ef_search = 100;

IVFFlat 的lists有个经验公式:行数小于 100 万时lists = rows / 1000,大于 100 万时lists = sqrt(rows)。查询时probes从sqrt(lists)起调。

5.3 写入与检索的完整示例

先看纯 SQL 的写入和近邻查询:

-- 写入(向量用字符串字面量传) INSERT INTO doc_chunks (doc_id, chunk_no, content, embedding) VALUES (1001, 0, '示例文本内容', '[0.0123,-0.0456,0.0789,...]'); -- 余弦距离近邻查询,取前 10 SELECT id, doc_id, content, 1 - (embedding <=> '[0.0123,-0.0456,0.0789,...]') AS similarity FROM doc_chunks WHERE embedding IS NOT NULL ORDER BY embedding <=> '[0.0123,-0.0456,0.0789,...]' LIMIT 10;

距离算子必须记牢,用错算子索引直接失效:

算子含义配套索引 opclass
<->欧氏距离(L2)vector_l2_ops
<#>负内积vector_ip_ops
<=>余弦距离vector_cosine_ops
<+>L1 距离vector_l1_ops

关键点:ORDER BY里的算子必须和建索引时的 opclass 一致,否则索引不生效。比如索引建的是vector_cosine_ops,查询却用<->,优化器只能老老实实全表扫。

Python 侧的完整例子,用 psycopg3:

import psycopg from pgvector.psycopg import register_vector DSN = "postgresql://vecadmin:password@127.0.0.1:5432/vectordb" def search(vec, topk=10, threshold=0.7): with psycopg.connect(DSN) as conn: register_vector(conn) with conn.cursor() as cur: cur.execute("SET hnsw.ef_search = 100") cur.execute( """ SELECT id, content, 1 - (embedding <=> %s) AS score FROM doc_chunks WHERE embedding IS NOT NULL ORDER BY embedding <=> %s LIMIT %s """, (vec, vec, topk), ) return [(r[0], r[1], r[2]) for r in cur.fetchall() if r[2] >= threshold]

register_vector这一步不能漏。它负责把 numpy 数组或者 Python list 转成 PG 能识别的向量字面量,不注册的话要么报类型转换错,要么被当成字符串存进去。

5.4 执行计划怎么看,索引到底有没有用上

验证索引是否生效,唯一可靠的方式是EXPLAIN ANALYZE:

EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM doc_chunks WHERE embedding IS NOT NULL ORDER BY embedding <=> '[...]' LIMIT 10;

看输出里有没有这一行:

Index Scan using idx_items_embedding_hnsw on doc_chunks ...

如果出现的是Seq Scan,说明索引没用上。常见原因有四个:表太小优化器觉得全表扫更快、opclass 和算子不匹配、ORDER BY里做了表达式运算导致无法走索引、查询里带了WHERE过滤条件导致优化器选择先过滤。

还有一个隐蔽的情况:查询里写了ORDER BY embedding <=> $1 LIMIT 10但同时有WHERE doc_id = 123,如果过滤后行数很少,优化器会先走doc_id索引过滤再排序,结果就是向量索引完全没参与。这种场景要么建组合策略,要么用 0.8 版本之后的迭代扫描。

6. 踩坑实录与排查手册

6.1 扩展装不上、连不上、启动就挂

报错extension "vector" is not available八成是镜像用错了,拉成了postgres:16。检查docker inspect <容器> | grep -i image,确认镜像名是pgvector/pgvector:pg16。也可能是PGDATA指向了一个旧数据目录,那个目录是在没有扩展的环境下初始化的,CREATE EXTENSION找不到控制文件——这种情况检查/usr/share/postgresql/16/extension/vector.control是否存在,存在的话直接连上去手建扩展即可。

报错could not access file "$libdir/vector"扩展的控制文件在,但.so二进制不在或者路径不对。基本只发生在自己编译安装的场景,Docker 镜像不会有这个错。确认用了pg_config --pkglibdir指的目录。

容器起来就退出,docker logs只有一行initdb: error最常见的原因是挂载目录权限不对。容器内 postgres 用户 UID 是 999,宿主机目录如果是 root 拥有且 700 权限,initdb 写不进去。处理办法是先chown -R 999:999 /data/pgvector/pgdata,或者干脆让容器自己创建目录(去掉宿主机上的预先mkdir,让 Docker 生成)。

报错FATAL: password authentication failedPOSTGRES_PASSWORD只在那次初始化时生效。数据目录已经有数据的情况下改这个环境变量完全没用,你得连进去ALTER USER改密码。

现象大概率原因处理方式
启动后立刻退出数据目录权限 / 端口占用看docker logs,调整属主或端口
外部连不上listen_addresses未开 / 端口未映射查 conf 和-p参数
扩展不可用镜像错误换pgvector/pgvector镜像
改密码无效数据库已初始化连进去ALTER USER

6.2 索引相关的坑,一个比一个隐蔽

建索引时被 OOM 干掉报错类似server process was terminated by signal 9: Killed。这就是内存不够,被内核 OOM Killer 杀了。处理顺序:先加maintenance_work_mem,不行就把容器的--memory和--shm-size一起调大,还是不行就分批建——先建部分索引,再合并,或者干脆降维。

维度不匹配:expected 1536 dimensions, not 768换了 embedding 模型但没改表结构。vector的维度是强类型的一部分,改模型必须ALTER TABLE或者重建列。我的建议是建表时把维度写死在表定义里,同时在一张配置表里记录当前使用的模型名和维度,两处对照,换模型时不会漏。

建了 IVFFlat 索引但表是空的IVFFlat 需要在有数据的基础上做聚类,空表建索引会警告并且索引无效。正确顺序是:先灌数据,再建索引。HNSW 没有这个限制,空表也能建。

索引体积比数据大好几倍HNSW 就是这个特性,正常。如果磁盘吃紧,可以考虑halfvec存储,索引体积能砍一半,召回损失通常在 1% 以内,很多场景完全可接受。

6.3 容器层的那些坑

重启后数据全没了-v没挂或者挂载路径写错。检查docker inspect pgvector16 | grep -A5 Mounts,看Source是不是你预期的宿主机路径。

建索引时No space left on device,但df -h显示磁盘还有空间大概率是/dev/shm满了,不是磁盘满了。这就是前面说的--shm-size。用df -h /dev/shm在容器内确认一下:docker exec pgvector16 df -h /dev/shm。

docker compose restart之后参数没生效restart只是重启进程,不会重新读挂载的配置文件之外的东西。如果是改了postgresql.conf,restart能读到;但改了shared_preload_libraries这类需要重载动态库的,必须docker compose down && docker compose up -d真正重建容器。

备份恢复了但向量类型丢了pg_dump会导出CREATE EXTENSION语句,但目标库必须先有扩展可用。恢复顺序是:先在目标库CREATE EXTENSION vector,再pg_restore。反过来会报type "vector" does not exist。

6.4 一个我踩了很久的坑:过滤条件下的召回不足

这个坑值得单独拎出来说。假设查询是这样的:

SELECT id FROM doc_chunks WHERE metadata @> '{"lang":"zh"}' ORDER BY embedding <=> '[...]' LIMIT 10;

如果lang=zh的行只占全表 1%,优化器会让 HNSW 索引先扫ef_search个候选,然后过滤,最后可能只剩 1~2 条,达不到LIMIT 10。表现出来就是"明明有数据,查出来却很少"。

三个应对办法,按推荐度排:

  1. 升级到支持iterative scan的版本,让扫描自动扩大候选范围:
    SET hnsw.iterative_scan = relaxed_order;
  2. 手动调大hnsw.ef_search,代价是查询变慢。
  3. 建部分索引,把过滤条件写进索引定义:
    CREATE INDEX idx_zh_hnsw ON doc_chunks USING hnsw (embedding vector_cosine_ops) WHERE metadata @> '{"lang":"zh"}';

第三个办法效果最好,但索引会变多,写入开销上升。我在中英混合语料的项目上就是给中文和英文各建了一个部分索引,查询性能提升很明显。

7. 我个人的几条使用建议

跑了一段时间下来,有几个习惯我觉得值得养成。

第一,容器里的 PG 参数一律走挂载的postgresql.conf,不要在容器里ALTER SYSTEM。容器是可替换的,配置放在宿主机文件里才能跟着版本控制走,换机器时复制目录就完事。

第二,向量表单独放一个表空间或者至少单独一张表,别跟业务主表混。原因是向量表动不动几十上百 GB,备份策略、清理策略、索引重建周期都跟业务表不一样,混在一起彼此拖累。

第三,ef_search这类查询期参数,在连接池层面设默认值,别指望每个查询都记得写SET。很多驱动支持连接初始化 SQL,把SET hnsw.ef_search = 100塞进去,后面写查询就不用心疼了。

第四,定期跑一次REINDEX检查。HNSW 索引在大量删除之后会留下无效节点,召回下降。可以用pg_stat_user_indexes观察索引大小和扫描次数的变化趋势,尺寸异常膨胀就重建。

第五,别在一个容器里塞全部数据。千万级向量的时候,单机单实例的瓶颈往往出现在建索引的内存峰值和磁盘 IO 上。真到那一步,分区表加多个 HNSW 索引,比硬扛一个超大索引要靠谱得多。

这套环境我从三年前的 0.4 版本一路用到现在,中间经历过两次大版本升级、一次磁盘故障恢复、无数次参数重调。回头看,最难的不是装,是搞清楚它在什么情况下会不按预期工作。把上面这些边界和坑摸清楚之后,pgvector加 Docker 这个组合的稳定性其实相当够用——至少对于绝大多数业务场景来说,它省下来的运维复杂度,远比那几十毫秒的检索差距值钱。

返回列表