نحن نستعد للبحث عن النص الكامل في بوستجرس. الجزء 2

في المقالة الأخيرة ، قمنا بتحسين البحث في PostgreSQL باستخدام الأدوات القياسية. في هذه المقالة ، سنواصل تحسين استخدام مؤشر RUM وتحليل إيجابيات وسلبيات مقارنة مع GIN.


مقدمة


RUM امتداد لـ Postgres ، وهو فهرس جديد للبحث عن النص الكامل. يسمح لك بإرجاع النتائج مرتبة حسب الصلة عند المرور بالفهرس. لن أركز على تثبيته - فهو موصوف في README في المستودع.


نحن نستخدم فهرس


يتم إنشاء فهرس مشابه لمؤشر GIN ، ولكن مع بعض المعلمات. يمكن العثور على قائمة المعلمات بأكملها في الوثائق.


CREATE INDEX idx_rum_document ON documents_documentvector USING rum ("text" rum_tsvector_ops); 

استعلام البحث عن RUM:


 SELECT document_id, "text" <=> plainto_tsquery('') AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank; 

طلب للحصول على الجن
 SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC; 

الفرق من GIN هو أن الصلة يتم الحصول عليها لا باستخدام الدالة ts_rank ، ولكن باستخدام استعلام مع المشغل <=> : "text" <=> plainto_tsquery('') . يُرجع هذا الاستعلام بعض المسافة بين متجه البحث واستعلام البحث. كلما كان حجمها أصغر ، كلما كان الاستعلام أفضلًا مع المتجه.


مقارنة مع الجن


سنقارن هنا على أساس اختبار بـ 500 ألف مستند تقريبًا لإشعار الاختلافات في نتائج البحث.


طلب السرعة


دعونا نرى ما الذي ستنتجه شركة EXPLAIN for GIN في هذه القاعدة:


 Gather Merge (actual time=563.840..611.844 rows=119553 loops=1) Workers Planned: 2 Workers Launched: 2 -> Sort (actual time=553.427..557.857 rows=39851 loops=3) Sort Key: (ts_rank(text, plainto_tsquery(''::text))) Sort Method: external sort Disk: 1248kB -> Parallel Bitmap Heap Scan on documents_documentvector (actual time=13.402..538.879 rows=39851 loops=3) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=5616 -> Bitmap Index Scan on idx_gin_document (actual time=12.144..12.144 rows=119553 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 4.573 ms Execution time: 617.534 ms 

ولروم؟


 Sort (actual time=1668.573..1676.168 rows=119553 loops=1) Sort Key: ((text <=> plainto_tsquery(''::text))) Sort Method: external merge Disk: 3520kB -> Bitmap Heap Scan on documents_documentvector (actual time=16.706..1605.382 rows=119553 loops=1) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=15599 -> Bitmap Index Scan on idx_rum_document (actual time=14.548..14.548 rows=119553 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 0.650 ms Execution time: 1679.315 ms 

ما هذا؟ ما هو استخدام هذا RUM المتبجح ، كما تسأل ، إذا كان يعمل ثلاث مرات أبطأ من GIN؟ وأين الفرز الشهير داخل الفهرس؟


الهدوء: دعنا نحاول إضافة LIMIT 1000 إلى الطلب.


اشرح لـ RUM
  الحد (الوقت الفعلي = 115.568..137.313 الصفوف = 1000 حلقة = 1)
    -> فهرس المسح الضوئي باستخدام idx_rum_document على documents_documentvector (الوقت الفعلي = 115.567..137.239 الصفوف = 1000 حلقة = 1)
          فهرس الفهرس: (textplainto_tsquery ('query' :: text))
          الترتيب حسب: (text <=> plainto_tsquery ('query' :: text))
  وقت التخطيط: 0.481 مللي ثانية
  مدة التنفيذ: 137.678 مللي ثانية 

شرح للحصول على الجن
  الحد (الوقت الفعلي = 579.905..585.650 الصفوف = 1000 حلقة = 1)
    -> جمع دمج (الوقت الفعلي = 579.904..585.604 الصفوف = 1000 حلقة = 1)
          العمال المخطط: 2
          تم إطلاق العمال: 2
          -> الترتيب (الوقت الفعلي = 574.061..574.171 صفوف = 992 حلقة = 3)
                مفتاح التصنيف: (ts_rank (text ، plainto_tsquery ('query' :: text))) DESC
                طريقة الفرز: قرص دمج خارجي: 1224 كيلو بايت
                -> مسح كومة نقطية متوازية على documents_documentvector (الوقت الفعلي = 8.920..555.571 الصفوف = 39851 حلقة = 3)
                      إعادة فحص Cond: (textplainto_tsquery ('query' :: text))
                      كتل الكومة: بالضبط = 5422
                      -> مسح فهرس الصور النقطية على idx_gin_document (الوقت الفعلي = 8.945..8.945 الصفوف = 119553 حلقات = 1)
                            فهرس الفهرس: (textplainto_tsquery ('query' :: text))
  وقت التخطيط: 0.223 مللي ثانية
  مدة التنفيذ: 585.948 مللي ثانية 

~ 150 مللي مقابل ~ 600 مللي ثانية! بالفعل لا تؤيد GIN ، أليس كذلك؟ وقد انتقل الفرز داخل الفهرس!


وإذا كنت تبحث عن LIMIT 100 ؟


اشرح لـ RUM
  الحد (الوقت الفعلي = 105.863..108.530 الصفوف = 100 حلقة = 1)
    -> فهرس المسح الضوئي باستخدام idx_rum_document على documents_documentvector (الوقت الفعلي = 105.862..108.517 الصفوف = 100 حلقة = 1)
          فهرس الفهرس: (textplainto_tsquery ('query' :: text))
          الترتيب حسب: (text <=> plainto_tsquery ('query' :: text))
  وقت التخطيط: 0.199 مللي ثانية
  مدة التنفيذ: 108.958 مللي ثانية 

شرح للحصول على الجن
  الحد (الوقت الفعلي = 582.924..588.351 الصفوف = 100 حلقة = 1)
    -> جمع الدمج (الوقت الفعلي = 582.923..588.344 الصفوف = 100 حلقة = 1)
          العمال المخطط: 2
          تم إطلاق العمال: 2
          -> الترتيب (الوقت الفعلي = 573.809..573.889 الصفوف = 806 حلقة = 3)
                مفتاح التصنيف: (ts_rank (text ، plainto_tsquery ('query' :: text))) DESC
                طريقة الفرز: قرص دمج خارجي: 1224 كيلو بايت
                -> مسح كومة نقطية متوازية على documents_documentvector (الوقت الفعلي = 18.038..552.827 الصفوف = 39851 حلقة = 3)
                      إعادة فحص Cond: (textplainto_tsquery ('query' :: text))
                      كتل الكومة: بالضبط = 5275
                      -> مسح فهرس الصور النقطية على idx_gin_document (الوقت الفعلي = 16.541..16.541 الصفوف = 119553 حلقات = 1)
                            فهرس الفهرس: (textplainto_tsquery ('query' :: text))
  وقت التخطيط: 0.487 مللي ثانية
  مدة التنفيذ: 588.583 مللي ثانية 

الفرق هو أكثر وضوحا.


الشيء المهم هو أن GIN لا يهم بالضبط عدد الخطوط التي تحصل عليها في النهاية - بل يجب أن تمر عبر جميع الخطوط التي نجح الطلب ، وترتيبها. RUM يفعل هذا فقط لتلك الخطوط التي نحتاجها حقًا. إذا احتجنا إلى الكثير من الخطوط ، يفوز GIN. يقوم ts_rank الخاص ts_rank بإجراء العمليات الحسابية بشكل ts_rank كفاءة من المشغل <=> . ولكن في الاستعلامات الصغيرة ، لا يمكن إنكار ميزة RUM.


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


تاريخ التسامح


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


قارن:


طلب مع اثنين من الانضمام للحصول على الجن
 SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank, case_number FROM documents_documentvector RIGHT JOIN documents_document ON documents_documentvector.document_id = documents_document.id LEFT JOIN documents_case ON documents_document.case_id = documents_case.id WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC LIMIT 10; 

النتيجة:


 الحد (الوقت الفعلي = 1637.902..1643.483 الصفوف = 10 حلقات = 1)
    -> جمع الدمج (الوقت الفعلي = 1637.901..1643.479 الصفوف = 10 حلقات = 1)
          العمال المخطط: 2
          تم إطلاق العمال: 2
          -> الترتيب (الوقت الفعلي = 1070.614..1070.687 الصفوف = 652 حلقة = 3)
                مفتاح التصنيف: (ts_rank (documents_documentvector.text، plainto_tsquery ('query' :: text))) DESC
                طريقة الفرز: قرص دمج خارجي: 2968 كيلو بايت
                -> Hash Left Join (الوقت الفعلي = 323.386..1049.092 صفوف = 39851 حلقة = 3)
                      Hash Cond: (documents_document.case_id = documents_case.id)
                      -> التجزئة التجزئة (الوقت الفعلي = 239.312..324.797 الصفوف = 39851 حلقة = 3)
                            Hash Cond: (documents_documentvector.document_id = documents_document.id)
                            -> مسح كومة نقطية متوازية على documents_documentvector (الوقت الفعلي = 11.022..37.073 الصفوف = 39851 حلقة = 3)
                                  إعادة فحص Cond: (textplainto_tsquery ('query' :: text))
                                  كتل الكومة: بالضبط = 9362
                                  -> مسح فهرس الصور النقطية على idx_gin_document (الوقت الفعلي = 12.094..12.094 الصفوف = 119553 حلقات = 1)
                                        فهرس الفهرس: (textplainto_tsquery ('query' :: text))
                            -> التجزئة (الوقت الفعلي = 227.856..227.856 الصفوف = 472089 حلقات = 3)
                                  الدلاء: 65536 الدفعات: 16 استخدام الذاكرة: 2264 كيلو بايت
                                  -> Seq Scan على documents_document (الوقت الفعلي = 0.009..147.104 الصفوف = 472089 حلقة = 3)
                      -> التجزئة (الوقت الفعلي = 83.338..83.338 الصفوف = 273695 حلقة = 3)
                            الدلاء: 65536 الدفعات: 8 استخدام الذاكرة: 2602 كيلو بايت
                            -> Seq Scan على documents_case (الوقت الفعلي = 0.009..39.082 الصفوف = 273695 حلقة = 3)
 وقت التخطيط: 0.857 مللي ثانية
 مدة التنفيذ: 1644.028 مللي ثانية

في ثلاث صلات وأكبر ، يصل وقت الطلب إلى 2-3 ثوان وينمو بعدد صلات.


ولكن ماذا عن رم؟ دع الطلب يتم على الفور مع خمسة صلات.


خمسة الانضمام طلب للحصول على رم
 SELECT document_id, "text" <=> plainto_tsquery('') AS rank, case_number, classifier_procedure.title, classifier_division.title, classifier_category.title FROM documents_documentvector RIGHT JOIN documents_document ON documents_documentvector.document_id = documents_document.id LEFT JOIN documents_case ON documents_document.case_id = documents_case.id LEFT JOIN classifier_procedure ON documents_case.procedure_id = classifier_procedure.id LEFT JOIN classifier_division ON documents_case.division_id = classifier_division.id LEFT JOIN classifier_category ON documents_document.category_id = classifier_category.id WHERE "text" @@ plainto_tsquery('') AND documents_document.is_active IS TRUE ORDER BY rank LIMIT 10; 

النتيجة:


  الحد (الوقت الفعلي = 70.524..72.292 الصفوف = 10 حلقات = 1)
   -> Left Loop Left Join (الوقت الفعلي = 70.521..72.279 صفوف = 10 حلقات = 1)
         -> الانضمام إلى اليسار حلقة متداخلة (الوقت الفعلي = 70.104..70.406 الصفوف = 10 حلقات = 1)
               -> Left Loop Left Join (الوقت الفعلي = 70.089..70.351 صفوف = 10 حلقات = 1)
                     -> Left Loop Left Join (الوقت الفعلي = 70.073..70.302 الصفوف = 10 حلقات = 1)
                           -> حلقة متداخلة (الوقت الفعلي = 70.052..70.201 الصفوف = 10 حلقات = 1)
                                 -> فهرس المسح الضوئي باستخدام document_vector_rum_index على documents_documentvector (الوقت الفعلي = 70.001..70.035 الصفوف = 10 حلقات = 1)
                                       فهرس الفهرس: (textplainto_tsquery ('query' :: text))
                                       الترتيب حسب: (text <=> plainto_tsquery ('query' :: text))
                                 -> فهرس المسح الضوئي باستخدام documents_document_pkey على documents_document (الوقت الفعلي = 0.013..0.013 الصفوف = 1 حلقات = 10)
                                       فهرس الفهرس: (id = documents_documentvector.document_id)
                                       عامل التصفية: (is_active IS TRUE)
                           -> فهرس المسح الضوئي باستخدام documents_case_pkey على documents_case (الوقت الفعلي = 0.009..0.009 الصفوف = 1 حلقات = 10)
                                 فهرس الفهرس: (documents_document.case_id = id)
                     -> فهرس المسح الضوئي باستخدام classifier_procedure_pkey على classifier_procedure (الوقت الفعلي = 0.003..0.003 الصفوف = 1 حلقات = 10)
                           فهرس الفهرس: (documents_case.procedure_id = id)
               -> فهرس المسح الضوئي باستخدام classifier_division_pkey على classifier_division (الوقت الفعلي = 0.004..0.004 الصفوف = 1 حلقات = 10)
                     فهرس الفهرس: (documents_case.division_id = id)
         -> فهرس المسح الضوئي باستخدام classifier_category_pkey على classifier_category (الوقت الفعلي = 0.003..0.003 الصفوف = 1 حلقات = 10)
               فهرس الفهرس: (documents_document.category_id = id)
 وقت التخطيط: 2.861 مللي ثانية
 مدة التنفيذ: 72.865 مللي ثانية

إذا لم يكن بإمكانك الاستغناء عن الانضمام عند البحث ، فسيكون RUM مناسبًا لك بوضوح.


مساحة القرص


على قاعدة اختبار من حوالي 500 ألف وثيقة و 3.6 غيغابايت فهارس احتلت أحجام مختلفة جدا .


  idx_rum_document |  1950 ميغابايت
  idx_gin_document |  418 ميغابايت

نعم ، محرك رخيص. ولكن 2 غيغابايت بدلاً من 400 ميغابايت لا يمكن الرجاء. نصف حجم القاعدة كثيرًا بالنسبة للمؤشر. هنا يفوز GIN دون قيد أو شرط.


النتائج


تحتاج إلى رم إذا:


  • لديك الكثير من المستندات ، لكنك تعطي صفحة نتائج البحث صفحة
  • تحتاج إلى تصفية متطورة لنتائج البحث
  • أنت لا تمانع في مساحة القرص

ستكون راضيًا تمامًا عن GIN إذا:


  • لديك قاعدة صغيرة
  • لديك قاعدة كبيرة ، ولكن عليك أن تنتج نتائج على الفور وهذا كل شيء
  • لا تحتاج إلى تصفية مع Join s
  • هل أنت مهتم في الحد الأدنى لحجم الفهرس على القرص

آمل أن تزيل هذه المقالة الكثير من WTF ؟! يحدث ذلك عند العمل وإعداد البحث في بوستجرس. سأكون سعيدًا لسماع نصيحة من أولئك الذين يعرفون كيفية تكوين كل شيء بشكل أفضل!)


أخطط في الأجزاء التالية لمعرفة المزيد عن RUM في مشروعي: حول استخدام خيارات RUM إضافية ، والعمل في حزمة Django + PostgreSQL.

Source: https://habr.com/ru/post/ar443368/


All Articles