Teljesítmény optimalizálása pgvector használata esetén az Azure Cosmos DB for PostgreSQL-ben
A KÖVETKEZŐKRE VONATKOZIK: Azure Cosmos DB for PostgreSQL (a Citus adatbázisbővítménye a PostgreSQL-re)
A pgvector
bővítmény egy nyílt forráskódú vektoros hasonlósági keresést ad hozzá a PostgreSQL-hez.
Ez a cikk a particionálás, az indexelés és a keresési beállítások használatának korlátozásait és kompromisszumait pgvector
ismerteti a teljesítmény javítása érdekében.
A bővítményről további információt a következő alapvető tudnivalókban talál : .pgvector> Érdemes lehet a projekt hivatalos README-jára is hivatkozni.
Teljesítmény
Először mindig vizsgálja meg a lekérdezési tervet. Ha a lekérdezés meglehetősen gyorsan leáll, futtassa a parancsot EXPLAIN (ANALYZE,VERBOSE, BUFFERS)
.
EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
Az olyan lekérdezések esetében, amelyek végrehajtása túl sokáig tart, érdemes lehet elvetni a kulcsszót ANALYZE
. Az eredmény kevesebb részletet tartalmaz, de azonnal meg lesz adva.
EXPLAIN (VERBOSE, BUFFERS) SELECT * FROM t_test ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
A külső webhelyek, például a explain.depesz.com hasznosak lehetnek a lekérdezési tervek megértésében. Néhány kérdés, amelyet meg kell próbálnia megválaszolni, a következők:
- Párhuzamos volt a lekérdezés?
- Használt indexet?
- Ugyanazt a feltételt használtam a WHERE záradékban, mint egy részleges indexdefinícióban?
- Ha particionálást használok, a nem szükséges partíciókat metszették?
Ha a vektorok 1 hosszúságúra vannak normalizálva, például OpenAI-beágyazásokra. Érdemes megfontolni a belső termék (<#>
) használatát a legjobb teljesítmény érdekében.
Párhuzamos végrehajtás
A magyarázó terv kimenetében keresse meg Workers Planned
és Workers Launched
(csak kulcsszó használata esetén ANALYZE
). A max_parallel_workers_per_gather
PostgreSQL paraméter határozza meg, hogy az adatbázis hány háttérmunkást indíthat el minden Gather
egyes csomóponthoz, és Gather Merge
tervezze meg a csomópontokat. Az érték növelése felgyorsíthatja a pontos keresési lekérdezéseket anélkül, hogy indexeket kellene létrehoznia. Vegye figyelembe azonban, hogy előfordulhat, hogy az adatbázis nem úgy dönt, hogy párhuzamosan futtatja a tervet, még akkor sem, ha ez az érték magas.
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)
Indexelés
Indexek nélkül a bővítmény pontos keresést végez, amely tökéletes visszahívást biztosít a teljesítmény rovására.
A legközelebbi szomszédkeresés végrehajtásához indexeket hozhat létre az adatokon, amelyek a végrehajtási teljesítményre való visszahívással kereskednek.
Amikor lehetséges, az indexelés előtt mindig töltse be az adatokat. Az index így gyorsabban hozható létre, és az eredményül kapott elrendezés optimálisabb.
Két támogatott indextípus létezik:
Az IVFFlat
index gyorsabb buildelési idővel rendelkezik, és kevesebb memóriát használ, mint HNSW
, de alacsonyabb lekérdezési teljesítménnyel rendelkezik (a gyors visszahívási kompromisszum szempontjából).
Korlátok
- Egy oszlop indexeléséhez dimenzióknak kell definiálnia. Az eredményként
col vector
definiált oszlop indexelésének kísérlete a következő hibára:ERROR: column does not have dimensions
. - Legfeljebb 2000 dimenzióval rendelkező oszlop indexelhető. Ha több dimenzióval rendelkező oszlopot próbál indexelni, a következő hibaüzenet jelenik meg:
ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index
holINDEX_TYPE
találhatóivfflat
vagyhnsw
.
Bár több mint 2000 dimenzióval rendelkező vektorokat tárolhat, nem indexelheti őket. A méretcsökkentéssel a korlátokon belül is elfér. Másik lehetőségként az Azure Cosmos DB for PostgreSQL particionálására és/vagy horizontális skálázására is támaszkodhat, hogy indexelés nélkül elfogadható teljesítményt érjen el.
Invertált fájl lapos tömörítéssel (IVVFlat)
Ez ivfflat
egy index a legközelebbi szomszéd (ANN) kereséséhez. Ez a módszer fordított fájlindexet használ az adathalmaz több listára való particionálásához. A mintavételi paraméter szabályozza, hogy hány lista van keresve, ami a lassabb keresési sebesség árán javíthatja a keresési eredmények pontosságát.
Ha a mintavételi paraméter az indexben lévő listák számára van beállítva, akkor a rendszer az összes listát megkeresi, és a keresés a legközelebbi szomszédkereséssé válik. Ebben az esetben a tervező nem használja az indexet, mert az összes lista keresése egyenértékű egy találgatásos keresés végrehajtásával a teljes adatkészleten.
Az indexelési módszer több listára particionálja az adathalmazt a k-means fürtözési algoritmus használatával. Minden lista egy adott fürtközponthoz legközelebbi vektorokat tartalmaz. A keresés során a lekérdezésvektort összehasonlítjuk a fürtközpontokkal annak meghatározásához, hogy mely listák tartalmazzák a legközelebbi szomszédokat. Ha a mintavételi paraméter értéke 1, akkor csak a legközelebbi fürtközpontnak megfelelő lista lesz keresve.
Indexelési beállítások
Ha a mintavételek számának megfelelő értéket választja ki, és a listák mérete hatással lehet a keresési teljesítményre. Jó kiindulópontok a következők:
rows / 1000
Legfeljebblists
1 millió sorból éssqrt(rows)
nagyobb adathalmazból áll.lists / 10
Elsőprobes
lépésként legfeljebb 1 millió sort tartalmazó táblákhoz éssqrt(lists)
nagyobb adathalmazokhoz.
Az érték lists
az index létrehozásakor lesz meghatározva a lists
következő beállítással:
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 5000);
A mintavételek beállíthatók a teljes kapcsolathoz vagy tranzakciónként (tranzakcióblokkon SET LOCAL
belül):
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
Indexelés folyamata
A PostgreSQL 12 és újabb pg_stat_progress_create_index
használatával ellenőrizheti az indexelés előrehaladását.
SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
Az IVFFlat-indexek létrehozásának fázisai a következők:
initializing
performing k-means
assigning tuples
loading tuples
Feljegyzés
A folyamat százalékos aránya (%
) csak a fázisban loading tuples
van kitöltve.
Hierarchikus navigálható kisvilágok (HNSW)
Ez hnsw
egy index a legközelebbi szomszéd (ANN) kereséshez a Hierarchikus Navigable Small Worlds algoritmus használatával. Úgy működik, hogy egy gráfot hoz létre a véletlenszerűen kiválasztott belépési pontok köré, és megkeresi a legközelebbi szomszédokat. A gráf ezután több réteggel bővül, mindegyik alsó réteg több pontot tartalmaz. Ez a többrétegű gráf kereséskor a tetején kezdődik, és szűkíti a gráfot, amíg el nem éri a lekérdezés legközelebbi szomszédait tartalmazó legalacsonyabb réteget.
Az index létrehozása több időt és memóriát igényel, mint az IVFFlat, azonban jobb sebességvisszahívási kompromisszumokkal rendelkezik. Emellett nincs olyan betanítási lépés, mint az IVFFlat esetén, így az index egy üres táblán hozható létre.
Indexelési beállítások
Az index létrehozásakor két paramétert hangolhat:
m
- a kapcsolatok maximális száma rétegenként (alapértelmezés szerint 16)ef_construction
- a gráfépítéshez használt dinamikus jelöltlista mérete (alapértelmezés szerint 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
A lekérdezések során megadhatja a keresés dinamikus jelöltlistáját (alapértelmezés szerint 40).
A dinamikus keresésre jelölt lista a teljes kapcsolatra vagy tranzakciónként (tranzakcióblokkon belül) SET LOCAL
állítható be:
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
Indexelés folyamata
A PostgreSQL 12 és újabb pg_stat_progress_create_index
használatával ellenőrizheti az indexelés előrehaladását.
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
A HNSW-indexek létrehozásának fázisai a következők:
initializing
loading tuples
Az indexelérési függvény kiválasztása
A vector
típussal háromféle keresést hajthat végre a tárolt vektorokon. Ki kell választania az index megfelelő hozzáférési függvényét, hogy az adatbázis figyelembe vegye az indexet a lekérdezések végrehajtásakor. A példák az indextípusokra ivfflat
mutatnak be, de ugyanez elvégezhető az indexek esetében hnsw
is. Ez a lists
beállítás csak az ivfflat
indexekre vonatkozik.
Koszinusz távolsága
A koszinusza hasonlóság kereséséhez használja a vector_cosine_ops
hozzáférési módszert.
CREATE INDEX t_test_embedding_cosine_idx ON t_test USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
A fenti index használatához a lekérdezésnek cosine hasonlósági keresést kell végeznie, amely az <=>
operátorral történik.
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 távolság
Az L2 távolság (más néven euklideszi távolság) esetében használja a vector_l2_ops
hozzáférési módszert.
CREATE INDEX t_test_embedding_l2_idx ON t_test USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
A fenti index használatához a lekérdezésnek L2 távolsági keresést kell végrehajtania, amely az <->
operátorral történik.
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)
Belső termék
A belső termék hasonlóságához használja a vector_ip_ops
hozzáférési módszert.
CREATE INDEX t_test_embedding_ip_idx ON t_test USING ivfflat (embedding vector_ip_ops) WITH (lists = 100);
A fenti index használatához a lekérdezésnek belső termék-hasonlósági keresést kell végeznie, amely az <#>
operátorral történik.
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)
Részleges indexek
Bizonyos esetekben előnyös, ha olyan indexet használ, amely csak az adatok egy részét fedi le. Létrehozhatunk például egy indexet csak a prémium felhasználók számára:
CREATE INDEX t_premium ON t_test USING ivfflat (vec vector_ip_ops) WITH (lists = 100) WHERE tier = 'premium';
Most már látható, hogy a prémium szint az indexet használja:
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)
Míg az ingyenes szintű felhasználók nem rendelkeznek az előnyökkel:
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)
Ha csak az adatok egy részhalmazát indexeli, az azt jelenti, hogy az index kevesebb helyet foglal el a lemezen, és gyorsabb a keresés.
Előfordulhat, hogy a PostgreSQL nem ismeri fel, hogy az index biztonságosan használható, ha a részleges indexdefiníció záradékában WHERE
használt űrlap nem egyezik meg a lekérdezésekben használt űrlappal.
A példaadatkészletben csak a pontos értékek 'free'
, 'test'
valamint 'premium'
a rétegoszlop különböző értékei szerepelnek. Még a PostgreSQL-t használó tier LIKE 'premium'
lekérdezések sem használják az 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)
Particionálás
A teljesítmény javításának egyik módja az adathalmaz több partícióra való felosztása. El tudunk képzelni egy rendszert, amikor természetes, hogy csak az aktuális évből vagy az elmúlt két évből származó adatokra hivatkozunk. Ilyen rendszerben dátumtartomány szerint particionálhatja az adatokat, majd kihasználhatja a jobb teljesítményt, ha a rendszer csak a lekérdezett év által meghatározott megfelelő partíciókat tudja beolvasni.
Definiáljunk egy particionált táblát:
CREATE TABLE t_test_partitioned(vec vector(3), vec_date date default now()) partition by range (vec_date);
Minden évben létrehozhatunk manuálisan partíciókat, vagy használhatjuk a Citus segédprogramot (elérhető a Cosmos DB for PostgreSQL-ben).
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
);
Ellenőrizze a létrehozott partíciókat:
\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')
Partíció manuális létrehozása:
CREATE TABLE t_test_partitioned_p2019 PARTITION OF t_test_partitioned FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
Ezután győződjön meg arról, hogy a lekérdezések valójában az elérhető partíciók egy részhalmazára szűrnek. Az alábbi lekérdezésben például két partícióra szűrtünk le:
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
A particionált táblák indexelhetők.
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)
Összegzés
Gratulálunk, most tanulta meg a kompromisszumokat, korlátozásokat és ajánlott eljárásokat a legjobb teljesítmény pgvector
eléréséhez.