优化 PostgreSQL 向量搜索:终极教程 – wiki基地

优化 PostgreSQL 向量搜索:终极教程

引言

在当今数据驱动的世界中,理解和利用非结构化数据变得越来越重要。从推荐系统到语义搜索,再到大语言模型 (LLM) 的应用,向量搜索(或相似性搜索)已成为核心技术。它允许我们通过比较高维向量的相似性来查找最相关的数据点。

PostgreSQL,作为世界上最先进的开源关系型数据库,通过其强大的扩展机制,也能够胜任向量搜索的任务。特别是结合了 pgvector 这样的扩展,PostgreSQL 可以成为一个功能强大且灵活的向量数据库。然而,仅仅安装扩展并不能保证最佳性能。为了在生产环境中实现高效、可扩展的向量搜索,必须深入理解并应用一系列优化策略。

本教程将为您提供一个全面的指南,详细阐述如何在 PostgreSQL 中优化向量搜索,涵盖从索引选择到查询调优,再到数据管理和扩展性的各个方面。

先决条件:pgvector 扩展

在开始之前,确保您的 PostgreSQL 实例已安装并启用了 pgvector 扩展。pgvector 是一个开源的 PostgreSQL 扩展,它为 PostgreSQL 增加了存储、索引和查询向量数据类型的能力。

安装步骤概述:

  1. 安装 pgvector 通常通过包管理器或从源代码编译安装。例如,在基于 Debian 的系统上:
    bash
    sudo apt-get update
    sudo apt-get install postgresql-$(pg_config --pg-version | cut -d. -f1)-pgvector

    或者从源代码:
    bash
    git clone https://github.com/pgvector/pgvector.git
    cd pgvector
    make
    sudo make install
  2. 在数据库中启用扩展:
    连接到您的 PostgreSQL 数据库并执行:
    sql
    CREATE EXTENSION vector;

启用 pgvector 后,您就可以在表中定义 vector 类型列来存储向量嵌入。

sql
CREATE TABLE items (
id SERIAL PRIMARY KEY,
embedding vector(1536) -- 例如,OpenAI ada-002 模型输出的向量维度
);

索引策略:加速相似性搜索

向量搜索的核心挑战之一是在高维空间中高效地找到最近邻。遍历所有向量(暴力搜索)对于大型数据集来说是不可行的。这就是向量索引发挥作用的地方。pgvector 支持多种索引类型,每种都有其适用场景。

1. IVFFlat (Inverted File Index with Flat Quantization)

IVFFlat 是一种近似最近邻 (ANN) 索引,它将向量空间划分为多个聚类。查询时,它只搜索与查询向量最近的几个聚类,从而显著减少了需要比较的向量数量。

何时使用:
* 需要平衡查询速度和召回率(recall)。
* 数据分布相对均匀。
* 数据集大小适中到大型。

创建 IVFFlat 索引:

sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
-- 或者使用余弦相似度:
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

关键参数 lists
* lists 参数定义了聚类的数量。
* 过小: 导致更快的查询,但可能降低召回率(因为相关的向量可能不在被搜索的聚类中)。
* 过大: 提高召回率,但会增加查询时间(因为需要搜索更多的聚类)。
* 经验法则: 建议将 lists 设置为 num_rows / 1000num_rows / 10 之间,或者在您的数据集中至少是 sqrt(num_rows)。对于少于 100 万行的数据,可以从 lists = num_rows / 100 开始。

2. HNSW (Hierarchical Navigable Small World)

HNSW 是一种更先进的 ANN 索引,它构建了一个多层图结构。在查询时,它从顶层开始,快速导航到查询点附近,然后在较低层进行更精细的搜索。HNSW 通常提供比 IVFFlat 更好的召回率和查询速度。

何时使用:
* 对召回率和查询速度都有较高要求。
* 数据集非常大。
* 可以接受更高的索引构建时间和内存消耗。

创建 HNSW 索引:

sql
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
-- 或者使用余弦相似度:
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

关键参数:
* m (Maximum number of connections per node): 每个节点的最大连接数。
* 过小: 索引构建速度快,内存占用少,但可能降低召回率。
* 过大: 提高召回率,但索引构建速度慢,内存占用高。
* 经验法则: 8 到 64 之间,常见值是 16。
* ef_construction (Size of the dynamic list during construction): 索引构建过程中,搜索的候选邻居数量。
* 过小: 索引构建速度快,但可能导致索引质量差,召回率低。
* 过大: 提高索引质量和召回率,但索引构建速度慢,内存占用高。
* 经验法则: 40 到 500 之间,通常是 m * 2 或更高,建议从 64 开始。

选择索引类型总结:
* 对于需要更高准确性和性能的大型数据集,HNSW 通常是更好的选择,但会消耗更多资源。
* 对于内存和构建时间敏感,或数据集适中的场景,IVFFlat 仍是一个不错的选择。

3. 不带索引的暴力搜索

如果数据量非常小(例如,少于几千行),或者您需要 100% 的召回率(精确最近邻),则可以不使用 ANN 索引,直接进行暴力搜索。

sql
SELECT id, embedding <-> '[...query_vector...]' AS distance
FROM items
ORDER BY distance
LIMIT 5;

这种方法在数据量很小的情况下可能比构建和维护索引更快。

查询优化:高效地执行相似性搜索

即使有了正确的索引,编写高效的查询也是至关重要的。

1. 使用 LIMIT

始终在您的相似性查询中使用 LIMIT 子句。向量搜索通常只需要返回少数最相似的结果,LIMIT 可以极大地减少需要处理的数据量。

sql
SELECT id, embedding <-> '[...query_vector...]' AS distance
FROM items
ORDER BY distance
LIMIT 10;

2. 调整 ef_search (HNSW) 或 ivfflat.probes (IVFFlat)

这些参数在查询时控制索引搜索的广度,直接影响召回率和查询速度。

  • HNSW 的 ef_search
    • 通过 SET hnsw.ef_search = N; 在会话级别设置,或作为查询提示。
    • N 越大,搜索越精确,召回率越高,但查询速度越慢。
    • N 应该至少与 LIMIT 值一样大,通常是 LIMIT * 2 或更高。
  • IVFFlat 的 ivfflat.probes
    • 通过 SET ivfflat.probes = N; 在会话级别设置。
    • N 越大,搜索的聚类越多,召回率越高,但查询速度越慢。
    • N 应该小于或等于 lists 参数。

示例 (HNSW):

sql
SET hnsw.ef_search = 100; -- 假设您期望返回 10 个结果,且 m=16, ef_construction=64
SELECT id, embedding <-> '[...query_vector...]' AS distance
FROM items
ORDER BY distance
LIMIT 10;

示例 (IVFFlat):

sql
SET ivfflat.probes = 10; -- 假设 lists=100
SELECT id, embedding <-> '[...query_vector...]' AS distance
FROM items
ORDER BY distance
LIMIT 10;

调优原则:
在开发和测试环境中,逐渐增加 ef_searchivfflat.probes 的值,同时监测查询时间和召回率,直到找到满足您业务需求的最优平衡点。

3. 避免在 WHERE 子句中过滤高维向量列

WHERE 子句中直接对 vector 列进行复杂过滤操作(除了直接相等比较)通常会导致全表扫描或索引失效。如果需要过滤,请尽可能在向量搜索之前或之后使用其他常规索引列进行过滤。

“`sql
— 较差的性能:可能无法有效利用向量索引
SELECT id, embedding <-> ‘[…query_vector…]’ AS distance
FROM items
WHERE embedding[0] > 0.5 — 避免这种类型的过滤
ORDER BY distance
LIMIT 10;

— 更好的方法:在向量搜索之前或之后进行过滤
SELECT id, distance
FROM (
SELECT id, category, embedding <-> ‘[…query_vector…]’ AS distance
FROM items
ORDER BY distance
LIMIT 100 — 先取多一些结果
) AS subquery
WHERE category = ‘electronics’ — 再基于其他列过滤
ORDER BY distance
LIMIT 10;
“`
如果过滤条件非常严格且能够显著减少搜索空间,可以考虑先过滤,再对过滤后的子集进行向量搜索。但通常情况下,向量索引会首先找到最近邻,再对这些邻居进行二次过滤。

数据管理:高效存储和维护向量

1. 选择正确的向量维度

向量的维度对性能有显著影响。维度越高,存储空间越大,计算复杂度越高,索引构建和查询速度越慢。选择一个既能捕捉必要信息又不过分冗余的维度。大多数预训练模型(如 OpenAI 的 text-embedding-ada-002)生成 1536 维的向量,这是常见的选择。

2. 定期 VACUUMANALYZE

pgvector 索引与其他 PostgreSQL 索引一样,受益于定期的 VACUUMANALYZE
* VACUUM 回收被删除或更新行占用的空间。
* ANALYZE 更新统计信息,帮助查询优化器选择最佳执行计划。
对于频繁更新的表,可能需要更频繁地执行这些操作,或者配置自动清理。

3. 批量插入和更新

插入或更新大量向量时,使用批量操作而不是单行操作可以显著提高效率。
sql
INSERT INTO items (embedding) VALUES
('[...vector_1...]'),
('[...vector_2...]'),
-- ...
('[...vector_N...]');

4. 数据类型考虑

pgvector 存储向量为 float4[] 类型。确保您的应用程序在生成和使用向量时与此数据类型兼容。

性能调优:PostgreSQL 配置

除了 pgvector 特定的参数外,标准的 PostgreSQL 配置参数也对向量搜索性能有影响。

1. shared_buffers

增加 shared_buffers 可以让 PostgreSQL 在内存中缓存更多数据和索引页,减少磁盘 I/O。对于大量读操作的向量搜索,这至关重要。

“`ini

postgresql.conf

shared_buffers = 4GB # 根据服务器可用内存调整,通常是总内存的 25% 左右
“`

2. work_mem

work_mem 控制内部排序和哈希表操作使用的内存量。复杂的查询(例如涉及大量排序的查询)可能会受益于更大的 work_mem

“`ini

postgresql.conf

work_mem = 256MB # 根据并发查询数量和查询复杂度调整
“`

3. effective_cache_size

effective_cache_size 告诉查询优化器可用于缓存数据的总内存量(包括 OS 缓存)。这个值不分配内存,但会影响优化器的决策。

“`ini

postgresql.conf

effective_cache_size = 12GB # 通常设置为总内存的 50% – 75%
“`

4. maintenance_work_mem

maintenance_work_mem 用于维护操作,如 CREATE INDEXVACUUMALTER TABLE ADD FOREIGN KEY。在构建大型向量索引时,增加此值可以显著加速索引创建过程。

“`ini

postgresql.conf

maintenance_work_mem = 1GB # 在创建或重建大型索引时临时增加
“`

5. random_page_costseq_page_cost

这些参数是查询优化器用于估算磁盘 I/O 成本的。对于 SSD 存储,random_page_cost 可以适当降低,因为随机读的成本接近顺序读。

“`ini

postgresql.conf

random_page_cost = 1.1 # 默认 4.0,SSD 盘可以设置为 1.1-2.0
seq_page_cost = 1.0 # 默认 1.0
“`

扩展性考虑

当数据量和查询负载持续增长时,单一 PostgreSQL 实例可能无法满足需求。

1. 只读副本

对于读密集的向量搜索工作负载,可以设置 PostgreSQL 只读副本。将向量搜索查询路由到这些副本上,从而分散主数据库的压力。

2. 分区 (Partitioning)

如果您的数据集可以基于某个键(例如时间、类别)进行逻辑分区,PostgreSQL 的声明式分区功能可以帮助您管理非常大的表。然而,请注意,跨分区进行向量搜索可能需要额外的复杂性,因为它可能无法直接在所有分区上有效地利用单个全局向量索引。通常,向量索引是在每个分区上独立创建的。

3. 外部数据包装器 (Foreign Data Wrappers – FDW)

虽然不是直接的扩展性解决方案,但 FDW 允许您将 PostgreSQL 连接到其他外部数据源,包括其他 PostgreSQL 实例。这在某些分片架构中可能有用,但增加了查询的复杂性。

4. 分片 (Sharding)

对于极大规模的数据集,您可能需要考虑分片。将数据分布到多个 PostgreSQL 实例上,每个实例管理数据的一个子集。这通常需要应用程序层面的逻辑来确定哪个分片包含相关数据,并在每个分片上执行向量搜索,然后聚合结果。这个方案会显著增加系统复杂性。

5. 考虑专门的向量数据库 (在某些情况下)

虽然 PostgreSQL + pgvector 功能强大,但在某些极端规模、超高性能或特定功能需求(如混合搜索)的场景下,专门的向量数据库(如 Milvus, Weaviate, Pinecone, Qdrant)可能提供更优的解决方案。它们通常从底层为向量操作进行设计,提供更高级的分布式能力、过滤和混合搜索功能。权衡您的需求、资源和现有基础设施来做出选择。

结论

PostgreSQL 结合 pgvector 为向量搜索提供了一个强大而灵活的平台。通过选择正确的索引策略(IVFFlat 或 HNSW),精细调整索引和查询参数,以及优化 PostgreSQL 的核心配置,您可以显著提升向量搜索的性能。在数据量和负载增长时,考虑采用只读副本、分区甚至分片等扩展策略。最终,理解您的数据、查询模式和性能要求是实现高效 PostgreSQL 向量搜索的关键。

滚动至顶部