Postgresで全文検索を準備しています。 パート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;
      
      





GINのリクエスト
 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



を追加してみましょう。







RUMの説明
 制限(実際の時間= 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



を探したら?







RUMの説明
 制限(実際の時間= 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件の投稿しか必要としません そして、まさにこのような場合にこのインデックスがシャープ化され、大規模なベースでの検索パフォーマンスが大幅に向上します。







公差に参加する



検索で別のテーブルを結合する必要がある場合はどうなりますか? たとえば、結果にドキュメントの種類、その所有者を表示するには? または、私の場合のように、関連するエンティティの名前でフィルタリングしますか?







比較する:







GINの2つの参加を伴うリクエスト
 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つの結合を使用してリクエストを行います。







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に完全に満足します。









この記事が多くのWTFを削除することを願っています!!これは、Postgresで検索を設定して設定するときに起こります。 すべてをさらに良く設定する方法を知っている人からアドバイスをうれしく思います!)







次のパートでは、プロジェクトでRUMについて詳しく説明する予定です。追加のRUMオプションの使用、Django + PostgreSQLバンドルでの作業について。








All Articles