Поделиться через


Оптимизация производительности при использовании pgvector в Azure Cosmos DB для PostgreSQL

Область применения: Azure Cosmos DB для PostgreSQL (на базе расширения базы данных Citus до PostgreSQL)

Расширение pgvector добавляет поиск сходства векторов с открытым исходным кодом в PostgreSQL.

В этой статье рассматриваются ограничения и компромиссы pgvector и показано, как использовать параметры секционирования, индексирования и поиска для повышения производительности.

Дополнительные сведения о самом расширении см . в основах pgvector. Вы также можете обратиться к официальному README проекта.

Производительность

Следует всегда начинать с изучения плана запроса. Если запрос завершается достаточно быстро, выполните команду EXPLAIN (ANALYZE,VERBOSE, BUFFERS).

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

Для выполнения запросов, которые занимают слишком много времени, рекомендуется удалить ANALYZE ключевое слово. Результат содержит меньше сведений, но предоставляется мгновенно.

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

Сторонние сайты, такие как explain.depesz.com , могут быть полезны в понимании планов запросов. Ниже приведены некоторые вопросы, которые следует попытаться ответить:

Если векторы нормализуются до длины 1, например внедрения OpenAI. Для оптимальной производительности следует использовать внутренний продукт (<#>).

Параллельное выполнение

В выходных данных плана объяснения найдите Workers Planned и Workers Launched (последний только при ANALYZE использовании ключевого слова). Параметр max_parallel_workers_per_gather PostgreSQL определяет количество фоновых рабочих ролей, которые база данных может запускать для каждого Gather узла и Gather Merge плана. Увеличение этого значения может ускорить точные поисковые запросы, не создавая индексы. Обратите внимание, что база данных может не решить запустить план параллельно, даже если это значение имеет высокий уровень.

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)

Индексирование

Без индексов расширение выполняет точный поиск, который обеспечивает идеальный отзыв за счет производительности.

Чтобы выполнить приблизительный поиск ближайших соседей, можно создать индексы данных, которые торгуют отзывом на производительность выполнения.

По возможности всегда загружайте данные перед индексированием. Это как быстрее создать индекс таким образом, так и результирующий макет является более оптимальным.

Существует два поддерживаемых типа индексов:

Индекс IVFFlat имеет более быстрое время сборки и использует меньше памяти, чем HNSW, но имеет более низкую производительность запросов (с точки зрения компромисса со скоростью отзыва).

Ограничения

  • Чтобы индексировать столбец, он должен иметь определенные измерения. Попытка индексировать столбец, определенный как col vector результат ошибки: ERROR: column does not have dimensions
  • Можно индексировать только столбец с размером до 2000 измерений. Попытка индексировать столбец с большими измерениями приводит к ошибке: ERROR: column cannot have more than 2000 dimensions for INDEX_TYPE index где INDEX_TYPE находится либоhnswivfflat.

Хотя вы можете хранить векторы с более чем 2000 измерениями, их нельзя индексировать. Вы можете использовать уменьшение размерности, чтобы соответствовать ограничениям. Кроме того, следует использовать секционирование и (или) сегментирование с помощью Azure Cosmos DB для PostgreSQL для достижения приемлемой производительности без индексирования.

Инвертированный файл с неструктурированным сжатием (IVVFlat)

Индекс ivfflat для приблизительного поиска ближайшего соседа (ANN). Этот метод использует инвертированные индексы файлов для секционирования набора данных в несколько списков. Параметр пробы определяет количество списков поиска, что может повысить точность результатов поиска за счет более медленной скорости поиска.

Если для параметра пробы задано число списков в индексе, то поиск всех списков становится точным ближайшим поиском соседей. В этом случае планировщик не использует индекс, так как поиск всех списков эквивалентен выполнению подбора для всего набора данных.

Метод индексирования секционирует набор данных в несколько списков с помощью алгоритма кластеризации k-средних. Каждый список содержит векторы, ближайшие к определенному центру кластера. Во время поиска вектор запроса сравнивается с центрами кластера, чтобы определить, какие списки, скорее всего, содержат ближайшие соседи. Если для параметра пробы задано значение 1, поиск будет выполнен только в списке, соответствующем ближайшему центру кластера.

Параметры индекса

Выбор правильного значения количества проб для выполнения и размеров списков может повлиять на производительность поиска. Хорошие места для начала:

  1. Используйте lists равный rows / 1000 для таблиц размером до 1 миллионов строк и sqrt(rows) для больших наборов данных.
  2. Для probes начала для lists / 10 таблиц до 1 миллиона строк и sqrt(lists) для больших наборов данных.

Количество lists определяется при создании индекса с параметром lists :

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

Пробы можно задать для всего подключения или для каждой транзакции (используя SET LOCAL в блоке транзакций):

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

Ход индексирования

С помощью PostgreSQL 12 и более поздней версии можно использовать pg_stat_progress_create_index для проверки хода выполнения индексирования.

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

Этапы создания индексов IVFlat:

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

Примечание.

Процент хода выполнения (%) заполняется только на loading tuples этапе.

Иерархические навигации небольшие миры (HNSW)

Это hnsw индекс для приблизительного ближайшего соседа (ANN) поиска с помощью алгоритма иерархических навигационно-малых миров. Он работает путем создания графа вокруг случайных выбранных точек входа поиска ближайших соседей, граф затем расширяется с несколькими слоями, каждый нижний слой, содержащий больше точек. Этот многослойный граф при поиске начинается в верхней части, сужая вниз, пока не достигнет самого низкого слоя, содержащего ближайшие соседи запроса.

Создание этого индекса занимает больше времени и памяти, чем IVFlat, однако он имеет лучший компромисс со скоростью отзыва. Кроме того, нет шага обучения, как с IVFlat, поэтому индекс можно создать в пустой таблице.

Параметры индекса

При создании индекса можно настроить два параметра:

  1. m — максимальное количество подключений на слой (по умолчанию — 16)
  2. ef_construction — размер динамического списка кандидатов, используемый для построения графа (по умолчанию — 64)
CREATE INDEX t_test_hnsw_l2_idx ON t_test USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);

Во время запросов можно указать динамический список кандидатов для поиска (по умолчанию — 40).

Динамический список кандидатов для поиска можно задать для всего подключения или для каждой транзакции (используя SET LOCAL в блоке транзакций):

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

Ход индексирования

С помощью PostgreSQL 12 и более поздней версии можно использовать pg_stat_progress_create_index для проверки хода выполнения индексирования.

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

Этапы создания индексов HNSW:

  1. initializing
  2. loading tuples

Выбор функции доступа к индексу

Тип vector позволяет выполнять три типа поиска по хранимым векторам. Необходимо выбрать правильную функцию доступа для индекса, чтобы база данных учитывала индекс при выполнении запросов. Примеры демонстрируют ivfflat типы индексов, однако их можно сделать для hnsw индексов. Параметр lists применяется только к ivfflat индексам.

Расстояние косинуса

Для совместного vector_cosine_ops поиска сходства используйте метод доступа.

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

Чтобы использовать приведенный выше индекс, запросу необходимо выполнить поиск сходства косинуса, который выполняется с оператором <=> .

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

Для расстояния L2 (также известного vector_l2_ops как Евклидеан расстояние), используйте метод доступа.

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

Чтобы использовать приведенный выше индекс, запрос должен выполнять поиск по расстоянию L2, который выполняется с оператором <-> .

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)

Скалярное произведение

Для внутреннего сходства продуктов используйте vector_ip_ops метод доступа.

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

Чтобы использовать приведенный выше индекс, запрос должен выполнить внутренний поиск сходства продуктов, который выполняется с оператором <#> .

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)

Частичные индексы

В некоторых сценариях полезно иметь индекс, охватывающий только частичный набор данных. Например, можно создать индекс только для наших пользователей класса Premium:

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

Теперь мы видим, что уровень "Премиум" теперь использует индекс:

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)

Хотя пользователи уровня "Бесплатный" не имеют преимущества:

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)

Наличие только подмножества индексированных данных означает, что индекс занимает меньше места на диске и быстрее выполняет поиск.

PostgreSQL может не распознать, что индекс является безопасным для использования, если форма, используемая в WHERE предложении определения частичного индекса, не соответствует одному, используемому в запросах. В нашем примере набора данных у нас есть только точные значения 'free', 'test' а 'premium' также в качестве отдельных значений столбца уровня. Даже при выполнении запроса с помощью tier LIKE 'premium' PostgreSQL индекс не используется.

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)

Секционирование

Одним из способов повышения производительности является разделение набора данных по нескольким секциям. Мы можем представить систему, когда это естественно ссылаться на данные только с текущего года или, возможно, последние два года. В такой системе можно секционировать данные по диапазону дат, а затем увеличить производительность, когда система сможет считывать только соответствующие секции, как определено запросным годом.

Определим секционированную таблицу:

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

Можно вручную создавать секции для каждого года или использовать служебную функцию Citus (доступную в Cosmos DB для 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
    );

Проверьте созданные секции:

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

Чтобы создать секцию вручную, выполните приведенные действия.

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

Затем убедитесь, что запросы фактически фильтруется до подмножества доступных секций. Например, в приведенном ниже запросе мы отфильтровали до двух разделов:

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

Можно индексировать секционированную таблицу.

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)

Заключение

Поздравляем, вы только что узнали компромиссы, ограничения и рекомендации по достижению наилучшей производительности.pgvector