Bagikan melalui


Cara mengoptimalkan performa saat menggunakan pgvector di Azure Cosmos DB for PostgreSQL

BERLAKU UNTUK: Azure Cosmos DB for PostgreSQL (didukung oleh ekstensi database Citus ke PostgreSQL)

Ekstensi ini pgvector menambahkan pencarian kesamaan vektor sumber terbuka ke PostgreSQL.

Artikel ini mengeksplorasi batasan dan tradeoff pgvector dan menunjukkan cara menggunakan pengaturan partisi, pengindeksan, dan pencarian untuk meningkatkan performa.

Untuk informasi selengkapnya tentang ekstensi itu sendiri, lihat dasar-dasar pgvector. Anda mungkin juga ingin merujuk ke README resmi proyek.

Performa

Anda harus selalu mulai dengan menyelidiki rencana kueri. Jika kueri Anda berakhir dengan cukup cepat, jalankan EXPLAIN (ANALYZE,VERBOSE, BUFFERS).

EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;

Untuk kueri yang membutuhkan waktu terlalu lama untuk dijalankan, pertimbangkan untuk menghilangkan ANALYZE kata kunci. Hasilnya berisi lebih sedikit detail tetapi disediakan secara instan.

EXPLAIN (VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;

Situs pihak ketiga, seperti explain.depesz.com dapat membantu dalam memahami rencana kueri. Beberapa pertanyaan yang harus Anda coba jawab adalah:

Jika vektor Anda dinormalisasi ke panjang 1, seperti penyematan OpenAI. Anda harus mempertimbangkan untuk menggunakan produk dalam (<#>) untuk performa terbaik.

Eksekusi paralel

Dalam output rencana penjelasan Anda, cari Workers Planned dan Workers Launched (yang terakhir hanya ketika ANALYZE kata kunci digunakan). Parameter max_parallel_workers_per_gather PostgreSQL menentukan berapa banyak pekerja latar belakang yang dapat diluncurkan database untuk setiap Gather simpul paket dan Gather Merge . Meningkatkan nilai ini mungkin mempercepat kueri pencarian Anda yang tepat tanpa harus membuat indeks. Namun, perhatikan bahwa database mungkin tidak memutuskan untuk menjalankan rencana secara paralel bahkan ketika nilai ini tinggi.

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)

Pengindeksan

Tanpa indeks yang ada, ekstensi melakukan pencarian yang tepat, yang memberikan pengenalan sempurna dengan mengorbankan performa.

Untuk melakukan perkiraan pencarian tetangga terdekat, Anda dapat membuat indeks pada data Anda, yang berdagang pengenalan untuk performa eksekusi.

Jika memungkinkan, selalu muat data Anda sebelum mengindeksnya. Keduanya lebih cepat untuk membuat indeks dengan cara ini dan tata letak yang dihasilkan lebih optimal.

Ada dua jenis indeks yang didukung:

Indeks IVFFlat memiliki waktu build yang lebih cepat dan menggunakan lebih sedikit memori daripada HNSW, tetapi memiliki performa kueri yang lebih rendah (dalam hal tradeoff pengenalan kecepatan).

Batas

  • Untuk mengindeks kolom, kolom harus memiliki dimensi yang ditentukan. Mencoba mengindeks kolom yang didefinisikan sebagai col vector hasil dalam kesalahan: ERROR: column does not have dimensions.
  • Anda hanya dapat mengindeks kolom yang memiliki hingga 2000 dimensi. Mencoba mengindeks kolom dengan lebih banyak dimensi menghasilkan kesalahan: ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index di mana INDEX_TYPE adalah ivfflat atau hnsw.

Meskipun Anda dapat menyimpan vektor dengan lebih dari 2000 dimensi, Anda tidak dapat mengindeksnya. Anda dapat menggunakan pengurangan dimensi agar sesuai dengan batas. Atau, mengandalkan pemartisian dan/atau sharding dengan Azure Cosmos DB for PostgreSQL untuk mencapai performa yang dapat diterima tanpa pengindeksan.

File Terbalik dengan Kompresi Datar (IVVFlat)

ivfflat adalah indeks untuk perkiraan pencarian tetangga terdekat (ANN). Metode ini menggunakan indeks file terbalik untuk mempartisi himpunan data ke dalam beberapa daftar. Parameter pemeriksaan mengontrol berapa banyak daftar yang dicari, yang dapat meningkatkan akurasi hasil pencarian dengan biaya kecepatan pencarian yang lebih lambat.

Jika parameter pemeriksaan diatur ke jumlah daftar dalam indeks, maka semua daftar dicari dan pencarian menjadi pencarian tetangga terdekat yang tepat. Dalam hal ini, perencana tidak menggunakan indeks karena mencari semua daftar setara dengan melakukan pencarian brute-force pada seluruh himpunan data.

Metode pengindeksan mempartisi himpunan data ke dalam beberapa daftar menggunakan algoritma pengklusteran k-means. Setiap daftar berisi vektor yang paling dekat dengan pusat kluster tertentu. Selama pencarian, vektor kueri dibandingkan dengan pusat kluster untuk menentukan daftar mana yang kemungkinan besar berisi tetangga terdekat. Jika parameter probe diatur ke 1, maka hanya daftar yang sesuai dengan pusat kluster terdekat yang akan dicari.

Opsi indeks

Memilih nilai yang benar untuk jumlah pemeriksaan yang akan dilakukan dan ukuran daftar dapat memengaruhi performa pencarian. Tempat yang bagus untuk memulai adalah:

  1. Gunakan lists sama rows / 1000 dengan untuk tabel dengan hingga 1 juta baris dan sqrt(rows) untuk himpunan data yang lebih besar.
  2. Untuk probes memulai dengan lists / 10 untuk tabel hingga 1 juta baris dan sqrt(lists) untuk himpunan data yang lebih besar.

Jumlah lists didefinisikan setelah pembuatan indeks dengan lists opsi :

CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 5000);

Pemeriksaan dapat diatur untuk seluruh koneksi atau per transaksi (menggunakan SET LOCAL dalam blok transaksi):

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

Kemajuan pengindeksan

Dengan PostgreSQL 12 dan yang lebih baru, Anda dapat menggunakan pg_stat_progress_create_index untuk memeriksa kemajuan pengindeksan.

SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;

Fase untuk membangun indeks IVFFlat adalah:

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

Catatan

Persentase kemajuan (%) hanya diisi selama loading tuples fase.

Dunia Kecil yang Dapat Dinavigasi Hierarkis (HNSW)

hnsw adalah indeks untuk perkiraan pencarian tetangga terdekat (ANN) menggunakan algoritma Dunia Kecil yang Dapat Dinavigasi Hierarkis. Ini bekerja dengan membuat grafik di sekitar titik masuk yang dipilih secara acak yang menemukan tetangga terdekat mereka, grafik kemudian diperluas dengan beberapa lapisan, setiap lapisan bawah yang berisi lebih banyak titik. Grafik multilayer ini ketika dicari dimulai di bagian atas, mempersempit hingga mencapai lapisan terendah yang berisi tetangga terdekat kueri.

Membangun indeks ini membutuhkan lebih banyak waktu dan memori daripada IVFFlat, namun memiliki tradeoff pengenalan kecepatan yang lebih baik. Selain itu, tidak ada langkah pelatihan seperti IVFFlat, sehingga indeks dapat dibuat pada tabel kosong.

Opsi indeks

Saat membuat indeks, Anda dapat menyetel dua parameter:

  1. m - jumlah maksimum koneksi per lapisan (default ke 16)
  2. ef_construction - ukuran daftar kandidat dinamis yang digunakan untuk konstruksi grafik (default ke 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);

Selama kueri, Anda dapat menentukan daftar kandidat dinamis untuk pencarian (default ke 40).

Daftar kandidat dinamis untuk pencarian dapat diatur untuk seluruh koneksi atau per transaksi (menggunakan SET LOCAL dalam blok transaksi):

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

Kemajuan pengindeksan

Dengan PostgreSQL 12 dan yang lebih baru, Anda dapat menggunakan pg_stat_progress_create_index untuk memeriksa kemajuan pengindeksan.

SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;

Fase untuk membangun indeks HNSW adalah:

  1. initializing
  2. loading tuples

Memilih fungsi akses indeks

Jenis ini vector memungkinkan Anda untuk melakukan tiga jenis pencarian pada vektor yang disimpan. Anda perlu memilih fungsi akses yang benar untuk indeks Anda agar database mempertimbangkan indeks Anda saat menjalankan kueri Anda. Contoh yang ditunjukkan pada ivfflat jenis indeks, namun hal yang sama dapat dilakukan untuk hnsw indeks. Opsi lists hanya berlaku untuk ivfflat indeks.

Jarak kosinus

Untuk pencarian kesamaan kosinus, gunakan vector_cosine_ops metode akses.

CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

Untuk menggunakan indeks di atas, kueri perlu melakukan pencarian kesamaan kosinus, yang dilakukan dengan <=> operator.

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)

Jarak L2

Untuk jarak L2 (juga dikenal sebagai jarak Euclidean), gunakan vector_l2_ops metode akses.

CREATE INDEX t_test_embedding_l2_idx ON t_test USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);

Untuk menggunakan indeks di atas, kueri perlu melakukan pencarian jarak L2, yang dilakukan dengan <-> operator.

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)

Produk dalam

Untuk kesamaan vector_ip_ops produk dalam, gunakan metode akses.

CREATE INDEX t_test_embedding_ip_idx ON t_test USING ivfflat (embedding vector_ip_ops) WITH (lists = 100);

Untuk menggunakan indeks di atas, kueri perlu melakukan pencarian kesamaan produk dalam, yang dilakukan dengan <#> operator.

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)

Indeks parsial

Dalam beberapa skenario, bermanfaat untuk memiliki indeks yang hanya mencakup sekumpulan data parsial. Kita dapat, misalnya, membangun indeks hanya untuk pengguna premium kita:

CREATE INDEX t_premium ON t_test USING ivfflat (vec vector_ip_ops) WITH (lists = 100) WHERE tier = 'premium';

Kita sekarang dapat melihat tingkat premium sekarang menggunakan indeks:

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)

Sementara pengguna tingkat gratis tidak memiliki manfaat:

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)

Hanya memiliki subset data yang diindeks, berarti indeks membutuhkan lebih sedikit ruang pada disk dan lebih cepat untuk dicari.

PostgreSQL mungkin gagal mengenali bahwa indeks aman digunakan jika formulir yang digunakan dalam WHERE klausa definisi indeks parsial tidak cocok dengan yang digunakan dalam kueri Anda. Dalam contoh himpunan data kami, kami hanya memiliki nilai 'free'yang tepat , 'test' dan 'premium' sebagai nilai yang berbeda dari kolom tingkat. Bahkan dengan kueri yang menggunakan tier LIKE 'premium' PostgreSQL tidak menggunakan indeks.

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)

Partisi

Salah satu cara untuk meningkatkan performa adalah dengan membagi himpunan data melalui beberapa partisi. Kita dapat membayangkan sistem ketika wajar untuk merujuk ke data hanya dari tahun ini atau mungkin dua tahun terakhir. Dalam sistem seperti itu, Anda dapat mempartisi data Anda dengan rentang tanggal dan kemudian memanfaatkan performa yang ditingkatkan ketika sistem hanya dapat membaca partisi yang relevan seperti yang didefinisikan oleh tahun yang dikueri.

Mari kita tentukan tabel yang dipartisi:

CREATE TABLE t_test_partitioned(vec vector(3), vec_date date default now()) partition by range (vec_date);

Kita dapat membuat partisi secara manual untuk setiap tahun atau menggunakan fungsi utilitas Citus (tersedia di 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
    );

Periksa partisi yang dibuat:

\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')

Untuk membuat partisi secara manual:

CREATE TABLE t_test_partitioned_p2019 PARTITION OF t_test_partitioned FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');

Kemudian pastikan kueri Anda benar-benar memfilter ke subset partisi yang tersedia. Misalnya dalam kueri di bawah ini kami memfilter ke dua partisi:

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

Anda dapat mengindeks tabel yang dipartisi.

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)

Kesimpulan

Selamat, Anda baru saja mempelajari tradeoff, batasan, dan praktik terbaik untuk mencapai performa terbaik dengan pgvector.