Dela via


Så här optimerar du prestanda när du använder pgvector i Azure Database for PostgreSQL – flexibel server

GÄLLER FÖR: Azure Database for PostgreSQL – flexibel server

Tillägget pgvector lägger till en vektorlikhetssökning med öppen källkod till en flexibel Azure Database for PostgreSQL-server.

Den här artikeln utforskar begränsningar och kompromisser pgvector för och visar hur du använder partitionerings-, indexerings- och sökinställningar för att förbättra prestandan.

Mer information om själva tillägget finns i grunderna pgvectorför . Du kanske också vill referera till projektets officiella README .

Prestanda

Du bör alltid börja med att undersöka frågeplanen. Om frågan avslutas ganska snabbt kör du EXPLAIN (ANALYZE,VERBOSE, BUFFERS).

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

För frågor som tar för lång tid att köra kan du överväga att ta bort nyckelordet ANALYZE . Resultatet innehåller färre detaljer men tillhandahålls omedelbart.

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

Webbplatser från tredje part, som explain.depesz.com kan vara till hjälp för att förstå frågeplaner. Några frågor som du bör försöka besvara är:

Om dina vektorer normaliseras till längd 1, till exempel OpenAI-inbäddningar. Du bör överväga att använda inre produkter (<#>) för bästa prestanda.

Parallell körning

I utdata från förklaringsplanen letar Workers Planned du efter och Workers Launched (senare endast när ANALYZE nyckelordet användes). Parametern max_parallel_workers_per_gather PostgreSQL definierar hur många bakgrundsarbetare databasen kan starta för varje Gather nod och Gather Merge plannod. Om du ökar det här värdet kan du påskynda dina exakta sökfrågor utan att behöva skapa index. Observera dock att databasen kanske inte bestämmer sig för att köra planen parallellt även när det här värdet är högt.

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)

Indexering

Utan index som finns utför tillägget en exakt sökning, vilket ger perfekt träffsäkerhet på bekostnad av prestanda.

För att kunna utföra ungefärliga närmsta grannsökningar kan du skapa index på dina data, vilket byter träffsäkerhet mot körningsprestanda.

Läs alltid in dina data innan du indexerar dem när det är möjligt. Det går både snabbare att skapa indexet på det här sättet och den resulterande layouten är mer optimal.

Det finns två indextyper som stöds:

Indexet IVFFlat har snabbare byggtider och använder mindre minne än HNSW, men har lägre frågeprestanda (när det gäller hastighetsåterkallningsavvägning).

Gränser

  • För att indexera en kolumn måste den ha definierade dimensioner. Försök att indexera en kolumn som definierats som col vector resulterar i felet: ERROR: column does not have dimensions.
  • Du kan bara indexera en kolumn som har upp till 2 000 dimensioner. Försök att indexera en kolumn med fler dimensioner resulterar i felet: ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index var INDEX_TYPE är antingen ivfflat eller hnsw.

Du kan lagra vektorer med fler än 2 000 dimensioner, men du kan inte indexeras. Du kan använda dimensionsminskning för att passa inom gränserna. Du kan också förlita dig på partitionering och/eller horisontell partitionering med Azure Cosmos DB for PostgreSQL för att uppnå godtagbara prestanda utan indexering.

Inverterad fil med platt komprimering (IVVFlat)

ivfflat är ett index för ungefärlig sökning efter närmaste granne (ANN). Den här metoden använder ett inverterat filindex för att partitionera datamängden i flera listor. Parametern probes styr hur många listor som genomsöks, vilket kan förbättra sökresultatens noggrannhet på bekostnad av långsammare sökhastighet.

Om parametern probes är inställd på antalet listor i indexet genomsöks alla listor och sökningen blir en exakt närmaste grannsökning. I det här fallet använder inte planeraren indexet eftersom sökning i alla listor motsvarar att utföra en brute-force-sökning på hela datamängden.

Indexeringsmetoden partitionerar datamängden i flera listor med hjälp av k-means-klustringsalgoritmen. Varje lista innehåller vektorer som är närmast ett visst klustercenter. Under en sökning jämförs frågevektorn med klustercenter för att avgöra vilka listor som mest sannolikt innehåller närmaste grannar. Om parametern avsökningar är inställd på 1 söks endast listan som motsvarar närmaste klustercenter genomsöks.

Indexalternativ

Om du väljer rätt värde för antalet avsökningar som ska utföras och listornas storlekar kan det påverka sökprestandan. Bra ställen att börja på är:

  1. Använd lists lika rows / 1000 med för tabeller med upp till 1 miljon rader och sqrt(rows) för större datauppsättningar.
  2. Till probes att börja med lists / 10 för tabeller upp till 1 miljon rader och sqrt(lists) för större datauppsättningar.

Mängden lists definieras när index skapas med alternativet lists :

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

Avsökningarna kan anges för hela anslutningen eller per transaktion (med hjälp av SET LOCAL ett transaktionsblock):

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

Indexeringsstatus

Med PostgreSQL 12 och senare kan du använda pg_stat_progress_create_index för att kontrollera indexeringsförloppet.

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

Faser för att skapa IVFFlat-index är:

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

Kommentar

Förloppsprocent (%) fylls endast i under loading tuples fasen.

Hierarkiska navigeringsbara små världar (HNSW)

hnsw är ett index för ungefärlig sökning efter närmaste granne (ANN) med hjälp av algoritmen Hierarkisk navigeringsbar liten värld. Det fungerar genom att skapa ett diagram runt slumpmässigt valda startpunkter som hittar sina närmaste grannar, grafen utökas sedan med flera lager, varje lägre lager som innehåller fler punkter. Det här flerlayererade diagrammet när sökningen startas överst och begränsas tills det når det lägsta lagret som innehåller de närmaste grannarna i frågan.

Att bygga detta index tar mer tid och minne än IVFFlat, men det har bättre hastighetsåterkallelseavvägning. Dessutom finns det inget träningssteg som med IVFFlat, så indexet kan skapas på en tom tabell.

Indexalternativ

När du skapar indexet kan du justera två parametrar:

  1. m – maximalt antal anslutningar per lager (standardvärdet är 16)
  2. ef_construction - storleken på den dynamiska kandidatlista som används för grafkonstruktion (standardvärdet är 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);

Under frågor kan du ange den dynamiska kandidatlistan för sökning (standardvärdet är 40).

Den dynamiska kandidatlistan för sökning kan anges för hela anslutningen eller per transaktion (med hjälp av SET LOCAL ett transaktionsblock):

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

Indexeringsstatus

Med PostgreSQL 12 och senare kan du använda pg_stat_progress_create_index för att kontrollera indexeringsförloppet.

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

Faser för att skapa HNSW-index är:

  1. initializing
  2. loading tuples

Välja funktionen för indexåtkomst

Med vector typen kan du utföra tre typer av sökningar på de lagrade vektorerna. Du måste välja rätt åtkomstfunktion för ditt index för att databasen ska kunna ta hänsyn till ditt index när du kör dina frågor. Exemplen visar på ivfflat indextyper, men samma sak kan göras för hnsw index. Alternativet lists gäller endast för ivfflat index.

Cosinnavstånd

Använd åtkomstmetoden för samexisteringssökning vector_cosine_ops .

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

Om du vill använda indexet ovan måste frågan utföra en cosinélikhetssökning, vilket görs med operatorn <=> .

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-avstånd

För L2-avstånd (även kallat euklidiska avstånd) använder du vector_l2_ops åtkomstmetoden.

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

Om du vill använda indexet ovan måste frågan utföra en L2-avståndssökning, vilket görs med operatorn <-> .

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)

Inre produkt

Använd åtkomstmetoden för vector_ip_ops inre produktlikhet.

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

Om du vill använda indexet ovan måste frågan utföra en inre produktlikhetssökning, vilket görs med operatorn <#> .

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)

Partiella index

I vissa scenarier är det fördelaktigt att ha ett index som endast omfattar en partiell uppsättning data. Vi kan till exempel skapa ett index bara för våra premiumanvändare:

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

Nu kan vi se att premiumnivån använder indexet:

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)

Användarna på den kostnadsfria nivån saknar förmånen:

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)

Att bara ha en delmängd data indexerade innebär att indexet tar mindre utrymme på disken och är snabbare att söka igenom.

PostgreSQL kan inte identifiera att indexet är säkert att använda om formuläret som används i satsen för WHERE den partiella indexdefinitionen inte matchar det som används i dina frågor. I vår exempeldatauppsättning har vi bara de exakta värdena 'free'och 'test''premium' som distinkta värden för nivåkolumnen. Inte ens med en fråga som använder tier LIKE 'premium' PostgreSQL används indexet.

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

Ett sätt att förbättra prestanda är att dela upp datamängden över flera partitioner. Vi kan föreställa oss ett system när det är naturligt att referera till data bara från det aktuella året eller kanske de senaste två åren. I ett sådant system kan du partitionera dina data efter ett datumintervall och sedan dra nytta av bättre prestanda när systemet bara kan läsa relevanta partitioner enligt definitionen av det efterfrågade året.

Låt oss definiera en partitionerad tabell:

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

Vi kan skapa partitioner för varje år manuellt eller använda Citus-verktyget (tillgängligt i Cosmos DB för 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
    );

Kontrollera de skapade partitionerna:

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

Så här skapar du en partition manuellt:

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

Se sedan till att dina frågor faktiskt filtrerar ned till en delmängd av tillgängliga partitioner. I frågan nedan filtrerade vi till exempel ned till två partitioner:

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

Du kan indexering av en partitionerad tabell.

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)

Nästa steg

Grattis, du har precis lärt dig kompromisser, begränsningar och bästa praxis för att uppnå bästa prestanda med pgvector.