PostgreSQL için Azure Cosmos DB'de pgvector kullanırken performansı iyileştirme
ŞUNLAR IÇIN GEÇERLIDIR: PostgreSQL için Azure Cosmos DB (PostgreSQL'e citus veritabanı uzantısıyla desteklenir)
Uzantı PostgreSQL'e pgvector
açık kaynak vektör benzerlik araması ekler.
Bu makalede, sınırlamaları ve dezavantajları pgvector
incelenip performansı geliştirmek için bölümleme, dizin oluşturma ve arama ayarlarının nasıl kullanılacağı gösterilmektedir.
Uzantının kendisi hakkında daha fazla bilgi için temel bilgilerine pgvector
bakın. Projenin resmi BENİOKU bölümüne de başvurmak isteyebilirsiniz.
Performans
Her zaman sorgu planını araştırarak başlamalısınız. Sorgunuz makul bir şekilde hızlı sonlandırılırsa komutunu çalıştırın EXPLAIN (ANALYZE,VERBOSE, BUFFERS)
.
EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
Yürütülmesi çok uzun süren sorgular için anahtar sözcüğünü ANALYZE
bırakmalısınız. Sonuç daha az ayrıntı içerir ancak anında sağlanır.
EXPLAIN (VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
explain.depesz.com gibi üçüncü taraf siteler sorgu planlarını anlamanıza yardımcı olabilir. Yanıtlamaya çalışmanız gereken bazı sorular şunlardır:
- Sorgu paralelleştirildi mi?
- Dizin kullanıldı mı?
- WHERE yan tümcesinde kısmi dizin tanımında olduğu gibi aynı koşulu kullandım mı?
- Bölümleme kullanırsam gerekli olmayan bölümler ayıklama yapıldı mı?
Vektörleriniz OpenAI eklemeleri gibi 1. uzunlukta normalleştirilmişse. En iyi performans için iç ürünü (<#>
) kullanmayı düşünmelisiniz.
Paralel yürütme
Açıklama planınızın çıktısında ve Workers Launched
(yalnızca ANALYZE
anahtar sözcük kullanıldığında ikinci) öğesini arayınWorkers Planned
. max_parallel_workers_per_gather
PostgreSQL parametresi, veritabanının her Gather
ve Gather Merge
plan düğümü için kaç arka plan çalışanı başlatabileceğini tanımlar. Bu değeri artırmak dizin oluşturmak zorunda kalmadan tam arama sorgularınızı hızlandırabilir. Ancak, bu değer yüksek olduğunda bile veritabanının planı paralel olarak çalıştırmaya karar vermeyebileceğini unutmayın.
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)
Dizinleme
Dizinler mevcut olmadığında, uzantı tam bir arama gerçekleştirir ve bu da performans pahasına mükemmel bir geri çağırma sağlar.
Yaklaşık en yakın komşu araması gerçekleştirmek için verilerinizde dizinler oluşturabilirsiniz ve bu da yürütme performansının geri çağrılmasıyla sonuçlanabilir.
Mümkün olduğunda, verilerinizi dizine eklemeden önce her zaman yükleyin. Hem dizini bu şekilde oluşturmak daha hızlıdır hem de sonuçta elde edilen düzen daha uygun olur.
Desteklenen iki dizin türü vardır:
Dizin IVFFlat
daha hızlı derleme sürelerine sahiptir ve değerinden HNSW
daha az bellek kullanır, ancak daha düşük sorgu performansına sahiptir (hızlı geri çağırma dengeleri açısından).
Sınırlar
- Bir sütunun dizinini oluşturmak için tanımlanmış boyutları olması gerekir. Olarak tanımlanan
col vector
bir sütunu dizine ekleme girişimi şu hatayla sonuçlanır:ERROR: column does not have dimensions
. - Yalnızca en fazla 2000 boyutu olan bir sütunun dizinini oluşturabilirsiniz. Daha fazla boyuta sahip bir sütunu dizine ekleme girişimi şu hatayla sonuçlanır:
ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index
buradaINDEX_TYPE
veyahnsw
olurivfflat
.
2000'den fazla boyuta sahip vektörleri depolayabilirsiniz ancak bunları dizine ekleyemezsiniz. Boyutsallığı azaltmayı sınırlara sığacak şekilde kullanabilirsiniz. Alternatif olarak dizin oluşturma olmadan kabul edilebilir performans elde etmek için PostgreSQL için Azure Cosmos DB ile bölümleme ve/veya parçalama işlemlerine de güvenebilirsiniz.
Düz Sıkıştırmalı Ters Dosya (IVVFlat)
, ivfflat
yaklaşık en yakın komşu (ANN) araması için bir dizindir. Bu yöntem, veri kümesini birden çok liste halinde bölümlendirmek için ters dosya dizini kullanır. Yoklamalar parametresi, kaç liste aranabileceğini denetler ve bu da arama sonuçlarının doğruluğunu daha düşük arama hızı karşılığında iyileştirebilir.
Yoklamalar parametresi dizindeki liste sayısına ayarlanırsa, tüm listeler aranırsa ve arama tam olarak en yakın komşu aramasına dönüşür. Bu durumda, tüm listelerde arama yapmak veri kümesinin tamamında deneme yanılma araması yapmaya eşdeğer olduğundan planlayıcı dizini kullanmıyordur.
Dizin oluşturma yöntemi, k ortalamaları kümeleme algoritmasını kullanarak veri kümesini birden çok liste halinde bölümler. Her liste, belirli bir küme merkezine en yakın vektörleri içerir. Arama sırasında, en yakın komşuları içerme olasılığının en yüksek olduğu listeleri belirlemek için sorgu vektöru küme merkezleriyle karşılaştırılır. Yoklamalar parametresi 1 olarak ayarlanırsa, yalnızca en yakın küme merkezine karşılık gelen liste aranırdı.
Dizin seçenekleri
Gerçekleştirilecek yoklama sayısı için doğru değerin seçilmesi ve listelerin boyutları arama performansını etkileyebilir. Başlamak için iyi yerler şunlardır:
- 1 milyon satıra
rows / 1000
kadar olan tablolar vesqrt(rows)
daha büyük veri kümeleri için eşittir seçeneğini kullanınlists
. lists / 10
Başlangıçprobes
olarak 1 milyon satıra kadar olan tablolar vesqrt(lists)
daha büyük veri kümeleri için.
lists
Miktarı, dizin oluşturma işleminde şu lists
seçenekle tanımlanır:
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 5000);
Yoklamalar tüm bağlantı için veya işlem başına ayarlanabilir (işlem bloğu içinde kullanılarak 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
Dizin oluşturma ilerleme durumu
PostgreSQL 12 ve daha yeni sürümlerle dizin oluşturma işleminin ilerleme durumunu denetlemek için kullanabilirsiniz 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 dizinleri oluşturma aşamaları şunlardır:
initializing
performing k-means
assigning tuples
loading tuples
Not
İlerleme yüzdesi (%
) yalnızca aşama sırasında loading tuples
doldurulur.
Hiyerarşik Gezinilebilir Küçük Dünyalar (HNSW)
hnsw
Hiyerarşik Gezinilebilir Küçük Dünyalar algoritmasını kullanarak yaklaşık en yakın komşu (ANN) araması için bir dizindir. Rastgele seçilen giriş noktalarının çevresinde bir graf oluşturarak en yakın komşularını bularak çalışır, graf daha sonra her alt katman daha fazla nokta içeren birden çok katmanla genişletilir. Arama yapıldığında bu çok katmanlı grafik üstten başlar ve sorgunun en yakın komşularını içeren en düşük katmana ulaşana kadar daraltılır.
Bu dizinin oluşturulması IVFFlat'tan daha fazla zaman ve bellek gerektirir, ancak daha iyi bir hız-geri çağırma dezavantajı vardır. Ayrıca IVFFlat gibi bir eğitim adımı olmadığından dizin boş bir tabloda oluşturulabilir.
Dizin seçenekleri
Dizini oluştururken iki parametre ayarlayabilirsiniz:
m
- katman başına en fazla bağlantı sayısı (varsayılan olarak 16'dır)ef_construction
- Grafik oluşturma için kullanılan dinamik aday listesinin boyutu (varsayılan olarak 64'tür)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
Sorgular sırasında, arama için dinamik aday listesini belirtebilirsiniz (varsayılan olarak 40'tır).
Arama için dinamik aday listesi, bağlantının tamamı veya işlem başına ayarlanabilir (işlem bloğu içinde kullanılarak 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
Dizin oluşturma ilerleme durumu
PostgreSQL 12 ve daha yeni sürümlerle dizin oluşturma işleminin ilerleme durumunu denetlemek için kullanabilirsiniz 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 dizinleri oluşturma aşamaları şunlardır:
initializing
loading tuples
Dizin erişim işlevini seçme
türü, vector
depolanan vektörlerde üç tür arama gerçekleştirmenizi sağlar. Sorgularınızı yürütürken veritabanının dizininizi dikkate alabilmesi için dizininiz için doğru erişim işlevini seçmeniz gerekir. Örneklerde dizin türleri gösterilmektedir ivfflat
, ancak dizinler için hnsw
de aynı işlem yapılabilir. seçeneği lists
yalnızca dizinler için ivfflat
geçerlidir.
Kosinüs uzaklığı
Kosinüs benzerlik araması için erişim yöntemini kullanın vector_cosine_ops
.
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
Yukarıdaki dizini kullanmak için sorgunun işleçle yapılan bir kosinüs benzerliği araması gerçekleştirmesi <=>
gerekir.
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 uzaklığı
L2 uzaklığı (Öklid uzaklığı olarak da bilinir) için erişim yöntemini kullanın vector_l2_ops
.
CREATE INDEX t_test_embedding_l2_idx ON t_test USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
Yukarıdaki dizini kullanmak için sorgunun işleciyle yapılan bir L2 uzaklık araması gerçekleştirmesi <->
gerekir.
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)
İç ürün
İç ürün benzerliği için erişim yöntemini kullanın vector_ip_ops
.
CREATE INDEX t_test_embedding_ip_idx ON t_test USING ivfflat (embedding vector_ip_ops) WITH (lists = 100);
Yukarıdaki dizini kullanmak için sorgunun işleciyle yapılan bir iç ürün benzerliği araması gerçekleştirmesi <#>
gerekir.
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)
Kısmi dizinler
Bazı senaryolarda, verilerin yalnızca kısmi bir kümesini kapsayan bir dizine sahip olmak yararlıdır. Örneğin, yalnızca premium kullanıcılarımız için bir dizin oluşturabiliriz:
CREATE INDEX t_premium ON t_test USING ivfflat (vec vector_ip_ops) WITH (lists = 100) WHERE tier = 'premium';
Artık premium katmanın dizini kullandığını görebiliriz:
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)
Ücretsiz katman kullanıcıları avantajdan yoksun olsa da:
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)
Verilerin yalnızca bir alt kümesinin dizine sahip olması, dizinin diskte daha az yer kaplayıp aramanın daha hızlı olduğu anlamına gelir.
Kısmi dizin tanımının yan tümcesinde WHERE
kullanılan form sorgularınızda kullanılan formla eşleşmiyorsa PostgreSQL dizinin güvenli olduğunu algılayamaz.
Örnek veri kümemizde yalnızca tam değerlerine 'free'
'test'
ve 'premium'
katman sütununun benzersiz değerlerine sahibiz. PostgreSQL kullanan tier LIKE 'premium'
bir sorguda bile dizin kullanılmaz.
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)
Bölümleme
Performansı artırmanın bir yolu, veri kümesini birden çok bölüme bölmektir. Yalnızca geçerli yıla veya belki de son iki yıla ait verilere başvurmanın doğal olduğu bir sistem hayal edebiliriz. Böyle bir sistemde, verilerinizi bir tarih aralığına göre bölümleyebilir ve ardından sistem sorgulanan yıl tarafından tanımlandığı şekilde yalnızca ilgili bölümleri okuyabildiğinde geliştirilmiş performansa göre büyük harf kullanabilirsiniz.
Bölümlenmiş bir tablo tanımlayalım:
CREATE TABLE t_test_partitioned(vec vector(3), vec_date date default now()) partition by range (vec_date);
Her yıl için el ile bölümler oluşturabilir veya Citus yardımcı programı işlevini kullanabiliriz (PostgreSQL için Cosmos DB'de kullanılabilir).
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
);
Oluşturulan bölümleri denetleyin:
\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')
El ile bölüm oluşturmak için:
CREATE TABLE t_test_partitioned_p2019 PARTITION OF t_test_partitioned FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
Ardından sorgularınızın aslında kullanılabilir bölümlerin bir alt kümesine göre filtrelediğinden emin olun. Örneğin aşağıdaki sorguda iki bölüme filtreledik:
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
Bölümlenmiş bir tablonun dizinini oluşturabilirsiniz.
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)
Sonuç
Tebrikler, ile pgvector
en iyi performansı elde etmek için dezavantajları, sınırlamaları ve en iyi yöntemleri öğrendin.