web-dev-qa-db-ja.com

PostgreSQL FTSおよびTrigram-similarity Query Optimization

最近PostgreSQLの作業を開始し、適用したい12M行を処理したい全文検索。私はそのようなデータベースを扱う経験がありません。クエリを最適化しようとしましたが、完全に最適化されているとは思いません。

現在私は Gist Index を使用しています。更新は GIN Index の方が遅く、データベースは定期的に更新されるということです。

今、データベースの2つの列のみに焦点を当てる必要がありますmerchant varchar(80)およびproduct varchar(400)

FTSを使用して商品を見つける必要があります。また、販売者のスペルが間違っている場合でも商品を入手しようとしています。

次の結果を得るために、約30K行のサンプルデータベースに対していくつかのクエリを実行しました。

  • 最初に、基本的なFTSクエリを実行して結果を分析しました。

    explain analyze
    select count(*) from products
    where to_tsvector('english', product) @@ to_tsquery('hat');
    
Aggregate  (cost=2027.27..2027.28 rows=1 width=0) (actual time=349.032..349.032 rows=1 loops=1)  
->  Seq Scan on products  (cost=0.00..2026.90 rows=147 width=0) (actual time=43.322..348.961 rows=307 loops=1)
 Filter: (to_tsvector((product)::text) @@ to_tsquery('hat'::text))
Total runtime: 349.140 ms
  • 次に、Gistインデックスを作成し、同じクエリを実行して改善を確認しました。結果は非常に良好でした。少なくとも私にとっては。

    create index product_Gist on products using Gist(to_tsvector('english', product));
    
Aggregate  (cost=447.17..447.18 rows=1 width=0) (actual time=12.911..12.911 rows=1 loops=1)
->  Bitmap Heap Scan on products  (cost=9.40..446.80 rows=147 width=0) (actual time=2.256..12.776 rows=307 loops=1)
 Recheck Cond: (to_tsvector('english'::regconfig, (product)::text) @@ to_tsquery('hat'::text))
 ->  Bitmap Index Scan on pn  (cost=0.00..9.37 rows=147 width=0) (actual time=2.111..2.111 rows=307 loops=1)
       Index Cond: (to_tsvector('english'::regconfig, (product)::text) @@ to_tsquery('hat'::text))
Total runtime: 13.051 ms

GINインデックスもテストしましたが、その結果は驚くべきものでした。 Total Runtime: 0.583msしかし、GINインデックスは使用できないため、Gistインデックスに戻りましょう。

  • 今、私は pg_trgm モジュールを使用して、2つの単語間の類似性を見つけています(スペルミスのある商人に使用しています)。

    create index merchant_trgm on products using Gist(merchant Gist_trgm_ops);
    
    select count(*) from products
    where to_tsvector('english', product) @@ to_tsquery('hat')
    AND   similarity(merchant,'fashion') > 0.2;
    
Aggregate  (cost=447.64..447.65 rows=1 width=0) (actual time=14.644..14.645 rows=1 loops=1)
->  Bitmap Heap Scan on products  (cost=9.38..447.51 rows=49 width=0) (actual time=2.187..14.635 rows=12 loops=1)
 Recheck Cond: (to_tsvector('english'::regconfig, (product)::text) @@ to_tsquery('hat'::text))
 Filter: (similarity((merchant)::text, 'fashion'::text) > 0.2::double precision)
 ->  Bitmap Index Scan on product_Gist  (cost=0.00..9.37 rows=147 width=0) (actual time=2.055..2.055 rows=307 loops=1)
       Index Cond: (to_tsvector('english'::regconfig, (product)::text) @@ to_tsquery('hat'::text))
Total runtime: 14.705 ms

12M行を含むデータベースでこれらのクエリを実行すると、明らかにそれはもっと時間がかかります。 合計実行時間をさらに短縮するために誰かが私を助けることができます。

今、私の心の中にいくつかの質問があります:

  • 「WALMART BAGS」のようなクエリを検索するにはどうすればよいですか。最初に販売者WALMARTの製品BAGが返され、次に他の販売者のBAGSが返されます。

  • GINインデックスとGistインデックスの両方を使用できますか?

編集:

  • また、昨夜このクエリを実行したところ、次の結果が得られました。 Gistインデックスが既に作成されており、呼び出されていることを確認しました。それでもパフォーマンスは私の期待に応えていません。

    select count(*) from products 
    where (setweight(to_tsvector('english', merchant || ' ' || product), 'A') || 
    setweight(to_tsvector('english', product), 'B') ||
    setweight(to_tsvector('english', merchant), 'C')) @@ to_tsquery('hat')
    AND similarity(merchant,'fashion') > 0.2;
    
   Aggregate  (cost=450.97..450.98 rows=1 width=0) (actual time=18.228..18.228 rows=1 loops=1)
   ->  Bitmap Heap Scan on products  (cost=9.40..450.84 rows=49 width=0) (actual time=2.399..18.220 rows=12 loops=1)
    Recheck Cond: (((setweight(to_tsvector('english'::regconfig, (((merchant)::text || ' '::text) || (product)::text)), 'A'::"char") || setweight(to_tsvector('english'::regconfig, (product)::text), 'B'::"char")) || setweight(to_tsvector('english'::regconfig, (merchant)::text), 'C'::"char")) @@ to_tsquery('hat'::text))
    Filter: (similarity((merchant)::text, 'fashion'::text) > 0.2::double precision)
    ->  Bitmap Index Scan on products_weighted_index  (cost=0.00..9.39 rows=147 width=0) (actual time=2.206..2.206 rows=307 loops=1)
          Index Cond: (((setweight(to_tsvector('english'::regconfig, (((merchant)::text || ' '::text) || (product)::text)), 'A'::"char") || setweight(to_tsvector('english'::regconfig, (product)::text), 'B'::"char")) || setweight(to_tsvector('english'::regconfig, (merchant)::text), 'C'::"char")) @@ to_tsquery('hat'::text))
   Total runtime: 18.289 ms
   (7 rows)
4
Ankit Popli

評価

最後のクエリでは、「帽子」を探すビットマップインデックススキャンで307ヒットが生成されます。
その後、Postgresはビットマップヒープスキャンを実行して、商人を十分にフィルタリングし(similarity(...) > 0.2)、12行を生成します。テストは30K行で行われるため、実際のクエリは約300倍のヒットを生成します。当面のテストケースでは90k/3.5kです。 merchantの追加のインデックスが役立ちます。

助言

類似検索用に追加のトライグラムインデックスを作成することをお勧めします。必ずお読みください トライグラムインデックスのサポートに関するマニュアルの章追加モジュールpg_trgminstalled が必要です(明らかにそうです)。

最初のリクエストの場合:

「WALMART BAGS」のようなクエリを検索するにはどうすればよいですか。最初に販売者WALMARTの製品BAGが返され、次に他の販売者のBAGSが返されます。

類似演算子% を使用してこのクエリをお勧めします。

-- SELECT set_limit(0.2)  -- Adjust similarity operator only if needed

SELECT *
FROM   products
WHERE  to_tsvector('english', product) @@ to_tsquery('bag')
AND    merchant % 'walmart'
ORDER  BY merchant <-> 'walmart'
--    LIMIT  n; -- possibly limit to top n results

ここでも、GistとGINのどちらかを選択できますが、今回はGistが決定的な利点をもたらします。

これはGistインデックスではかなり効率的に実装できますが、GINインデックスでは実装できません。少数の最も近い一致のみが必要な場合、通常は最初の定式化に勝ります。

したがって、私はこのインデックスをお勧めします:

CREATE INDEX prod_merchant_trgm_idx ON products USING Gist (merchant Gist_trgm_ops);

あなたの2番目のリクエストについて:

GINインデックスとGistインデックスの両方を使用できますか?

はい、できます。同じ列(の組み合わせ)に両方のタイプがあることはほとんど意味がありませんが、Postgresは同じクエリでGistインデックスとGINインデックスを非常にうまく組み合わせることができます。 Combining Multiple Indexes で、優れたマニュアルをもう一度引用します。

複数のインデックスを組み合わせるために、システムは必要な各インデックスをスキャンし、メモリ内にビットマップを準備して、そのインデックスの条件に一致すると報告されるテーブル行の場所を提供します。次に、ビットマップは、クエリでの必要に応じて、ANDおよびORされます。最後に、実際のテーブル行にアクセスして返されます。テーブルの行は、物理的な順序でアクセスされます。これは、ビットマップのレイアウト方法だからです。これは、元のインデックスの順序が失われることを意味します。そのため、クエリにORDER BY句がある場合は、別の並べ替え手順が必要になります。このため、インデックススキャンを追加するたびに時間が追加されるため、プランナは、使用された可能性のある追加のインデックスが使用できる場合でも、単純なインデックススキャンの使用を選択することがあります。

9