共用方式為


如何在 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 PlannedWorkers 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_TYPEivfflathnsw)。

雖然您可以儲存超過 2000 個維度的向量,但無法為其編製索引。 您可以使用維度縮減來符合限制。 或者,使用 Azure Cosmos DB for PostgreSQL 進行資料分割和/或分區化,在不編製索引的情況下達到可接受的效能。

具有一般壓縮的反轉檔案 (IVVFlat)

ivfflat 是近似最接近像素 (ANN) 搜尋的索引。 這個方法會使用反向檔案索引,將資料集分割成多份清單。 探查參數會控制要搜尋多少清單,這可藉由較慢的搜尋速度來改善搜尋結果的正確性。

如果探查參數已設為索引中的清單數目,系統會搜尋所有清單,而這筆搜尋就會變成最接近像素的精確搜尋。 在此情況下,規劃工具不會使用索引,因為搜尋所有清單相當於對整個資料集執行暴力搜尋。

索引方法會使用 k-means 叢集演算法,將資料集分割成多份清單。 每個清單都包含最接近特定叢集中心的向量。 在搜尋期間,查詢向量會與叢集中心進行比較,從而判斷哪些清單最有可能包含最接近像素。 如果探查參數設定為 1,則只會搜尋對應至最接近叢集中心的清單。

索引選項

選取要執行之探查數目的正確值,而清單的大小可能會影響搜尋效能。 建議從以下幾點開始著手:

  1. 如果是不超過 100 萬個資料列的表格,使用 lists 等於 rows / 1000;如果資料集規模更大,則使用 sqrt(rows)
  2. 如果是不超過 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 索引有以下幾個階段:

  1. initializing
  2. performing k-means
  3. assigning tuples
  4. loading tuples

注意

進度百分比 (%) 只會在階段期間 loading tuples 填入。

階層式導覽小型世界 (HNSW)

hnsw 是使用「階層式導覽小型世界」演算法的近似最接近像素 (ANN) 的索引。 這個索引的運作方式是在隨機選取的進入點周圍建立圖表,以尋找最接近的像素,然後以多個圖層延伸圖形,每個較低的圖層都包含更多點。 從頂端開始搜尋時,此多層圖表會縮小,直至抵達包含查詢最接近像素的最底層為止。

建置此索引所需的時間和記憶體比 IVFFlat 多,但只需犧牲些許速度就能提高叫用效果。 此外,沒有類似 IVFFlat 的訓練步驟,因此可以在空白資料表上建立索引。

索引選項

建立索引時,您可以微調兩個參數:

  1. m - 每個圖層的連線數目上限 (預設值為 16)
  2. 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 索引可分成以下幾個階段:

  1. initializing
  2. 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 達到最佳效能的取捨、限制和最佳做法。