Delen via


Prestaties optimaliseren bij gebruik op pgvector Azure Database for PostgreSQL - Flexible Server

VAN TOEPASSING OP: Azure Database for PostgreSQL - Flexibele server

De pgvector extensie voegt een opensource vector gelijkenis zoeken toe aan Azure Database for PostgreSQL flexibele server.

In dit artikel worden de beperkingen en afwegingen van pgvector en beschreven hoe u partitionering, indexering en zoekinstellingen gebruikt om de prestaties te verbeteren.

Zie de basisbeginselen van pgvectorde extensie zelf voor meer informatie over de extensie zelf. U kunt ook verwijzen naar de officiële LEESMIJ van het project.

Prestaties

U moet altijd beginnen met het onderzoeken van het queryplan. Als uw query redelijk snel wordt beëindigd, voert u de opdracht uit EXPLAIN (ANALYZE,VERBOSE, BUFFERS).

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

Voor query's die te lang duren om het trefwoord uit te voeren, kunt u overwegen het ANALYZE trefwoord te verwijderen. Het resultaat bevat minder details, maar wordt direct verstrekt.

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

Sites van derden, zoals explain.depesz.com kunnen handig zijn bij het begrijpen van queryplannen. Enkele vragen die u moet beantwoorden, zijn:

Als uw vectoren worden genormaliseerd tot lengte 1, zoals OpenAI-insluitingen. Overweeg het gebruik van inner product (<#>) voor de beste prestaties.

Parallelle uitvoering

Zoek in de uitvoer van uw uitlegplan naar Workers Planned en Workers Launched (laatste alleen wanneer ANALYZE het trefwoord werd gebruikt). Met de max_parallel_workers_per_gather postgreSQL-parameter wordt gedefinieerd hoeveel achtergrondwerkers de database voor elk Gather knooppunt kan starten en Gather Merge knooppunten kunnen plannen. Als u deze waarde verhoogt, worden uw exacte zoekquery's mogelijk sneller uitgevoerd zonder dat u indexen hoeft te maken. Houd er echter rekening mee dat de database mogelijk niet besluit om het plan parallel uit te voeren, zelfs niet wanneer deze waarde hoog is.

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)

Indexeren

Zonder indexen aanwezig, voert de extensie een exacte zoekopdracht uit, wat een perfecte terugroep biedt ten koste van de prestaties.

Als u bij benadering dichtstbijzijnde burenzoekopdrachten wilt uitvoeren, kunt u indexen maken op uw gegevens, waardoor relevante overeenkomsten worden ingetrokken voor de uitvoeringsprestaties.

Laad indien mogelijk altijd uw gegevens voordat u deze indexeert. Het is beide sneller om de index op deze manier te maken en de resulterende indeling is beter.

Er zijn twee ondersteunde indextypen:

De IVFFlat index heeft snellere buildtijden en gebruikt minder geheugen dan HNSW, maar heeft lagere queryprestaties (wat betreft snelheid intrekken).

Limieten

  • Als u een kolom wilt indexeren, moet er dimensies zijn gedefinieerd. Het indexeren van een kolom die is gedefinieerd als col vector resultaten in de fout: ERROR: column does not have dimensions.
  • U kunt alleen een kolom met maximaal 2000 dimensies indexeren. Het indexeren van een kolom met meer dimensies resulteert in de fout: ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index waar INDEX_TYPE is ivfflat of hnsw.

Hoewel u vectoren met meer dan 2000 dimensies kunt opslaan, kunt u ze niet indexeren. U kunt dimensionaliteitsreductie gebruiken om binnen de limieten te passen. U kunt ook gebruikmaken van partitionering en/of sharding met Azure Cosmos DB for PostgreSQL om acceptabele prestaties te bereiken zonder indexering.

Omgekeerd bestand met platte compressie (IVVFlat)

Dit ivfflat is een index voor het zoeken naar dichtstbijzijnde buren (ANN). Deze methode maakt gebruik van een omgekeerde bestandsindex om de gegevensset te partitioneren in meerdere lijsten. De parameter test bepaalt hoeveel lijsten worden doorzocht, waardoor de nauwkeurigheid van de zoekresultaten kan worden verbeterd ten koste van tragere zoeksnelheid.

Als de parameter tests is ingesteld op het aantal lijsten in de index, worden alle lijsten doorzocht en wordt de zoekopdracht een exacte dichtstbijzijnde buurzoekopdracht. In dit geval gebruikt de planner de index niet, omdat het doorzoeken van alle lijsten gelijk is aan het uitvoeren van een brute-force zoekopdracht op de hele gegevensset.

De indexeringsmethode partitioneert de gegevensset in meerdere lijsten met behulp van het k-means-clustering-algoritme. Elke lijst bevat vectoren die zich het dichtst bij een bepaald clustercentrum bevinden. Tijdens een zoekopdracht wordt de queryvector vergeleken met de clustercentra om te bepalen welke lijsten waarschijnlijk de dichtstbijzijnde buren bevatten. Als de parameter voor tests is ingesteld op 1, wordt alleen de lijst die overeenkomt met het dichtstbijzijnde clustercentrum doorzocht.

Indexopties

Het selecteren van de juiste waarde voor het aantal tests dat moet worden uitgevoerd en de grootte van de lijsten kan van invloed zijn op de zoekprestaties. Goede plaatsen om te beginnen zijn:

  1. Gebruik lists gelijk aan rows / 1000 voor tabellen met maximaal 1 miljoen rijen en sqrt(rows) voor grotere gegevenssets.
  2. Begin probes met lists / 10 tabellen tot 1 miljoen rijen en sqrt(lists) voor grotere gegevenssets.

De hoeveelheid wordt gedefinieerd bij het maken van lists een index met de lists optie:

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

De tests kunnen worden ingesteld voor de hele verbinding of per transactie (met behulp van SET LOCAL binnen een transactieblok):

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

Voortgang van indexeren

Met PostgreSQL 12 en hoger kunt u de pg_stat_progress_create_index voortgang van de indexering controleren.

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

Fasen voor het bouwen van IVFFlat-indexen zijn:

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

Notitie

Voortgangspercentage (%) wordt alleen ingevuld tijdens loading tuples de fase.

Hiërarchische navigable Small Worlds (HNSW)

Dit hnsw is een index voor het zoeken naar dichtstbijzijnde buren (ANN) met behulp van het algoritme Hierarchical Navigable Small Worlds. Het werkt door een grafiek te maken rond willekeurig geselecteerde toegangspunten die de dichtstbijzijnde buren vinden, de grafiek wordt vervolgens uitgebreid met meerdere lagen, elke lagere laag met meer punten. Deze meerlaagse grafiek die wordt doorzocht, begint bovenaan en wordt beperkt totdat deze de laagste laag bereikt die de dichtstbijzijnde buren van de query bevat.

Het bouwen van deze index kost meer tijd en geheugen dan IVFFlat, maar het heeft een betere terugroepactie. Daarnaast is er geen trainingsstap zoals bij IVFFlat, zodat de index kan worden gemaakt in een lege tabel.

Indexopties

Wanneer u de index maakt, kunt u twee parameters afstemmen:

  1. m - maximum aantal verbindingen per laag (standaard ingesteld op 16)
  2. ef_construction - grootte van de dynamische kandidaatlijst die wordt gebruikt voor grafiekconstructie (standaard ingesteld op 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);

Tijdens query's kunt u de dynamische lijst met kandidaten voor zoeken opgeven (standaard ingesteld op 40).

De dynamische lijst met kandidaten voor zoeken kan worden ingesteld voor de hele verbinding of per transactie (met behulp van SET LOCAL een transactieblok):

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

Voortgang van indexeren

Met PostgreSQL 12 en hoger kunt u de pg_stat_progress_create_index voortgang van de indexering controleren.

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

Fasen voor het bouwen van HNSW-indexen zijn:

  1. initializing
  2. loading tuples

De functie voor indextoegang selecteren

Met vector het type kunt u drie typen zoekopdrachten uitvoeren op de opgeslagen vectoren. U moet de juiste toegangsfunctie voor uw index selecteren om ervoor te zorgen dat de database rekening houdt met uw index bij het uitvoeren van uw query's. De voorbeelden laten zien in ivfflat indextypen, maar hetzelfde kan worden gedaan voor hnsw indexen. De lists optie is alleen van toepassing op ivfflat indexen.

Cosinusafstand

Gebruik de vector_cosine_ops toegangsmethode voor cosinus-overeenkomsten zoeken.

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

Als u de bovenstaande index wilt gebruiken, moet de query een cosinus-gelijkeniszoekopdracht uitvoeren, die wordt uitgevoerd met de <=> 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)

L2 afstand

Gebruik voor L2-afstand (ook wel Euclidische afstand genoemd) de vector_l2_ops toegangsmethode.

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

Als u de bovenstaande index wilt gebruiken, moet de query een L2-zoekopdracht op afstand uitvoeren, die wordt uitgevoerd met de <-> 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)

Binnenste product

Gebruik de vector_ip_ops toegangsmethode voor de gelijkenis van binnenste producten.

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

Als u de bovenstaande index wilt gebruiken, moet de query een zoekopdracht uitvoeren naar overeenkomsten tussen interne producten, die wordt uitgevoerd met de <#> 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)

Gedeeltelijke indexen

In sommige scenario's is het handig om een index te hebben die slechts een gedeeltelijke set gegevens omvat. We kunnen bijvoorbeeld alleen een index maken voor onze premium-gebruikers:

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

We kunnen nu zien dat de Premium-laag nu gebruikmaakt van de 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)

Hoewel gebruikers van de gratis laag het voordeel missen:

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)

Als er slechts een subset van gegevens is geïndexeerd, betekent dit dat de index minder ruimte op de schijf nodig heeft en sneller kan worden doorzocht.

PostgreSQL kan mogelijk niet herkennen dat de index veilig kan worden gebruikt als het formulier dat wordt gebruikt in de WHERE component van de gedeeltelijke indexdefinitie niet overeenkomt met het formulier dat in uw query's wordt gebruikt. In onze voorbeeldgegevensset hebben we alleen de exacte waarden 'free''test' en 'premium' als de afzonderlijke waarden van de kolom Laag. Zelfs met een query die tier LIKE 'premium' PostgreSQL gebruikt, wordt de index niet gebruikt.

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)

Partitionering

Een manier om de prestaties te verbeteren, is door de gegevensset over meerdere partities te splitsen. We kunnen ons een systeem voorstellen als het natuurlijk is om te verwijzen naar gegevens uit het huidige jaar of misschien de afgelopen twee jaar. In een dergelijk systeem kunt u uw gegevens partitioneren op basis van een datumbereik en vervolgens gebruikmaken van verbeterde prestaties wanneer het systeem alleen de relevante partities kan lezen zoals gedefinieerd door het opgevraagde jaar.

Laten we een gepartitioneerde tabel definiëren:

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

We kunnen handmatig partities maken voor elk jaar of de functie Citus gebruiken (beschikbaar op 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
    );

Controleer de gemaakte partities:

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

Handmatig een partitie maken:

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

Controleer vervolgens of uw query's daadwerkelijk filteren op een subset van beschikbare partities. In de onderstaande query hebben we bijvoorbeeld gefilterd op twee partities:

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

U kunt een gepartitioneerde tabel indexeren.

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)

Volgende stappen

Gefeliciteerd, u hebt zojuist de compromissen, beperkingen en best practices geleerd om de beste prestaties met pgvectorte bereiken.