كيفية تحسين الأداء عند استخدام pgvector على قاعدة بيانات Azure ل PostgreSQL - خادم مرن

ينطبق على: قاعدة بيانات Azure ل PostgreSQL - خادم مرن

pgvector يضيف الملحق بحث تشابه متجه مفتوح المصدر إلى قاعدة بيانات Azure لخادم 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 إما ivfflat أو hnsw.

بينما يمكنك تخزين المتجهات بأكثر من 2000 بعد، لا يمكنك فهرستها. يمكنك استخدام تقليل الأبعاد للاحتواء ضمن الحدود. بدلا من ذلك، الاعتماد على التقسيم و/أو التقسيم باستخدام Azure Cosmos DB ل PostgreSQL لتحقيق أداء مقبول دون فهرسة.

ملف مقلوب مع ضغط مسطح (IVVFlat)

ivfflat هو فهرس للبحث التقريبي عن أقرب جار (ANN). يستخدم هذا الأسلوب فهرس ملف مقلوب لتقسيم مجموعة البيانات إلى قوائم متعددة. تتحكم معلمة التحقيق في عدد القوائم التي يتم البحث فيها، والتي يمكن أن تحسن دقة نتائج البحث على حساب سرعة بحث أبطأ.

إذا تم تعيين معلمة probes إلى عدد القوائم في الفهرس، البحث في جميع القوائم ويصبح البحث أقرب بحث جار بالضبط. في هذه الحالة، لا يستخدم المخطط الفهرس لأن البحث في جميع القوائم يعادل إجراء بحث بقوة غاشمة على مجموعة البيانات بأكملها.

يقسم أسلوب الفهرسة مجموعة البيانات إلى قوائم متعددة باستخدام خوارزمية تكوين أنظمة المجموعات k-means. تحتوي كل قائمة على خطوط متجهة الأقرب إلى مركز مجموعة معين. أثناء البحث، تتم مقارنة متجه الاستعلام بمراكز نظام المجموعة لتحديد القوائم التي من المرجح أن تحتوي على أقرب الجيران. إذا تم تعيين معلمة التحقيق إلى 1، البحث فقط في القائمة المقابلة لأقرب مركز نظام مجموعة.

خيارات الفهرس

قد يؤثر تحديد القيمة الصحيحة لعدد التحقيقات التي سيتم إجراؤها وأحجام القوائم على أداء البحث. الأماكن الجيدة للبدء هي:

  1. استخدم lists يساوي rows / 1000 للجداول التي تحتوي على ما يصل إلى مليون صف ولمجموعات sqrt(rows) البيانات الأكبر.
  2. للبدء probes بجداول lists / 10 تصل إلى مليون صف ومجموعات 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) باستخدام خوارزمية العوام الصغيرة القابلة للتنقل الهرمية. وهو يعمل عن طريق إنشاء رسم بياني حول نقاط الإدخال المحددة عشوائيا للعثور على أقرب جيرانهم، ثم يتم توسيع الرسم البياني مع طبقات متعددة، كل طبقة صغيرة تحتوي على المزيد من النقاط. يبدأ هذا الرسم البياني متعدد الطبقات عند البحث في الأعلى، ويضيق لأسفل حتى يصل إلى أدنى طبقة تحتوي على أقرب جيران الاستعلام.

يستغرق بناء هذا الفهرس وقتا وذاكرة أكثر من IVFFlat، ولكن لديه أفضل مقايضة استدعاء السرعة. بالإضافة إلى ذلك، لا توجد خطوة تدريب مثل IVFFlat، لذلك يمكن إنشاء الفهرس على جدول فارغ.

خيارات الفهرس

عند إنشاء الفهرس، يمكنك ضبط معلمتين:

  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)

فهارس جزئية

في بعض السيناريوهات، من المفيد أن يكون لديك فهرس يغطي مجموعة جزئية فقط من البيانات. يمكننا، على سبيل المثال، إنشاء فهرس لمستخدمينا المتميزين فقط:

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.