Kami sedang mempersiapkan pencarian teks lengkap di Postgres. Bagian 2

Pada artikel terakhir, kami mengoptimalkan pencarian di PostgreSQL menggunakan alat standar. Dalam artikel ini, kami akan terus mengoptimalkan menggunakan indeks RUM dan menganalisis pro dan kontra dibandingkan dengan GIN.


Pendahuluan


RUM adalah ekstensi untuk Postgres, indeks baru untuk pencarian teks lengkap. Ini memungkinkan Anda untuk mengembalikan hasil yang diurutkan berdasarkan relevansi saat melewati indeks. Saya tidak akan fokus pada instalasinya - ini dijelaskan dalam README di repositori.


Kami menggunakan indeks


Indeks dibuat mirip dengan indeks GIN, tetapi dengan beberapa parameter. Seluruh daftar parameter dapat ditemukan dalam dokumentasi.


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

Permintaan pencarian untuk RUM:


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

Minta GIN
 SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC; 

Perbedaan dari GIN adalah relevansi yang diperoleh tidak menggunakan fungsi ts_rank, tetapi menggunakan kueri dengan operator <=> : "text" <=> plainto_tsquery('') . Permintaan semacam itu mengembalikan jarak antara vektor pencarian dan permintaan pencarian. Semakin kecil, semakin baik kueri yang cocok dengan vektor.


Perbandingan dengan GIN


Di sini kita akan membandingkan berdasarkan pengujian dengan ~ 500 ribu dokumen untuk melihat perbedaan dalam hasil pencarian.


Kecepatan Permintaan


Mari kita lihat apa yang EXPLAIN untuk GIN akan hasilkan di pangkalan ini:


 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 

Dan untuk RUM?


 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 

Apa ini Apa gunanya RUM yang dipamerkan ini, Anda bertanya, apakah ini berjalan tiga kali lebih lambat dari GIN? Dan di mana penyortiran terkenal di dalam indeks?


Tenang: mari kita coba tambahkan LIMIT 1000 ke permintaan.


JELASKAN untuk RUM
  Batas (waktu aktual = 115.568..137.313 baris = 1000 putaran = 1)
    -> Pemindaian Indeks menggunakan idx_rum_document pada document_documentvector (waktu aktual = 115.567..137.239 baris = 1000 loop = 1)
          Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
          Pesan Menurut: (teks <=> plainto_tsquery ('kueri' :: teks))
  Waktu perencanaan: 0,481 ms
  Waktu pelaksanaan: 137.678 ms 

JELASKAN untuk GIN
  Batas (waktu aktual = 579.905..585.650 baris = 1000 putaran = 1)
    -> Kumpulkan Gabung (waktu aktual = 579.904..585.604 baris = 1000 loop = 1)
          Pekerja yang Direncanakan: 2
          Peluncuran Pekerja: 2
          -> Sortir (waktu aktual = 574.061..574.171 baris = 992 putaran = 3)
                Sortir Kunci: (ts_rank (teks, plainto_tsquery ('query' :: text))) DESC
                Metode Sortir: disk gabungan eksternal: 1224kB
                -> Paralel Bitmap Heap Scan pada document_documentvector (waktu aktual = 8.920..555.571 baris = 39851 loop = 3)
                      Periksa kembali Cond: (text @@ plainto_tsquery ('query' :: text))
                      Heap Blocks: tepat = 5422
                      -> Pemindaian Indeks Bitmap pada idx_gin_document (waktu aktual = 8,945..8,945 baris = 119553 loop = 1)
                            Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
  Waktu perencanaan: 0,223 ms
  Waktu pelaksanaan: 585.948 ms 

~ 150 ms vs ~ 600 ms! Sudah tidak mendukung GIN, kan? Dan penyortiran telah bergerak di dalam indeks!


Dan jika Anda mencari LIMIT 100 ?


JELASKAN untuk RUM
  Batas (waktu aktual = 105.863..108.530 baris = 100 putaran = 1)
    -> Pemindaian Indeks menggunakan idx_rum_document pada document_documentvector (waktu aktual = 105.862..108.517 baris = 100 loop = 1)
          Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
          Pesan Menurut: (teks <=> plainto_tsquery ('kueri' :: teks))
  Waktu perencanaan: 0,199 ms
  Waktu pelaksanaan: 108.958 ms 

JELASKAN untuk GIN
  Batas (waktu aktual = 582.924..588.351 baris = 100 putaran = 1)
    -> Kumpulkan Gabung (waktu aktual = 582.923..588.344 baris = 100 putaran = 1)
          Pekerja yang Direncanakan: 2
          Peluncuran Pekerja: 2
          -> Sortir (waktu aktual = 573.809..573.889 baris = 806 loop = 3)
                Sortir Kunci: (ts_rank (teks, plainto_tsquery ('query' :: text))) DESC
                Metode Sortir: disk gabungan eksternal: 1224kB
                -> Paralel Bitmap Heap Scan pada document_documentvector (waktu aktual = 18.038..552.827 baris = 39851 loop = 3)
                      Periksa kembali Cond: (text @@ plainto_tsquery ('query' :: text))
                      Heap Blocks: tepat = 5275
                      -> Pemindaian Indeks Bitmap pada idx_gin_document (waktu aktual = 16.541..16.541 baris = 11.953 loop = 1)
                            Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
  Waktu perencanaan: 0,487 ms
  Waktu pelaksanaan: 588.583 ms 

Perbedaannya bahkan lebih terlihat.


Masalahnya adalah bahwa GIN tidak masalah berapa banyak baris yang Anda dapatkan pada akhirnya - itu harus melewati semua baris yang permintaannya berhasil, dan memberi peringkat mereka. RUM melakukan ini hanya untuk baris yang benar-benar kita butuhkan. Jika kita membutuhkan banyak baris, GIN menang. ts_rank melakukan perhitungan ts_rank efisien daripada operator <=> . Namun pada pertanyaan kecil, keunggulan RUM tidak dapat disangkal.


Paling sering, pengguna tidak perlu membongkar semua 50 ribu dokumen dari database sekaligus. Ia hanya membutuhkan 10 posting di halaman pertama, kedua, ketiga, dll. Dan justru dalam kasus seperti itu indeks ini dipertajam, dan itu akan memberikan peningkatan kinerja pencarian yang baik pada basis yang besar.


Bergabunglah Toleransi


Bagaimana jika suatu pencarian mengharuskan Anda untuk bergabung dengan tabel lain atau lebih? Misalnya, untuk menampilkan dalam hasil jenis dokumen, pemiliknya? Atau, seperti dalam kasus saya, filter dengan nama entitas terkait?


Bandingkan:


Minta dengan dua orang bergabung untuk GIN
 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; 

Hasil:


 Batas (waktu aktual = 1637.902..1643.483 baris = 10 putaran = 1)
    -> Kumpulkan Gabung (waktu aktual = 1637.901..1643.479 baris = 10 putaran = 1)
          Pekerja yang Direncanakan: 2
          Peluncuran Pekerja: 2
          -> Sortir (waktu aktual = 1070.614..1070.687 baris = 652 putaran = 3)
                Sortir Kunci: (ts_rank (document_documentvector.text, plainto_tsquery ('query' :: text))) DESC
                Metode Sortir: disk gabungan eksternal: 2968kB
                -> Hash Left Join (waktu aktual = 323.386..1049.092 baris = 39851 loop = 3)
                      Cond Hash: (document_document.case_id = dokumen_case.id)
                      -> Hash Join (waktu aktual = 239.312..324.797 baris = 39851 loop = 3)
                            Hash Cond: (document_documentvector.document_id = document_document.id)
                            -> Parallel Bitmap Heap Scan pada document_documentvector (waktu aktual = 11.022..37.073 baris = 39851 loop = 3)
                                  Periksa kembali Cond: (text @@ plainto_tsquery ('query' :: text))
                                  Heap Blocks: tepat = 9362
                                  -> Pemindaian Indeks Bitmap pada idx_gin_document (waktu aktual = 12.094..12.094 baris = 119553 loop = 1)
                                        Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
                            -> Hash (waktu aktual = 227.856..227.856 baris = 472089 loop = 3)
                                  Bucket: 65536 Batch: 16 Penggunaan Memori: 2264kB
                                  -> Pemindaian Seq pada dokumen document_d (waktu aktual = 0,009..147,104 baris = 472089 loop = 3)
                      -> Hash (waktu aktual = 83.338..83.338 baris = 273695 loop = 3)
                            Bucket: 65536 Batch: 8 Penggunaan Memori: 2602kB
                            -> Pemindaian Seq pada document_case (waktu aktual = 0,009..39.082 baris = 273695 loop = 3)
 Waktu perencanaan: 0,857 ms
 Waktu pelaksanaan: 1644.028 ms

Pada tiga bergabung dan lebih, waktu permintaan mencapai 2-3 detik dan bertambah dengan jumlah bergabung.


Tapi bagaimana dengan RUM? Biarkan permintaan segera dengan lima bergabung.


Lima permintaan bergabung untuk RUM
 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; 

Hasil:


  Batas (waktu aktual = 70.524..72.292 baris = 10 putaran = 1)
   -> Nested Loop Left Join (waktu aktual = 70.521..72.279 baris = 10 loop = 1)
         -> Nested Loop Left Join (waktu aktual = 70.104..70.406 baris = 10 loop = 1)
               -> Nested Loop Left Join (waktu aktual = 70.089..70.351 baris = 10 loop = 1)
                     -> Nested Loop Left Join (waktu aktual = 70.073..70.302 baris = 10 loop = 1)
                           -> Nested Loop (waktu aktual = 70.052..70.201 baris = 10 loop = 1)
                                 -> Pemindaian Indeks menggunakan document_vector_rum_index pada document_documentvector (waktu aktual = 70.001..70.035 baris = 10 loop = 1)
                                       Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
                                       Pesan Menurut: (teks <=> plainto_tsquery ('kueri' :: teks))
                                 -> Pemindaian Indeks menggunakan document_document_pkey pada dokumen_dokumen (waktu aktual = 0,013..0.013 baris = 1 loop = 10)
                                       Indeks Cond: (id = document_documentvector.document_id)
                                       Filter: (is_active BENAR)
                           -> Pemindaian Indeks menggunakan document_case_pkey pada dokumen_case (waktu aktual = 0,009..0.009 baris = 1 loop = 10)
                                 Indeks Cond: (document_document.case_id = id)
                     -> Pemindaian Indeks menggunakan classifier_procedure_pkey pada classifier_procedure (waktu aktual = 0,003..0.003 baris = 1 loop = 10)
                           Indeks Cond: (document_case.procedure_id = id)
               -> Pemindaian Indeks menggunakan classifier_division_pkey pada classifier_division (waktu aktual = 0,004..0.004 baris = 1 loop = 10)
                     Indeks Cond: (document_case.division_id = id)
         -> Pemindaian Indeks menggunakan classifier_category_pkey pada classifier_category (waktu aktual = 0,003..0.003 baris = 1 loop = 10)
               Indeks Cond: (document_document.category_id = id)
 Waktu perencanaan: 2,861 ms
 Waktu pelaksanaan: 72.865 ms

Jika Anda tidak dapat melakukannya tanpa bergabung saat mencari, maka RUM jelas cocok untuk Anda.


Ruang disk


Pada basis pengujian ~ 500 ribu dokumen dan indeks 3,6 GB menempati volume yang sangat berbeda.


  idx_rum_document |  1950 MB
  idx_gin_document |  418 MB

Ya, drive itu murah. Tapi 2 GB bukannya 400 MB tidak bisa tolong. Setengah ukuran basis agak banyak untuk indeks. Di sini GIN menang tanpa syarat.


Kesimpulan


Anda memerlukan RUM jika:


  • Anda memiliki banyak dokumen, tetapi Anda memberikan halaman hasil pencarian demi halaman
  • Anda membutuhkan pemfilteran hasil pencarian yang canggih
  • Anda tidak keberatan dengan ruang disk

Anda akan sepenuhnya puas dengan GIN jika:


  • Anda memiliki basis kecil
  • Anda memiliki basis yang besar, tetapi Anda harus segera memberikan hasil dan hanya itu
  • Anda tidak perlu memfilter dengan bergabung
  • Apakah Anda tertarik dengan ukuran indeks minimum pada disk

Saya harap artikel ini akan menghapus banyak WTF ?! Itu terjadi ketika bekerja dan mengatur pencarian di Postgres. Saya akan senang mendengar saran dari mereka yang tahu cara mengkonfigurasi semuanya lebih baik!)


Pada bagian selanjutnya saya berencana untuk memberi tahu lebih banyak tentang RUM dalam proyek saya: tentang menggunakan opsi RUM tambahan, bekerja di bundel Django + PostgreSQL.

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


All Articles