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

محدث الجزء 2


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


التعادل


أؤيد مشروعًا كبيرًا نسبيًا يحتوي على بحث عام على المستندات. تحتوي قاعدة البيانات على حوالي 500 ألف مستند بإجمالي حجم يصل إلى 3.6 جيجابايت. جوهر البحث هو هذا: المستخدم يملأ نموذجًا به استعلام نص كامل وتصفية حسب مجموعة متنوعة من الحقول في قاعدة البيانات ، بما في ذلك مع Join-s.


يعمل البحث (أو بالأحرى ، يعمل) من خلال Sphinx ، ولم يعمل بشكل جيد للغاية. كانت المشاكل الرئيسية على النحو التالي:


  1. الفهرسة تستهلك حوالي 8 جيجابايت من ذاكرة الوصول العشوائي. هذه مشكلة على خادم به 8 جيجابايت من ذاكرة الوصول العشوائي. تبديل الذاكرة ، أدى إلى أداء فظيع .
  2. تم بناء المؤشر في حوالي 40 دقيقة. لم يكن هناك أي شك في مدى اتساق نتائج البحث ؛ فقد تم إطلاق الفهرسة مرة واحدة يوميًا.
  3. البحث يعمل لفترة طويلة . تم تنفيذ الطلبات لفترة طويلة بشكل خاص ، والتي تتوافق مع عدد كبير من الوثائق: تم نقل عدد هائل من أصحاب المعرفات من أبو الهول إلى قاعدة البيانات ، وفرزها حسب الصلة على الواجهة الخلفية.

بسبب هذه المشاكل ، نشأت المهمة - لتحسين البحث عن النص الكامل. هذه المهمة لها حلان:


  1. تشديد Sphinx: تكوين فهرس حقيقي ، وتخزين سمات للتصفية في الفهرس.
  2. استخدام المدمج في FTS PostgreSQL.

تقرر تنفيذ الحل الثاني: وبهذه الطريقة يمكنك توفير التحديث التلقائي للفهرس ، والتخلص من التواصل الطويل بين خدمتين ومراقبة خدمة واحدة بدلاً من خدمتين.


يبدو أن يكون حلا جيدا. لكن المشاكل ما زالت قائمة.


لنبدأ من البداية.


نحن نستخدم بسذاجة البحث عن النص الكامل


كما تقول الوثائق ، تتطلب عمليات البحث عن النص الكامل استخدام tsquery و tsquery . أول واحد يخزن نص المستند في نموذج محسّن للبحث ، والثاني يخزن استعلام النص الكامل.


للبحث في PostgreSQL ، هناك وظائف to_tsvector ، plainto_tsquery ، to_tsquery . لترتيب النتائج هناك ts_rank . استخدامها سهل الاستخدام وهو موصوف جيدًا في الوثائق ، لذلك لن نتناول تفاصيل استخدامهم.


سيبدو استعلام البحث التقليدي الذي يستخدمه كما يلي:


 SELECT id, ts_rank(to_tsvector("document_text"), plainto_tsquery('')) FROM documents_document WHERE to_tsvector("document_text") @@ plainto_tsquery('') ORDER BY ts_rank(to_tsvector("document_text"), plainto_tsquery('')) DESC; 

استنتجنا معرفات المستندات في النص الذي توجد به كلمة "استعلام" ، وصنفناها بترتيب تنازلي ذي صلة. يبدو أن كل شيء على ما يرام؟ رقم


النهج أعلاه لديه العديد من العيوب:


  1. نحن لا نستخدم الفهرس للبحث.
  2. يتم استدعاء الدالة ts_vector لكل صف من الجدول.
  3. يتم استدعاء الدالة ts_rank لكل صف من الجدول.

كل هذا يؤدي إلى حقيقة أن البحث يستغرق وقتا طويلا حقا . EXPLAIN النتائج على قاعدة قتالية:


 Gather Merge (actual time=420289.477..420313.969 rows=58742 loops=1) Workers Planned: 2 Workers Launched: 2 -> Sort (actual time=420266.150..420267.935 rows=19581 loops=3) Sort Key: (ts_rank(to_tsvector(document_text), plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 2278kB -> Parallel Seq Scan on documents_document (actual time=65.454..420235.446 rows=19581 loops=3) Filter: (to_tsvector(document_text) @@ plainto_tsquery(''::text)) Rows Removed by Filter: 140636 Planning time: 3.706 ms Execution time: 420315.895 ms 

420 ثانية! لطلب واحد!


تنشئ القاعدة أيضًا الكثير من أشكال [54000] word is too long to be indexed . لا يوجد شيء يدعو للقلق. السبب هو أنه في قاعدة البيانات الخاصة بي هي المستندات التي تم إنشاؤها في محرر WYSIWYG. تدرج الكثير من   كلما كان ذلك ممكنا ، وهناك 54 ألف على التوالي. يتجاهل بوستجرس كلمات بهذا الطول ويكتب نسخة لا يمكن تعطيلها.


سنحاول حل جميع المشكلات المذكورة وتسريع عملية البحث.


نحن بسذاجة تحسين البحث


لن نلعب مع القاعدة القتالية ، بالطبع - سننشئ قاعدة اختبار. يحتوي على حوالي 12 ألف مستند. يتم تنفيذ الطلب من المثال هناك ~ 35 ثانية. طويلة لا يغتفر!


اشرح النتائج
 Sort (actual time=35431.874..35432.208 rows=3593 loops=1) Sort Key: (ts_rank(to_tsvector(document_text), plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 377kB -> Seq Scan on documents_document (actual time=8.470..35429.261 rows=3593 loops=1) Filter: (to_tsvector(document_text) @@ plainto_tsquery(''::text)) Rows Removed by Filter: 9190 Planning time: 0.200 ms Execution time: 35432.294 ms 

الفهرس


بادئ ذي بدء ، بالطبع ، تحتاج إلى إضافة فهرس. أسهل طريقة: مؤشر وظيفي.


 CREATE INDEX idx_gin_document ON documents_document USING gin (to_tsvector('russian', "document_text")); 

سيتم إنشاء هذا الفهرس لفترة طويلة - استغرق الأمر 26 ثانية تقريبًا على قاعدة الاختبار. يحتاج إلى الانتقال إلى قاعدة البيانات واستدعاء دالة to_tsvector لكل سجل. على الرغم من أنه لا يزال يسرع البحث إلى 12 ثانية ، إلا أنه لا يزال طويلًا بشكل لا يغتفر!


اشرح النتائج
 Sort (actual time=12213.943..12214.327 rows=3593 loops=1) Sort Key: (ts_rank(to_tsvector('russian'::regconfig, document_text), plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 377kB -> Bitmap Heap Scan on documents_document (actual time=3.849..12212.248 rows=3593 loops=1) Recheck Cond: (to_tsvector('russian'::regconfig, document_text) @@ plainto_tsquery(''::text)) Heap Blocks: exact=946 -> Bitmap Index Scan on idx_gin_document (actual time=0.427..0.427 rows=3593 loops=1) Index Cond: (to_tsvector('russian'::regconfig, document_text) @@ plainto_tsquery(''::text)) Planning time: 0.109 ms Execution time: 12214.452 ms 

مكالمة متكررة to_tsvector


لحل هذه المشكلة ، تحتاج إلى تخزين tsvector في قاعدة البيانات. عند تغيير البيانات في جدول يحتوي على مستندات ، بالطبع ، تحتاج إلى تحديثها - من خلال المشغلات في قاعدة البيانات ، باستخدام الواجهة الخلفية.


هناك طريقتان للقيام بذلك:


  1. أضف عمودًا من النوع tsvector إلى الجدول مع المستندات.
  2. قم بإنشاء جدول منفصل به اتصال فردي بجدول المستندات ، وقم بتخزين المتجهات هناك.

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


تؤدي كلتا الرحلتين إلى حقيقة أن البيانات الموجودة على القرص تصبح ضعف حجمها: يتم تخزين نصوص المستندات وناقلاتها.


اخترت النهج الثاني بنفسي ، ومزاياه أكثر أهمية بالنسبة لي.


إنشاء الفهرس
 CREATE INDEX idx_gin_document ON documents_documentvector USING gin ("document_text"); 

استعلام بحث جديد
 SELECT documents_document.id, ts_rank("text", plainto_tsquery('')) FROM documents_document LEFT JOIN documents_documentvector ON documents_document.id = documents_documentvector.document_id WHERE "text" @@ plainto_tsquery('') ORDER BY ts_rank("text", plainto_tsquery('')) DESC; 

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


وكم مرة تسارعت عملية البحث؟


 Sort (actual time=48.147..48.432 rows=3593 loops=1) Sort Key: (ts_rank(documents_documentvector.text, plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 377kB -> Hash Join (actual time=2.281..47.389 rows=3593 loops=1) Hash Cond: (documents_document.id = documents_documentvector.document_id) -> Seq Scan on documents_document (actual time=0.003..2.190 rows=12783 loops=1) -> Hash (actual time=2.252..2.252 rows=3593 loops=1) Buckets: 4096 Batches: 1 Memory Usage: 543kB -> Bitmap Heap Scan on documents_documentvector (actual time=0.465..1.641 rows=3593 loops=1) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=577 -> Bitmap Index Scan on idx_gin_document (actual time=0.404..0.404 rows=3593 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 0.410 ms Execution time: 48.573 ms 

المقاييس دون الانضمام

طلب:


 SELECT id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank; 

النتيجة:


  الترتيب (الوقت الفعلي = 44.339..44.487 الصفوف = 3593 حلقة = 1)
   مفتاح التصنيف: (ts_rank (text ، plainto_tsquery ('query' :: text)))
   طريقة الفرز: فرز سريع الذاكرة: 265kB
   -> صورة نقطية كومة المسح الضوئي على documents_documentvector (الوقت الفعلي = 0.692..43.682 الصفوف = 3593 حلقة = 1)
         إعادة فحص Cond: (textplainto_tsquery ('query' :: text))
         كتل الكومة: بالضبط = 577
         -> مسح فهرس الصور النقطية على idx_gin_document (الوقت الفعلي = 0.577..0.577 الصفوف = 3593 حلقة = 1)
               فهرس الفهرس: (textplainto_tsquery ('query' :: text))
 وقت التخطيط: 0.182 مللي ثانية
 مدة التنفيذ: 44.610 مللي ثانية

لا يصدق! وهذا على الرغم من الانضمام و ts_rank . بالفعل نتيجة مقبولة تمامًا ، يتم ts_rank معظم الوقت ليس ts_rank طريق البحث ، ولكن عن طريق حساب ts_rank لكل سطر من الأسطر.


ts_rank دعوة متعددة


يبدو أننا نجحنا في حل جميع مشكلاتنا ، باستثناء هذه المشكلة. 44 ميلي ثانية هي مهلة لائقة. نهاية سعيدة تبدو قريبة؟ كان هناك!


قم بتشغيل نفس الاستعلام دون ts_rank وقارن النتائج.


بدون ts_rank

طلب:


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

النتيجة:


 Bitmap Heap Scan on documents_documentvector (actual time=0.503..1.609 rows=3593 loops=1) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=577 -> Bitmap Index Scan on idx_gin_document (actual time=0.439..0.439 rows=3593 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 0.147 ms Execution time: 1.715 ms 

1.7 مللي ثانية! ثلاثين مرة أسرع! بالنسبة لقاعدة قتالية ، تكون النتائج ~ 150 مللي ثانية و 1.5 ثانية. الفرق في أي حال هو ترتيب من حيث الحجم ، و 1.5 ثانية ليست هي الوقت الذي تريد انتظار إجابة من القاعدة. ماذا تفعل؟


لا يمكنك إيقاف الفرز حسب الصلة ؛ لا يمكنك تقليل عدد أسطر الحساب (يجب أن تحسب قاعدة البيانات ts_rank لجميع المستندات ts_rank ، وإلا لا يمكن فرزها).


في بعض الأماكن على الإنترنت ، يوصى بتخزين أكثر الطلبات تكرارًا (وبالتالي ، اتصل بـ ts_rank). لكنني لا أحب هذا الأسلوب: من الصعب تحديد الاستعلامات الصحيحة بشكل صحيح ، وسيظل البحث بطيئًا في الاستعلامات الخاطئة.


أرغب بشدة في ذلك بعد الاطلاع على الفهرس ، جاءت البيانات في شكل تم فرزه بالفعل ، كما يفعل Sphinx. لسوء الحظ ، لا يمكن فعل أي شيء من المربع في PostgreSQL.


لكننا كنا محظوظين - يمكن لمؤشر RUM القيام بذلك. يمكن العثور على تفاصيل حوله ، على سبيل المثال ، في عرض مؤلفيه . إنه يخزن معلومات إضافية حول الطلب ، والذي يسمح لك بتقييم ما يسمى مباشرة. "المسافة" بين tsvector و tsquery نتيجة فرزها مباشرة بعد مسح الفهرس.


ولكن رمي GIN وتثبيت RUM لا يستحق كل هذا العناء على الفور. يحتوي على السلبيات والإيجابيات وحدود التطبيق - سأكتب عن ذلك في المقالة التالية.

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


All Articles