Sdílet prostřednictvím


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ří:

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ů:

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 kde INDEX_TYPE je nebo ivfflat 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:

  1. Pro tabulky s až 1 miliony řádků a sqrt(rows) větších datových sad použijte lists rovná se rows / 1000 tabulkám.
  2. lists / 10 Začněte probes u tabulek o 1 milionech řádků a sqrt(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:

  1. initializing
  2. performing k-means
  3. assigning tuples
  4. 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:

  1. m – maximální počet připojení na vrstvu (výchozí hodnota je 16)
  2. 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:

  1. initializing
  2. 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ž IVFFlatmá 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í diskannmůžete zadat různé parametry pro řízení jeho chování. Tady jsou možnosti, které aktuálně máme:

  1. max_neighbors: Maximální počet hran na uzel v grafu. (Výchozí hodnota je 32)
  2. 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:

  1. initializing
  2. 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)