Optimalizace výkonu při použití nástroje pgvector na flexibilním serveru Azure Database for PostgreSQL
PLATÍ PRO: Flexibilní server Azure Database for PostgreSQL
Rozšíření pgvector
přidá do flexibilního serveru Azure Database for PostgreSQL hledání open source vektorové podobnosti.
Tento článek popisuje omezení a kompromisy pgvector
a ukazuje, jak používat dělení, indexování a nastavení vyhledávání ke zlepšení výkonu.
Další informace o samotném rozšíření naleznete v základech pgvector
. Můžete také chtít odkazovat na oficiální soubor README projektu.
Výkon
Vždy byste měli začít zkoumáním plánu dotazu. Pokud se dotaz přiměřeně rychle ukončí, spusťte EXPLAIN (ANALYZE,VERBOSE, BUFFERS)
příkaz .
EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
U dotazů, které trvá příliš dlouho, zvažte vyřazení klíčového ANALYZE
slova. Výsledek obsahuje méně podrobností, ale poskytuje se okamžitě.
EXPLAIN (VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
Weby třetích stran, jako je explain.depesz.com , můžou být užitečné při pochopení plánů dotazů. Mezi otázky, na které byste se měli pokusit odpovědět, patří:
- Byl dotaz paralelizovaný?
- Použil se index?
- Použil(a) jsem v klauzuli WHERE stejnou podmínku jako v částečné definici indexu?
- Pokud používám dělení, nebyly vyřazené oddíly potřeba?
Pokud jsou vektory normalizovány na délku 1, například vkládání OpenAI. Pro zajištění nejlepšího výkonu byste měli zvážit použití vnitřního produktu (<#>
).
Paralelní spouštění
Ve výstupu plánu vysvětlení vyhledejte Workers Planned
a Workers Launched
(druhý pouze v případě ANALYZE
použití klíčového slova). Parametr max_parallel_workers_per_gather
PostgreSQL definuje, kolik pracovních procesů na pozadí může databáze spustit pro každý Gather
uzel plánu.Gather Merge
Zvýšení této hodnoty může urychlit přesné vyhledávací dotazy, aniž byste museli vytvářet indexy. Upozorňujeme však, že se databáze nemusí rozhodnout spustit plán paralelně, i když je tato hodnota vysoká.
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)
Indexování
Bez přítomnosti indexů rozšíření provádí přesné vyhledávání, které poskytuje perfektní úplnost na úkor výkonu.
Aby bylo možné provést přibližné hledání nejbližšího souseda, můžete vytvořit indexy na vašich datech, které se obchodují s výkonem provádění.
Pokud je to možné, před indexováním je vždy načtěte. Vytvoření indexu je rychlejší a výsledné rozložení je optimální.
Existují tři podporované typy indexů:
- Invertovaný soubor s plochou kompresí (IVVFlat)
- Hierarchické navigace v malých světech (HNSW)
- Přibližný nejbližší soused disku (DiskANN)
Index IVFFlat
má rychlejší časy sestavení a využívá méně paměti než HNSW
, ale má nižší výkon dotazů (z hlediska kompromisu při rychlém odvolání). DiskANN
nabízí skvělou rovnováhu mezi nabídkami vysoce přesného výkonu dotazů a rychlých časů sestavení.
Omezení
- Aby bylo možné indexovat sloupec, musí mít definované dimenze. Pokus o indexování sloupce definovaného jako
col vector
výsledek chyby:ERROR: column does not have dimensions
. - Indexovat můžete pouze sloupec, který má až 2 000 dimenzí. Při pokusu o indexování sloupce s více dimenzemi dojde k chybě:
ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index
kdeINDEX_TYPE
je neboivfflat
hnsw
.
I když můžete ukládat vektory s více než 2000 dimenzemi, nemůžete je indexovat. K přizpůsobení limitů můžete použít redukci rozměrnosti. Pokud chcete dosáhnout přijatelného výkonu bez indexování, můžete také spoléhat na dělení nebo horizontální dělení pomocí služby Azure Cosmos DB for PostgreSQL.
Invertovaný soubor s plochou kompresí (IVVFlat)
Jedná se ivfflat
o index pro přibližné hledání nejbližšího souseda (ANN). Tato metoda používá invertovaný index souborů k rozdělení datové sady do více seznamů. Parametr sond určuje, kolik seznamů se prohledá, což může zlepšit přesnost výsledků hledání za cenu pomalejší rychlosti hledání.
Pokud je parametr sond nastaven na počet seznamů v indexu, budou prohledány všechny seznamy a hledání se stane přesným hledáním nejbližšího souseda. V tomto případě plánovač nepoužívá index, protože vyhledávání ve všech seznamech odpovídá provedení hledání hrubou silou pro celou datovou sadu.
Metoda indexování rozdělí datovou sadu do více seznamů pomocí algoritmu clusteringu k-means. Každý seznam obsahuje vektory, které jsou nejblíže konkrétnímu centru clusteru. Během hledání se vektor dotazu porovnává s středy clusteru a zjišťuje, které seznamy budou pravděpodobně obsahovat nejbližší sousedy. Pokud je parametr sond nastaven na hodnotu 1, prohledá se bude prohledávat pouze seznam odpovídající nejbližšímu centru clusteru.
Možnosti indexu
Výběr správné hodnoty pro počet sond, které se mají provést, a velikosti seznamů můžou mít vliv na výkon hledání. Dobrá místa, kde začít, jsou:
- Pro tabulky s až 1 miliony řádků a
sqrt(rows)
větších datových sad použijtelists
rovná serows / 1000
tabulkám. lists / 10
Začněteprobes
u tabulek o 1 milionech řádků asqrt(lists)
u větších datových sad.
lists
Velikost je definována při vytvoření indexu lists
s možností:
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 5000);
Sondy lze nastavit pro celé připojení nebo pro každou transakci (pomocí SET LOCAL
v rámci bloku transakce):
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
Průběh indexování
S PostgreSQL 12 a novějším můžete zkontrolovat pg_stat_progress_create_index
průběh indexování.
SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
Fáze vytváření indexů IVFFlat jsou:
initializing
performing k-means
assigning tuples
loading tuples
Poznámka:
Procento průběhu (%
) je vyplněno pouze během loading tuples
fáze.
Hierarchické navigace v malých světech (HNSW)
Je hnsw
index pro přibližné hledání nejbližšího souseda (ANN) pomocí algoritmu Hierarchical Navigable Small Worlds. Funguje to tak, že vytvoří graf kolem náhodně vybraných vstupních bodů, které najdou nejbližší sousedy, graf se pak rozšíří o více vrstev, přičemž každá nižší vrstva obsahuje více bodů. Tento vícevrstvý graf při hledání začíná nahoře a zúží se, dokud se nedotkne nejnižší vrstvy, která obsahuje nejbližší sousedy dotazu.
Sestavení tohoto indexu trvá více času a paměti než IVFFlat, ale má lepší kompromis pro rychlé odvolání. Kromě toho neexistuje žádný krok trénování jako u IVFFlat, takže index lze vytvořit v prázdné tabulce.
Možnosti indexu
Při vytváření indexu můžete ladit dva parametry:
m
– maximální počet připojení na vrstvu (výchozí hodnota je 16)ef_construction
- velikost dynamického kandidátního seznamu použitého pro vytváření grafů (výchozí hodnota je 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
Během dotazů můžete zadat seznam dynamických kandidátů pro vyhledávání (výchozí hodnota je 40).
Seznam dynamických kandidátů pro vyhledávání lze nastavit pro celé připojení nebo pro každou transakci (pomocí SET LOCAL
v rámci bloku transakce):
SET hnsw.ef_search = 100;
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
Průběh indexování
S PostgreSQL 12 a novějším můžete zkontrolovat pg_stat_progress_create_index
průběh indexování.
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
Fáze vytváření indexů HNSW jsou:
initializing
loading tuples
Přibližný nejbližší soused disku (DiskANN)
DiskANN
je škálovatelný přibližný vyhledávací algoritmus nejbližšího souseda pro efektivní vektorové vyhledávání v libovolném měřítku. Nabízí vysokou úplnost, vysoké dotazy za sekundu (QPS) a nízkou latenci dotazů, a to i pro datové sady s miliardami bodů. Díky tomu je výkonný nástroj pro zpracování velkých objemů dat. Přečtěte si další informace o DiskANN od Microsoftu.
Sestavení tohoto indexu trvá více času a paměti, než IVFFlat
má však lepší kompromis pro rychlé odvolání. Kromě toho neexistuje žádný krok trénování jako u IVFFlat
, takže index lze vytvořit v prázdné tabulce.
Možnosti indexu
Při vytváření indexu pomocí diskann
můžete zadat různé parametry pro řízení jeho chování. Tady jsou možnosti, které aktuálně máme:
max_neighbors
: Maximální počet hran na uzel v grafu. (Výchozí hodnota je 32)l_value_ib
: Velikost vyhledávacího seznamu během sestavení indexu (výchozí hodnota je 50)
CREATE INDEX my_table_embedding_diskann_custom_idx ON my_table USING diskann (embedding vector_cosine_ops)
WITH (
max_neighbors = 48,
l_value_ib = 100
);
Hodnotu L pro prohledávání indexu (l_value_is
) lze nastavit pro celé připojení nebo pro každou transakci (pomocí SET LOCAL
v rámci bloku transakce):
SET diskann.l_value_is = 100;
SELECT * FROM my_table ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- uses 100 candidates
Postgres se automaticky rozhodne, kdy se má použít index DiskANN. Pokud existují scénáře, které chcete vždy použít index, použijte následující příkaz:
SET LOCAL enable_seqscan TO OFF;
SELECT * FROM my_table ORDER BY embedding <=> '[1,2,3]' LIMIT 5; -- forces the use of index
Průběh indexování
S PostgreSQL 12 a novějším můžete zkontrolovat pg_stat_progress_create_index
průběh indexování.
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
Fáze vytváření indexů DiskANN jsou:
initializing
loading tuples
Výběr funkce pro přístup k indexu
Tento vector
typ umožňuje provádět tři typy hledání u uložených vektorů. Abyste mohli databázi při provádění dotazů vzít v úvahu, musíte pro index vybrat správnou přístupnou funkci. Příklady demonstrují typy ivfflat
indexů, ale totéž lze provést pro hnsw
a diskann
indexy. Tato lists
možnost se vztahuje pouze na ivfflat
indexy.
Kosinus vzdálenost
Pro vyhledávání kosinus podobnosti použijte metodu vector_cosine_ops
přístupu.
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
Pokud chcete použít výše uvedený index, musí dotaz provést kosinusové vyhledávání podobnosti, které se provádí s operátorem <=>
.
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)
Vzdálenost L2
Pro vzdálenost L2 (označovanou také jako Euclidean vzdálenost) použijte metodu vector_l2_ops
přístupu.
CREATE INDEX t_test_embedding_l2_idx ON t_test USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
Pokud chcete použít výše uvedený index, musí dotaz provést hledání vzdálenosti L2, které se provádí s operátorem <->
.
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)
Vnitřní produkt
Pro vnitřní podobnost produktu použijte metodu vector_ip_ops
přístupu.
CREATE INDEX t_test_embedding_ip_idx ON t_test USING ivfflat (embedding vector_ip_ops) WITH (lists = 100);
Pokud chcete použít výše uvedený index, musí dotaz provést vnitřní vyhledávání podobnosti produktu, které se provádí s operátorem <#>
.
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)
Částečné indexy
V některých scénářích je výhodné mít index, který pokrývá pouze částečnou sadu dat. Můžeme například vytvořit index pouze pro naše prémiové uživatele:
CREATE INDEX t_premium ON t_test USING ivfflat (vec vector_ip_ops) WITH (lists = 100) WHERE tier = 'premium';
Teď vidíme, že úroveň Premium teď používá index:
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)
I když uživatelé úrovně Free nemají výhodu:
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)
Když máte indexovanou jenom podmnožinu dat, znamená to, že index na disku zabírá méně místa a je rychlejší prohledávat.
PostgreSQL nemusí rozpoznat, že index je bezpečný, pokud se formulář použitý v WHERE
klauzuli částečné definice indexu neshoduje s formulářem použitým v dotazech.
V naší ukázkové datové sadě máme pouze přesné hodnoty 'free'
'test'
a 'premium'
jako jedinečné hodnoty sloupce vrstvy. I s dotazem, který používá tier LIKE 'premium'
PostgreSQL, index nepoužívá.
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)
dělení na části
Jedním ze způsobů, jak zlepšit výkon, je rozdělit datovou sadu na více oddílů. Systém si můžeme představit, když je přirozený odkazovat na data jen z aktuálního roku nebo možná za dva roky. V takovém systému můžete data rozdělit podle rozsahu kalendářních dat a pak využít vyššího výkonu, když systém dokáže číst jenom relevantní oddíly definované dotazovaným rokem.
Pojďme definovat dělenou tabulku:
CREATE TABLE t_test_partitioned(vec vector(3), vec_date date default now()) partition by range (vec_date);
Oddíly můžeme ručně vytvořit pro každý rok nebo použít funkci nástroje Citus (k dispozici ve službě 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
);
Zkontrolujte vytvořené oddíly:
\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')
Ruční vytvoření oddílu:
CREATE TABLE t_test_partitioned_p2019 PARTITION OF t_test_partitioned FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
Pak se ujistěte, že se vaše dotazy skutečně filtrují na podmnožinu dostupných oddílů. Například v následujícím dotazu jsme vyfiltrovali až dva oddíly:
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
Dělenou tabulku můžete indexovat.
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)
Související obsah
- Generování vektorových vkládání pomocí Azure OpenAI na flexibilním serveru Azure Database for PostgreSQL