UPD Bagian 2
Artikel ini adalah yang pertama dari serangkaian kecil artikel tentang cara mengkonfigurasi pencarian teks lengkap secara optimal di PostgreSQL. Saya baru-baru ini harus menyelesaikan masalah yang sama di tempat kerja - dan saya sangat terkejut dengan tidak adanya setidaknya beberapa bahan waras tentang hal ini. Pengalaman saya berjuang di bawah luka.
Dasi
Saya mendukung proyek yang relatif besar yang memiliki pencarian publik pada dokumen. Basis data berisi ~ 500 ribu dokumen dengan total volume ~ 3,6 GB. Inti dari pencarian adalah ini: pengguna mengisi formulir di mana ada permintaan teks lengkap dan pemfilteran oleh berbagai bidang dalam database, termasuk dengan join-s.
Pekerjaan pencarian (atau lebih tepatnya, bekerja) melalui Sphinx, dan tidak bekerja dengan baik. Masalah utama adalah sebagai berikut:
- Pengindeksan mengkonsumsi sekitar 8 GB RAM. Pada server dengan 8 GB RAM, ini merupakan masalah. Memori bertukar, itu menyebabkan kinerja yang mengerikan .
- Indeks dibangun sekitar 40 menit. Tidak ada pertanyaan tentang konsistensi hasil pencarian, pengindeksan diluncurkan sekali sehari.
- Pencarian berhasil untuk waktu yang lama . Permintaan untuk waktu yang sangat lama dilakukan, yang berhubungan dengan sejumlah besar dokumen: sejumlah besar id-shnik harus ditransfer dari sphinx ke database, dan diurutkan berdasarkan relevansi pada backend.
Karena masalah ini, tugas muncul - untuk mengoptimalkan pencarian teks lengkap. Tugas ini memiliki dua solusi:
- Kencangkan Sphinx: konfigurasikan indeks waktu nyata, simpan atribut untuk penyaringan dalam indeks.
- Gunakan FTS PostgreSQL bawaan.
Diputuskan untuk mengimplementasikan solusi kedua: dengan cara ini Anda dapat secara otomatis menyediakan pembaruan indeks secara otomatis, menyingkirkan komunikasi panjang antara dua layanan dan memantau satu layanan, bukan dua.
Tampaknya menjadi solusi yang bagus. Tapi masalah ada di depan.
Mari kita mulai dari awal.
Kami secara naif menggunakan pencarian teks lengkap
Seperti yang dikatakan dalam dokumentasi, pencarian teks lengkap membutuhkan penggunaan tipe tsvector
dan tsquery
. Yang pertama menyimpan teks dokumen dalam bentuk pencarian yang dioptimalkan, yang kedua menyimpan kueri teks lengkap.
Untuk mencari PostgreSQL, ada fungsi to_tsvector
, plainto_tsquery
, to_tsquery
. Untuk memberi peringkat hasil ada ts_rank
. Penggunaannya intuitif dan dijelaskan dengan baik dalam dokumentasi , jadi kami tidak akan membahas detail penggunaannya.
Kueri pencarian tradisional yang menggunakan mereka akan terlihat seperti ini:
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;
Kami menyimpulkan id dokumen dalam teks yang ada kata "kueri", dan mengurutkannya dalam urutan relevansi yang menurun. Segalanya tampak baik-baik saja? Tidak.
Pendekatan di atas memiliki banyak kelemahan:
- Kami tidak menggunakan indeks untuk pencarian.
- Fungsi ts_vector dipanggil untuk setiap baris tabel.
- Fungsi ts_rank dipanggil untuk setiap baris tabel.
Ini semua mengarah pada fakta bahwa pencarian membutuhkan waktu yang sangat lama. EXPLAIN
hasil di pangkalan tempur:
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 detik! Untuk satu permintaan!
Basis ini juga menghasilkan banyak vorings dari bentuk [54000] word is too long to be indexed
. Tidak ada yang perlu dikhawatirkan. Alasannya adalah bahwa dalam database saya adalah dokumen yang dibuat di editor WYSIWYG. Ini menyisipkan banyak
sedapat mungkin, dan ada 54 ribu berturut-turut. Postgres mengabaikan kata-kata panjang ini dan menulis vorning yang tidak dapat dinonaktifkan.
Kami akan mencoba memperbaiki semua masalah yang dicatat dan mempercepat pencarian.
Kami secara naif mengoptimalkan pencarian
Kami tidak akan bermain dengan pangkalan tempur, tentu saja - kami akan membuat basis tes. Ini berisi ~ 12 ribu dokumen. Permintaan dari contoh dijalankan di sana ~ 35 detik. Sangat panjang!
MENJELASKAN Hasil 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
Indeks
Pertama-tama, tentu saja, Anda perlu menambahkan indeks. Cara termudah: indeks fungsional.
CREATE INDEX idx_gin_document ON documents_document USING gin (to_tsvector('russian', "document_text"));
Indeks semacam itu akan dibuat untuk waktu yang lama - butuh ~ 26 detik pada basis tes. Dia harus melalui database dan memanggil fungsi to_tsvector untuk setiap record. Meskipun masih mempercepat pencarian hingga 12 detik, itu masih sangat panjang!
MENJELASKAN Hasil 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
Panggilan to_tsvector
berulang
Untuk mengatasi masalah ini, Anda perlu menyimpan tsvector
dalam database. Saat mengubah data dalam tabel dengan dokumen, tentu saja, Anda perlu memperbaruinya - melalui pemicu dalam database, menggunakan backend.
Ada dua cara untuk melakukan ini:
- Tambahkan kolom tipe
tsvector
ke tabel dengan dokumen. - Buat tabel terpisah dengan komunikasi satu-ke-satu dengan tabel dokumen, dan simpan vektor di sana.
Keuntungan dari pendekatan pertama: kurangnya bergabung dalam pencarian.
Keuntungan dari pendekatan kedua: kurangnya data tambahan dalam tabel dengan dokumen, ukurannya tetap sama seperti sebelumnya. Dengan cadangan, Anda tidak perlu membuang waktu dan memakai tsvector
, yang tidak perlu Anda tsvector
sama sekali.
Kedua perjalanan menyebabkan fakta bahwa data pada disk menjadi dua kali lipat: teks dokumen dan vektornya disimpan.
Saya memilih pendekatan kedua untuk diri saya sendiri, keuntungannya lebih signifikan bagi saya.
Pembuatan indeks CREATE INDEX idx_gin_document ON documents_documentvector USING gin ("document_text");
Permintaan pencarian baru 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;
Tambahkan data ke tabel yang ditautkan dan buat indeks. Menambahkan data membutuhkan waktu 24 detik berdasarkan pengujian, dan membuat indeks hanya membutuhkan 2,7 detik . Memperbarui indeks dan data, seperti yang kita lihat, tidak mempercepat secara signifikan, tetapi indeks itu sendiri sekarang dapat diperbarui dengan sangat cepat.
Dan berapa kali pencarian dipercepat?
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
Metrik tanpa bergabungMinta:
SELECT id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank;
Hasil:
Sortir (waktu aktual = 44.339..44.487 baris = 3593 putaran = 1)
Sortir Kunci: (ts_rank (teks, plainto_tsquery ('query' :: text)))
Metode Sortir: memori cepat: 265kB
-> Bitmap Heap Scan pada document_documentvector (waktu aktual = 0.692..43.682 baris = 3593 loop = 1)
Periksa kembali Cond: (text @@ plainto_tsquery ('query' :: text))
Heap Blocks: tepat = 577
-> Pemindaian Indeks Bitmap pada idx_gin_document (waktu aktual = 0,577..0.577 baris = 3593 loop = 1)
Indeks Cond: (teks @@ plainto_tsquery ('query' :: text))
Waktu perencanaan: 0,182 ms
Waktu pelaksanaan: 44.610 ms
Luar biasa! Dan ini meskipun bergabung dan ts_rank
. Sudah hasil yang cukup dapat diterima, sebagian besar waktu akan ts_rank
bukan oleh pencarian, tetapi dengan perhitungan ts_rank
untuk setiap baris.
ts_rank
beberapa panggilan
Tampaknya kami telah berhasil menyelesaikan semua masalah kami, kecuali yang ini. 44 milidetik adalah waktu yang layak. Happy ending sepertinya dekat? Itu dia!
Jalankan kueri yang sama tanpa ts_rank
dan bandingkan hasilnya.
Tanpa ts_rankMinta:
SELECT document_id, 1 AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank;
Hasil:
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 md! Tiga puluh kali lebih cepat! Untuk pangkalan tempur, hasilnya ~ 150 ms dan 1,5 detik. Perbedaan dalam hal apa pun adalah urutan besarnya, dan 1,5 detik bukanlah waktu yang Anda inginkan untuk menunggu jawaban dari pangkalan. Apa yang harus dilakukan
Anda tidak dapat mematikan penyortiran berdasarkan relevansi; Anda tidak dapat mengurangi jumlah baris untuk penghitungan (database harus menghitung ts_rank
untuk semua dokumen yang ts_rank
, jika tidak mereka tidak dapat disortir).
Di beberapa tempat di Internet, disarankan untuk melakukan cache permintaan yang paling sering (dan, karenanya, hubungi ts_rank). Tapi saya tidak suka pendekatan ini: cukup sulit untuk memilih kueri yang benar, dan pencarian akan tetap melambat pada kueri yang salah.
Saya akan sangat menyukai itu setelah melalui indeks data datang dalam bentuk yang sudah diurutkan, seperti yang dilakukan Sphinx. Sayangnya, tidak ada yang dapat dilakukan dari kotak di PostgreSQL.
Tapi kami beruntung - indeks RUM dapat melakukan ini. Rincian tentang hal itu dapat ditemukan, misalnya, dalam presentasi penulisnya . Ini menyimpan informasi tambahan tentang permintaan, yang memungkinkan Anda untuk secara langsung mengevaluasi apa yang disebut. "jarak" antara tsvector
dan tsquery
dan menghasilkan hasil yang diurutkan segera setelah memindai indeks.
Tetapi melempar GIN dan menginstal RUM tidak segera berarti. Ini memiliki minus, plus dan batas aplikasi - saya akan menulis tentang ini di artikel berikutnya.