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:
- Apakah kueri diparalelkan?
- Apakah indeks digunakan?
- Apakah saya menggunakan kondisi yang sama dalam klausa WHERE seperti dalam definisi indeks parsial?
- Jika saya menggunakan partisi, partisi yang tidak diperlukan dipracu?
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 manaINDEX_TYPE
adalahivfflat
atauhnsw
.
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:
- Gunakan
lists
samarows / 1000
dengan untuk tabel dengan hingga 1 juta baris dansqrt(rows)
untuk himpunan data yang lebih besar. - Untuk
probes
memulai denganlists / 10
untuk tabel hingga 1 juta baris dansqrt(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:
initializing
performing k-means
assigning tuples
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:
m
- jumlah maksimum koneksi per lapisan (default ke 16)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:
initializing
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
.