前の記事で、標準ツールを使用して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('')
を使用したクエリを使用して取得されることです。 このようなクエリは、検索ベクトルと検索クエリの間の距離を返します。 小さければ小さいほど、クエリはベクトルとよく一致します。
GINとの比較
ここでは、検索結果の違いに気付くために、テストベースで50万文書までを比較します。
リクエスト速度
このベースでGINのEXPLAINが生成するものを見てみましょう。
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
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
これは何? この自慢のRUMは、GINの3倍の速度で動作する場合、どのように使用しますか? そして、インデックス内の悪名高いソートはどこにありますか?
穏やか:リクエストにLIMIT 1000
を追加してみましょう。
制限(実際の時間= 115.568..137.313行= 1000ループ= 1) -> documents_documentvectorでidx_rum_documentを使用したインデックススキャン(実際の時間= 115.567..137.239行= 1000ループ= 1) インデックス条件:(text @@ plainto_tsquery( 'query' :: text)) 順序:(text <=> plainto_tsquery( 'query' :: text)) 計画時間:0.481ミリ秒 実行時間:137.678ミリ秒
制限(実際の時間= 579.905..585.650行= 1000ループ= 1) -> Gather Merge(実際の時間= 579.904..585.604行= 1000ループ= 1) 予定労働者:2 立ち上げられた労働者:2 ->ソート(実際の時間= 574.061..574.171行= 992ループ= 3) ソートキー:(ts_rank(text、plainto_tsquery( 'query' :: text)))DESC ソート方法:外部マージディスク:1224kB -> documents_documentvectorでの並列ビットマップヒープスキャン(実際の時間= 8.920..555.571行= 39851ループ= 3) Condの再確認:(text @@ plainto_tsquery( 'query' :: text)) ヒープブロック:正確な= 5422 -> idx_gin_documentのビットマップインデックススキャン(実際の時間= 8.945..8.945行= 119553ループ= 1) インデックス条件:(text @@ plainto_tsquery( 'query' :: text)) 計画時間:0.223ミリ秒 実行時間:585.948ミリ秒
〜150 ms vs〜600 ms! すでにGINを支持していませんよね? そして、ソートはインデックス内に移動しました!
そして、もしあなたがLIMIT 100
を探したら?
制限(実際の時間= 105.863..108.530行= 100ループ= 1) -> documents_documentvectorでidx_rum_documentを使用したインデックススキャン(実際の時間= 105.862..108.517行= 100ループ= 1) インデックス条件:(text @@ plainto_tsquery( 'query' :: text)) 順序:(text <=> plainto_tsquery( 'query' :: text)) 計画時間:0.199ミリ秒 実行時間:108.958ミリ秒
制限(実際の時間= 582.924..588.351行= 100ループ= 1) -> Gather Merge(実際の時間= 582.923..588.344行= 100ループ= 1) 予定労働者:2 立ち上げられた労働者:2 ->ソート(実際の時間= 573.809..573.889行= 806ループ= 3) ソートキー:(ts_rank(text、plainto_tsquery( 'query' :: text)))DESC ソート方法:外部マージディスク:1224kB -> documents_documentvectorでの並列ビットマップヒープスキャン(実際の時間= 18.038..552.827行= 39851ループ= 3) Condの再確認:(text @@ plainto_tsquery( 'query' :: text)) ヒープブロック:正確な= 5275 -> idx_gin_documentのビットマップインデックススキャン(実際の時間= 16.541..16.541行= 119553ループ= 1) インデックス条件:(text @@ plainto_tsquery( 'query' :: text)) 計画時間:0.487ミリ秒 実行時間:588.583ミリ秒
違いはさらに顕著です。
問題は、GINが最終的に何行になるかは問題ではないということです。GINは、要求が成功したすべての行を調べて、それらをランク付けする必要があります。 RUMは、本当に必要な行に対してのみこれを行います。 多くの行が必要な場合は、GINが勝ちます。 ts_rank
は、 <=>
演算子よりも効率的に計算を実行します。 しかし、小さなクエリでは、RUMの利点は否定できません。
ほとんどの場合、ユーザーは一度に5万件のドキュメントすべてをデータベースからアンロードする必要はありません。 彼は、最初のページ、2番目のページ、3番目のページなどに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) -> Gather Merge(実際の時間= 1637.901..1643.479行= 10ループ= 1) 予定労働者:2 立ち上げられた労働者:2 ->ソート(実際の時間= 1070.614..1070.687行= 652ループ= 3) ソートキー:(ts_rank(documents_documentvector.text、plainto_tsquery( 'query' :: text)))DESC ソート方法:外部マージディスク:2968kB ->ハッシュ左結合(実際の時間= 323.386..1049.092行= 39851ループ= 3) ハッシュ条件:(documents_document.case_id = documents_case.id) ->ハッシュ結合(実際の時間= 239.312..324.797行= 39851ループ= 3) ハッシュ条件:(documents_documentvector.document_id = documents_document.id) -> documents_documentvectorでの並列ビットマップヒープスキャン(実際の時間= 11.022..37.073行= 39851ループ= 3) Condの再確認:(text @@ plainto_tsquery( 'query' :: text)) ヒープブロック:正確な= 9362 -> idx_gin_documentのビットマップインデックススキャン(実際の時間= 12.094..12.094行= 119553ループ= 1) インデックス条件:(text @@ plainto_tsquery( 'query' :: text)) ->ハッシュ(実際の時間= 227.856..227.856行= 472089ループ= 3) バケット:65536バッチ:16メモリ使用量:2264kB -> documents_documentのSeqスキャン(実際の時間= 0.009..147.104行= 472089ループ= 3) ->ハッシュ(実際の時間= 83.338..83.338行= 273695ループ= 3) バケット:65536バッチ:8メモリ使用量:2602kB -> documents_caseのSeqスキャン(実際の時間= 0.009..39.082行= 273695ループ= 3) 計画時間:0.857 ms 実行時間:1644.028 ms
3つ以上の結合では、要求時間は2〜3秒に達し、結合の数とともに増加します。
そして、RUMはどうですか? すぐに5つの結合を使用してリクエストを行います。
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) ->ネストされたループ左結合(実際の時間= 70.521..72.279行= 10ループ= 1) ->ネストされたループ左結合(実際の時間= 70.104..70.406行= 10ループ= 1) ->ネストされたループの左結合(実際の時間= 70.089..70.351行= 10ループ= 1) ->ネストされたループ左結合(実際の時間= 70.073..70.302行= 10ループ= 1) ->ネストされたループ(実際の時間= 70.052..70.201行= 10ループ= 1) -> documents_documentvectorでdocument_vector_rum_indexを使用したインデックススキャン(実際の時間= 70.001..70.035行= 10ループ= 1) インデックス条件:(text @@ plainto_tsquery( 'query' :: text)) 順序:(text <=> plainto_tsquery( 'query' :: text)) -> documents_documentでdocuments_document_pkeyを使用したインデックススキャン(実際の時間= 0.013..0.013行= 1ループ= 10) インデックス条件:(id = documents_documentvector.document_id) フィルター:(is_active IS TRUE) -> documents_caseでdocuments_case_pkeyを使用したインデックススキャン(実際の時間= 0.009..0.009行= 1ループ= 10) インデックス条件:(documents_document.case_id = id) -> classifier_procedureでclassifier_procedure_pkeyを使用したインデックススキャン(実際の時間= 0.003..0.003行= 1ループ= 10) インデックス条件:(documents_case.procedure_id = id) -> classifier_divisionでclassifier_division_pkeyを使用したインデックススキャン(実際の時間= 0.004..0.004行= 1ループ= 10) インデックス条件:(documents_case.division_id = id) -> classifier_categoryでclassifier_category_pkeyを使用したインデックススキャン(実際の時間= 0.003..0.003行= 1ループ= 10) インデックス条件:(documents_document.category_id = id) 計画時間:2.861ミリ秒 実行時間:72.865ミリ秒
検索時に結合なしで実行できない場合は、RUMが明らかに適しています。
ディスク容量
最大50万のドキュメントと3.6 GBのインデックスのテストベースでは、 非常に異なるボリュームを占有していました。
idx_rum_document | 1950 MB idx_gin_document | 418 MB
はい、ドライブは安いです。 ただし、400 MBではなく2 GBを使用することはできません。 インデックスの場合、ベースのサイズの半分が少し大きくなります。 ここでGINが無条件で勝ちます。
結論
次の場合は、RUMが必要です。
- 多くのドキュメントがありますが、ページごとに検索結果を提供します
- 検索結果の複雑なフィルタリングが必要です
- あなたはディスクスペースを気にしません
次の場合、GINに完全に満足します。
- あなたは小さなベースを持っています
- あなたは大きな基盤を持っていますが、すぐに結果を出す必要があり、それだけです
- joinでフィルタリングする必要はありません
- ディスク上の最小インデックスサイズに興味がありますか
この記事が多くのWTFを削除することを願っています!!これは、Postgresで検索を設定して設定するときに起こります。 すべてをさらに良く設定する方法を知っている人からアドバイスをうれしく思います!)
次のパートでは、プロジェクトでRUMについて詳しく説明する予定です。追加のRUMオプションの使用、Django + PostgreSQLバンドルでの作業について。