如何在 Azure Cosmos DB for PostgreSQL 上使用 Pgvector 時最佳化效能
適用於: Azure Cosmos DB for PostgreSQL (由 Citus 資料庫延伸模組支援 PostgreSQL)
pgvector
延伸模組會將開放原始碼向量相似度搜尋新增至 PostgreSQL。
本文探討 pgvector
的限制和取捨,並示範如何使用資料分割、編製索引和搜尋設定來提升效能。
如需延伸模組本身的詳細資訊,請參閱 pgvector
的基本概念。 建議您參閱專案的官方讀我檔案。
效能
您應一律從調查查詢計畫開始。 如果查詢很快就結束,請執行 EXPLAIN (ANALYZE,VERBOSE, BUFFERS)
。
EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
若查詢執行時間太長,請考慮卸除 ANALYZE
關鍵詞。 查詢結果包含較少詳細資料,但會立即提供。
EXPLAIN (VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
explain.depesz.com 等協力廠商網站有助於瞭解查詢計畫。 建議您試著回答以下幾個問題:
如果您的向量標準化為長度 1,例如 OpenAI 內嵌。 建議您使用內部產品 (<#>
) 以獲得最佳效能。
平行執行
在說明計畫的輸出中,尋找 Workers Planned
和 Workers Launched
(只有在使用 ANALYZE
關鍵詞時,才會使用後者)。 max_parallel_workers_per_gather
PostgreSQL 參數會定義資料庫可為每個 Gather
節點和 Gather Merge
計畫節點啟動多少背景工作角色。 增加此值可能會加快精確搜尋的查詢速度,而不需要建立索引。 還請注意,即使此值很高,資料庫可能也不會決定平行執行計畫。
EXPLAIN SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 3;
QUERY PLAN
------------------------------------------------------------------------------------------
Limit (cost=4214.82..4215.16 rows=3 width=33)
-> Gather Merge (cost=4214.82..13961.30 rows=84752 width=33)
Workers Planned: 1
-> Sort (cost=3214.81..3426.69 rows=84752 width=33)
Sort Key: ((embedding <-> '[1,2,3]'::vector))
-> Parallel Seq Scan on t_test (cost=0.00..2119.40 rows=84752 width=33)
(6 rows)
編製索引
如果沒有索引,延伸模組會執行精確搜尋,但要犧牲效能才能提供完美的叫用效果。
若要執行近似的最接近像素搜尋,建議您為資料建立索引,以換取執行效能的叫用效果。
可能的話,請一律先載入資料,再編製索引。 以這種方式建立索引的速度較快,所產生的配置也比較理想。
支援的索引類型有兩種:
索引 IVFFlat
的建置時間較快,且使用的記憶體低於 HNSW
,但查詢效能較低 (就速度和叫用效果之間的取捨而言)。
限制
- 若要為資料行編製索引,就必須定義維度。 嘗試為定義為
col vector
的資料行編製索引會導致錯誤:ERROR: column does not have dimensions
。 - 最多只能編製 2000 個維度的資料行索引。 嘗試為維度更多的資料行編製索引會導致錯誤:
ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index
(其中INDEX_TYPE
是ivfflat
或hnsw
)。
雖然您可以儲存超過 2000 個維度的向量,但無法為其編製索引。 您可以使用維度縮減來符合限制。 或者,使用 Azure Cosmos DB for PostgreSQL 進行資料分割和/或分區化,在不編製索引的情況下達到可接受的效能。
具有一般壓縮的反轉檔案 (IVVFlat)
ivfflat
是近似最接近像素 (ANN) 搜尋的索引。 這個方法會使用反向檔案索引,將資料集分割成多份清單。 探查參數會控制要搜尋多少清單,這可藉由較慢的搜尋速度來改善搜尋結果的正確性。
如果探查參數已設為索引中的清單數目,系統會搜尋所有清單,而這筆搜尋就會變成最接近像素的精確搜尋。 在此情況下,規劃工具不會使用索引,因為搜尋所有清單相當於對整個資料集執行暴力搜尋。
索引方法會使用 k-means 叢集演算法,將資料集分割成多份清單。 每個清單都包含最接近特定叢集中心的向量。 在搜尋期間,查詢向量會與叢集中心進行比較,從而判斷哪些清單最有可能包含最接近像素。 如果探查參數設定為 1,則只會搜尋對應至最接近叢集中心的清單。
索引選項
選取要執行之探查數目的正確值,而清單的大小可能會影響搜尋效能。 建議從以下幾點開始著手:
- 如果是不超過 100 萬個資料列的表格,使用
lists
等於rows / 1000
;如果資料集規模更大,則使用sqrt(rows)
。 - 如果是不超過 100 萬個資料列的表格,使用從
lists / 10
開始的probes
;如果資料集規模更大,則使用sqrt(lists)
。
使用 lists
選項在建立索引時定義 lists
的數量:
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 5000);
您可以為整個連線或每筆交易設定探查 (在交易區塊內使用 SET LOCAL
):
SET ivfflat.probes = 10;
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 10 probes
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 10 probes
BEGIN;
SET LOCAL ivfflat.probes = 10;
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 10 probes
COMMIT;
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses default, one probe
編製索引進度
若採用 PostgreSQL 12 和更新的版本,即可使用 pg_stat_progress_create_index
來查看編製索引進度。
SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
建置 IVFFlat 索引有以下幾個階段:
initializing
performing k-means
assigning tuples
loading tuples
注意
進度百分比 (%
) 只會在階段期間 loading tuples
填入。
階層式導覽小型世界 (HNSW)
hnsw
是使用「階層式導覽小型世界」演算法的近似最接近像素 (ANN) 的索引。 這個索引的運作方式是在隨機選取的進入點周圍建立圖表,以尋找最接近的像素,然後以多個圖層延伸圖形,每個較低的圖層都包含更多點。 從頂端開始搜尋時,此多層圖表會縮小,直至抵達包含查詢最接近像素的最底層為止。
建置此索引所需的時間和記憶體比 IVFFlat 多,但只需犧牲些許速度就能提高叫用效果。 此外,沒有類似 IVFFlat 的訓練步驟,因此可以在空白資料表上建立索引。
索引選項
建立索引時,您可以微調兩個參數:
m
- 每個圖層的連線數目上限 (預設值為 16)ef_construction
- 圖形建構的動態候選清單大小 (預設值為 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
在查詢期間,您可以指定搜尋的動態候選清單 (預設值為 40)。
搜尋的動態候選清單可為整個連線或每筆交易設定 (在交易區塊內使用 SET LOCAL
):
SET hnsw.ef_search = 100;
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 100 candidates
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 100 candidates
BEGIN;
SET hnsw.ef_search = 100;
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 100 candidates
COMMIT;
SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses default, 40 candidates
編製索引進度
若採用 PostgreSQL 12 和更新的版本,即可使用 pg_stat_progress_create_index
來查看編製索引進度。
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
建置 HNSW 索引可分成以下幾個階段:
initializing
loading tuples
選取索引存取函式
此 vector
類型可讓您對預存向量執行三種搜尋。 您必須為索引選取正確的存取函式,才能讓資料庫在執行查詢時將您的索引納入考量。 這些範例在 ivfflat
索引類型提供示範,但對於 hnsw
索引而言,可以執行相同的動作。 選項 lists
僅適用於 ivfflat
索引。
餘弦距離
如需執行餘弦相似度搜尋,請使用 vector_cosine_ops
存取機制。
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
若要使用上述索引,查詢必須執行餘弦相似度搜尋,這是使用 <=>
運算子來完成操作。
EXPLAIN SELECT * FROM t_test ORDER BY embedding <=> '[1,2,3]' LIMIT 5;
QUERY PLAN
------------------------------------------------------------------------------------------------------
Limit (cost=5.02..5.23 rows=5 width=33)
-> Index Scan using t_test_embedding_cosine_idx on t_test (cost=5.02..175.06 rows=4003 width=33)
Order By: (embedding <=> '[1,2,3]'::vector)
(3 rows)
L2 距離
針對 L2 距離 (也稱為歐幾里得距離),請使用 vector_l2_ops
存取機制。
CREATE INDEX t_test_embedding_l2_idx ON t_test USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
若要使用上述索引,查詢必須使用 <->
運算子來執行 L2 距離搜尋。
EXPLAIN SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
QUERY PLAN
--------------------------------------------------------------------------------------------------
Limit (cost=5.02..5.23 rows=5 width=33)
-> Index Scan using t_test_embedding_l2_idx on t_test (cost=5.02..175.06 rows=4003 width=33)
Order By: (embedding <-> '[1,2,3]'::vector)
(3 rows)
內積
對於內部產品相似性,請使用 vector_ip_ops
存取機制。
CREATE INDEX t_test_embedding_ip_idx ON t_test USING ivfflat (embedding vector_ip_ops) WITH (lists = 100);
若要使用上述索引,查詢必須使用 <#>
運算子來執行內部產品相似度搜尋。
EXPLAIN SELECT * FROM t_test ORDER BY embedding <#> '[1,2,3]' LIMIT 5;
QUERY PLAN
--------------------------------------------------------------------------------------------------
Limit (cost=5.02..5.23 rows=5 width=33)
-> Index Scan using t_test_embedding_ip_idx on t_test (cost=5.02..175.06 rows=4003 width=33)
Order By: (embedding <#> '[1,2,3]'::vector)
(3 rows)
部分索引
在某些情況下,只涵蓋部分資料集的索引很有幫助。 例如,我們可為進階使用者編製索引:
CREATE INDEX t_premium ON t_test USING ivfflat (vec vector_ip_ops) WITH (lists = 100) WHERE tier = 'premium';
我們現在可以看到進階層現已使用索引:
explain select * from t_test where tier = 'premium' order by vec <#> '[2,2,2]';
QUERY PLAN
------------------------------------------------------------------------------------
Index Scan using t_premium on t_test (cost=65.57..25638.05 rows=245478 width=39)
Order By: (vec <#> '[2,2,2]'::vector)
(2 rows)
雖然免費層使用者無法享有以下優點:
explain select * from t_test where tier = 'free' order by vec <#> '[2,2,2]';
QUERY PLAN
-----------------------------------------------------------------------
Sort (cost=44019.01..44631.37 rows=244941 width=39)
Sort Key: ((vec <#> '[2,2,2]'::vector))
-> Seq Scan on t_test (cost=0.00..15395.25 rows=244941 width=39)
Filter: (tier = 'free'::text)
(4 rows)
只有資料編製索引的子集,表示索引在磁碟的空間較少,而且搜尋速度較快。
如果部分索引定義的 WHERE
子句使用的表單不符合查詢所使用的表單,PostgreSQL 可能無法辨識索引可否安全使用。
在範例資料集中,我們只有 'free'
、'test'
和 'premium'
這些確切的值可作為層資料行的相異值。 即使使用 tier LIKE 'premium'
PostgreSQL 的查詢也不會使用索引。
explain select * from t_test where tier like 'premium' order by vec <#> '[2,2,2]';
QUERY PLAN
-----------------------------------------------------------------------
Sort (cost=44086.30..44700.00 rows=245478 width=39)
Sort Key: ((vec <#> '[2,2,2]'::vector))
-> Seq Scan on t_test (cost=0.00..15396.59 rows=245478 width=39)
Filter: (tier ~~ 'premium'::text)
(4 rows)
資料分割
改善效能的其中一種方法是將資料集分割成多個分割區。 假如有一個系統,自然會參考當年度或過去兩年的資料。 在這類系統中,您可以依日期範圍分割資料,然後在系統能夠讀取查詢年份所定義的相關分割區時,善加運用提升後的效能。
資料分割資料表定義如下:
CREATE TABLE t_test_partitioned(vec vector(3), vec_date date default now()) partition by range (vec_date);
我們可以手動建立每年的資料分割或使用 Citus 公用程式函式 (可在 Cosmos DB for PostgreSQL 上使用)。
select create_time_partitions(
table_name := 't_test_partitioned',
partition_interval := '1 year',
start_from := '2020-01-01'::timestamptz,
end_at := '2024-01-01'::timestamptz
);
檢查已建立的分割區:
\d+ t_test_partitioned
Partitioned table "public.t_test_partitioned"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
----------+-----------+-----------+----------+---------+----------+-------------+--------------+-------------
vec | vector(3) | | | | extended | | |
vec_date | date | | | now() | plain | | |
Partition key: RANGE (vec_date)
Partitions: t_test_partitioned_p2020 FOR VALUES FROM ('2020-01-01') TO ('2021-01-01'),
t_test_partitioned_p2021 FOR VALUES FROM ('2021-01-01') TO ('2022-01-01'),
t_test_partitioned_p2022 FOR VALUES FROM ('2022-01-01') TO ('2023-01-01'),
t_test_partitioned_p2023 FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')
若要手動建立分割區:
CREATE TABLE t_test_partitioned_p2019 PARTITION OF t_test_partitioned FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
然後,請確認您的查詢確實會向下篩選至可用分割區的子集。 例如,我們在下列查詢中篩選至兩個資料分割:
explain analyze select * from t_test_partitioned where vec_date between '2022-01-01' and '2024-01-01';
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=0.00..58.16 rows=12 width=36) (actual time=0.014..0.018 rows=3 loops=1)
-> Seq Scan on t_test_partitioned_p2022 t_test_partitioned_1 (cost=0.00..29.05 rows=6 width=36) (actual time=0.013..0.014 rows=1 loops=1)
Filter: ((vec_date >= '2022-01-01'::date) AND (vec_date <= '2024-01-01'::date))
-> Seq Scan on t_test_partitioned_p2023 t_test_partitioned_2 (cost=0.00..29.05 rows=6 width=36) (actual time=0.002..0.003 rows=2 loops=1)
Filter: ((vec_date >= '2022-01-01'::date) AND (vec_date <= '2024-01-01'::date))
Planning Time: 0.125 ms
Execution Time: 0.036 ms
您可以為資料分割資料表編製索引。
CREATE INDEX ON t_test_partitioned USING ivfflat (vec vector_cosine_ops) WITH (lists = 100);
explain analyze select * from t_test_partitioned where vec_date between '2022-01-01' and '2024-01-01' order by vec <=> '[1,2,3]' limit 5;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=4.13..12.20 rows=2 width=44) (actual time=0.040..0.042 rows=1 loops=1)
-> Merge Append (cost=4.13..12.20 rows=2 width=44) (actual time=0.039..0.040 rows=1 loops=1)
Sort Key: ((t_test_partitioned.vec <=> '[1,2,3]'::vector))
-> Index Scan using t_test_partitioned_p2022_vec_idx on t_test_partitioned_p2022 t_test_partitioned_1 (cost=0.04..4.06 rows=1 width=44) (actual time=0.022..0.023 rows=0 loops=1)
Order By: (vec <=> '[1,2,3]'::vector)
Filter: ((vec_date >= '2022-01-01'::date) AND (vec_date <= '2024-01-01'::date))
-> Index Scan using t_test_partitioned_p2023_vec_idx on t_test_partitioned_p2023 t_test_partitioned_2 (cost=4.08..8.11 rows=1 width=44) (actual time=0.015..0.016 rows=1 loops=1)
Order By: (vec <=> '[1,2,3]'::vector)
Filter: ((vec_date >= '2022-01-01'::date) AND (vec_date <= '2024-01-01'::date))
Planning Time: 0.167 ms
Execution Time: 0.139 ms
(11 rows)
推論
恭喜您,您剛剛瞭解使用 pgvector
達到最佳效能的取捨、限制和最佳做法。