PostgreSQL フルテキスト検索:あなたのアプリに十分なとき
多くのアプリは PostgreSQL のフルテキスト検索で足ります。シンプルな判断ルール、スタータークエリ、インデックスチェックリストでいつ専用検索エンジンを追加すべきか判断できます。

実際のアプリで「十分な検索」はどう見えるか
多くの人は「フルテキスト検索が欲しい」とは言いません。求めているのは、速く感じられて最初のページで意図した結果が見つかる検索ボックスです。結果が遅い、ノイズが多い、順序が不自然だと、ユーザーは使われた技術(PostgreSQL FTS か別エンジンか)には関心がなく、単に検索を信頼しなくなります。
ここでの決定は一つです:検索を Postgres 内に留めるか、専用の検索エンジンを追加するか。目標は完璧な関連性ではなく、迅速に出荷でき、運用が簡単で、実際の利用法に対して十分に良い堅実な基準です。
多くのアプリでは、PostgreSQL のフルテキスト検索で長い間十分です。タイトル、説明、メモなどいくつかのテキストフィールド、基本的なランキング、ステータスやカテゴリなどのフィルタが一つ二つある程度なら、Postgres で追加インフラ無しに処理できます。可動部が少なく、バックアップが簡単で、「アプリは動いているのに検索だけ落ちているのはなぜ?」という事故も減ります。
「十分」は通常、同時に三つの目標を満たせることを意味します:
- 品質: 共通クエリで正しいアイテムが上位に来る。明白な一致が欠けない。
- 速度: 通常負荷で結果が速く返る(開発マシンだけで速いのではない)。
- 運用努力: チームが二つ目のクラスタ、追加監視、別のインデックスパイプラインを運用せずに管理できる。
具体例:ユーザーがプロジェクト名やノートで検索する SaaS ダッシュボード。"onboarding checklist" のようなクエリで関連プロジェクトがトップ5に入り、1 秒未満で返り、解析や再インデックスに常時手間をかけていなければ、それは「十分」です。これらの目標を複雑さを積み上げないと達成できないなら、「組み込み検索 vs 検索エンジン」を本気で考える時です。
よくある検索要件とそれが示す本当の意味
チームはしばしば機能で検索を語りがちで、成果では語りません。それぞれの機能を、構築・調整・信頼性維持にかかるコストに翻訳するのが有用です。
初期の要求は典型的に:誤字許容、ファセットとフィルタ、ハイライト、「賢い」ランキング、オートコンプリートといったものです。最初のバージョンでは、必須と望ましいものを分けて考えましょう。基本的な検索ボックスは、関連項目を見つけ、一般的な語形(複数形や時制)を扱い、単純なフィルタを尊重し、テーブルが増えても速さを保つことが求められます。ここが Postgres FTS が適する領域です。
Postgres は、コンテンツが通常のテキストフィールドにあり、検索をデータに近いところで行いたい場合に得意です:ヘルプ記事、ブログ投稿、サポートチケット、社内ドキュメント、製品タイトルや説明、顧客レコードのメモなど。これらは主に「正しいレコードを見つける」問題であり、「検索製品を作る」問題ではありません。
一方、望ましい機能が複雑さを呼び込みます。誤字許容やリッチなオートコンプリートは通常、追加ツールを促します。ファセットは Postgres でも可能ですが、多数のファセット、深い分析、巨大データセットにわたる迅速なカウントが必要なら、専用エンジンの魅力が増します。
見えにくいコストはライセンス料ではなく、第二のシステムです。検索エンジンを追加すると、データ同期やバックフィル(とそれに伴うバグ)、監視とアップグレード、「検索が古いデータを出すのはなぜか?」というサポート業務、二つの関連性調整セットが増えます。
迷ったら Postgres から始め、シンプルなものを出荷し、満たせない明確な要件が出てきたときだけもう一つ加えましょう。
シンプルな判断ルール:組み込み検索 vs 専用エンジン
三つのチェックルールを使います。三つともパスすれば PostgreSQL FTS に留まってください。どれか一つを大きく失敗するなら専用検索エンジンを検討します。
三つのチェック項目
-
関連性の必要度: 「十分に良い」結果で受け入れられるか、それとも誤字・同義語・"people also searched"・パーソナライズなど多数のエッジケースでほぼ完璧な順位付けが必要か。多少の順序の不完全さを許容できるなら、Postgres は通常十分です。
-
クエリ量とレイテンシ: ピーク時に秒間どれだけの検索が来るか、実際のレイテンシ目標は何か。検索が全トラフィックの小さい部分で、適切なインデックスで速さを保てるなら Postgres で問題ありません。検索が主要なワークロードになり、コアの読み書きと競合し始めるなら警告です。
-
複雑さ: 一〜二のテキストフィールドを検索するだけか、多数のシグナル(タグ、フィルタ、時間減衰、人気度、権限)や多言語を組み合わせるか。ロジックが複雑なほど SQL 内で摩擦を感じるでしょう。
安全な出発点はシンプルに:Postgres でベースラインを出し、遅いクエリや「ヒットなし」検索をログに残し、それから判断してください。多くのアプリは成長しても Postgres で足りますし、二つ目のシステムを早すぎて導入するリスクを避けられます。
専用エンジンを示唆するレッドフラグ:
- 高品質な誤字許容、同義語、オートコンプリートが必須。
- 多数のフィールドに跨るファセットと複雑なフィルタ。
- 検索トラフィックがプライマリ DB に負荷をかける。
- 多言語で辞書以上の関連性が必要。
- 学習型ランキングやクリックフィードバックのような高度なランキング機能が必要。
Postgres に留まるグリーンフラグ:
- 検索は「正しいページを見つける」用途で、コンテンツの発見が目的ではない。
- 運用と信頼性のために一つの DB に留めたい。
- コンテンツが数列(タイトル、本文、タグ)に収まる。
- 単純な関連性チューニングを受け入れてゆっくり改善できる。
- チームが可動部を減らしたい(同期パイプラインや二重書き込みを避ける)。
PostgreSQL フルテキスト検索の仕組み(必要なことだけ)
PostgreSQL FTS は、テキストをデータベースが速く検索できる形式に変換する組み込みの仕組みです。すべての行をスキャンせずに済むようにするもので、コンテンツが既に Postgres にある場合に予測可能な運用で速くまともな検索を提供します。
知っておくべき三つの要素:
- tsvector: テキストの「検索可能な形式」。Postgres がテキストをトークン化し正規化して保存します。
- tsquery: ユーザーが探しているものを構造化して表現したもの(単語、AND/OR、フレーズ風の演算子)。
- ランキング: 結果を並べるためのスコア。
ts_rank(またはts_rank_cd)で関連性の高い行を上にできます。
言語設定は、単語の扱いを変えるため重要です。適切な設定があれば “running” と “run” が一致したり(ステミング)、一般的な不要語が無視されたりします。間違った設定だと、通常のユーザー表現がインデックスと一致しなくなり、検索が壊れたように感じられます。
プレフィックスマッチはタイプアヘッド風の挙動を実現したいときに使われます(例:「dev」で「developer」にマッチ)。Postgres FTS では通常プレフィックス演算子(例 term:*)で実現します。見た目の品質は上がりますが、クエリごとの作業が増えることが多いので、デフォルトではなくオプション的な扱いが良いでしょう。
Postgres が目指していないのは、あらゆる機能を備えた完全な検索プラットフォームです。曖昧検索のスペル補正、先進的なオートコンプリート、フィールドごとの複雑なアナライザ、複数ノードに跨る分散インデックスが必要なら、組み込みの快適域外です。しかし多くのアプリでは、PostgreSQL FTS はユーザーが期待するほとんどの機能を、はるかに少ない可動部で提供します。
コピーして使えるスタータークエリ
検索したいコンテンツに対する小さく現実的な形:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
PostgreSQL FTS の良いベースラインは:ユーザー入力からクエリを作り、可能なら先に行をフィルタし、その後残ったマッチをランキングすることです。
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at \u003e= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
後で時間を節約するいくつかのポイント:
- 安価なフィルタ(status, tenant_id, date ranges)を
WHEREに先に置いてからランキングする。ランク付けする行が少なくなり速くなります。 ORDER BYにタイブレーカー(例えばupdated_atやid)を常に追加して、同じランクの結果でページングが安定するようにする。- ユーザー入力には
websearch_to_tsqueryを使う。引用符やシンプルな演算子を扱う際に人が期待する挙動になります。
このベースラインが動いたら、to_tsvector(...) 式を保存列に移して、毎回計算しないようにし、インデックス化を簡単にします。
効果のあるインデックス設定
「PostgreSQL FTS が遅い」という話の多くは一つに帰着します:データベースが毎回検索ドキュメントを作っていること。まずはプリビルドの tsvector を保存してインデックスを作ることが解決策です。
tsvector を保存する:生成列かトリガーか?
検索ドキュメントが同じ行の列から構築されるなら生成列が最もシンプルです。更新時に自動で正しく保たれ、忘れにくいです。
ドキュメントが関連テーブルに依存する場合(例えば製品行とカテゴリ名を組み合わせるなど)や、単一の生成式で表現しにくいカスタムロジックがある場合は、トリガーで維持してください。トリガーは可動部分を増やすので、小さくしてテストしましょう。
ほとんどの場合欲しいインデックス
tsvector 列に GIN インデックスを作るのがベースラインで、多くのアプリ検索で瞬時に感じられるようになります。
多くのアプリで有効なセットアップ:
- 検索対象の
tsvectorを最も頻繁に検索する行と同じテーブルに置く。 - その
tsvectorに GIN インデックスを追加する。 - クエリが
to_tsvector(...)を毎回計算せず、保存されたtsvectorに対して@@を使っていることを確認する。 - 大量のバックフィルの後は
VACUUM (ANALYZE)を行い、プランナが新しいインデックスを理解するようにする。
同じテーブルにベクトルを置くのが通常は速く簡単です。ベーステーブルが非常に書き込み多となる場合や、複数テーブルに跨る結合ドキュメントを自分のスケジュールで更新したい場合は、別の検索テーブルが有用なこともあります。
部分インデックスは、検索対象が一部の行だけ(例:status = 'active'、マルチテナントの単一テナント、特定言語)であればインデックスサイズを小さくし検索を速くできますが、クエリが常に同じフィルタを含む場合に限ります。
過度な設計を避けて許容できる関連性を得る方法
関連性ルールをシンプルで予測可能にしておけば、Postgres FTS でも驚くほど良い結果が得られます。
最も簡単な改善はフィールド重み付けです:タイトルのヒットは本文のヒットより重要にすべきです。タイトルに高い重みを与えた結合 tsvector を作り、ts_rank や ts_rank_cd でランク付けします。
「新しさ」や「人気」を浮上させたいなら慎重に行ってください。小さなブーストは問題ありませんが、テキスト関連性を上書きしないように。実用的なパターンは:まずテキストでランク付けし、同点は新しさで分ける、あるいは上限付きのボーナスを与えて無関係な新着が完璧な古い一致に勝たないようにすることです。
同義語やフレーズマッチは期待が分かれる箇所です。同義語は自動的には付かないので、シソーラスやカスタム辞書を追加するか、クエリ側で用語を展開する必要があります(例:「auth」を「authentication」と扱う)。フレーズマッチもデフォルトではありません:通常クエリは単語をどこでもマッチさせます。ユーザーが引用符付きフレーズや長い質問を入力するケースが多ければ、phraseto_tsquery や websearch_to_tsquery の使用を検討してください。
混在言語のコンテンツは方針が必要です。ドキュメントごとに言語が分かっているならそれを保存し、適切な設定(English、Russian など)で tsvector を生成してください。分からない場合の安全策は simple 設定(ステミングしない)でインデックスするか、既知言語用のベクトルとすべてに対する simple ベクトルの両方を保持することです。
関連性を検証するには小さく具体的に:
- ユーザー(またはサポートチャット)からの実際のクエリを 10〜20 件収集する。
- 各クエリに対して上位に来るべき 1〜3 件の結果を書き出す。
- 各チューニング変更後に実行し、何が改善し何が悪化したかを記録する。
- アプリにとって「十分」に感じられたらそこで止める。
これで多くのアプリ検索ボックス(テンプレート、ドキュメント、プロジェクトなど)では PostgreSQL FTS で十分です。
Postgres 検索が悪く見える一般的なミス
「PostgreSQL FTS が遅い/関連性が低い」という話の多くは回避可能なミスから来ます。それらを直すのは、別の検索システムを導入するより簡単なことが多いです。
よくある落とし穴の一つは tsvector を計算済みの値として扱い、挿入や更新時に確実に更新しないことです。tsvector を列に保存しているのに毎回更新されていなければ、インデックスとテキストが一致せず結果がランダムに見えます。逆にクエリ内で to_tsvector(...) を毎回計算すると結果は正しくても遅くなり、専用インデックスの恩恵を受けられません。
もう一つの性能を悪化させるパターンは、候補セットを狭める前にランキングしてしまうことです。ts_rank は有用ですが、通常はインデックスでマッチした行を絞った後に実行すべきです。巨大な部分でランクを計算したり、先に他テーブルと結合したりすると、速い検索をテーブルスキャンに変えてしまいます。
人々はまた「contains 検索」を LIKE '%term%' のように期待しますが、前方ワイルドカードはフルテキスト検索の単語ベースの性質に合いません。製品コードや部分 ID にはトリグラム索引など別手段を使ってください。
パフォーマンスの問題はマッチングではなく結果処理に由来することも多いです。注意すべき二つのパターン:
- 大きな
OFFSETを使うページング。ページを進めるごとに Postgres がより多くの行をスキップする。 - 無制限の結果セットで、クエリが数万行以上を返す可能性がある。
運用面も重要です。頻繁な更新の後にインデックス膨張(bloat)が起き、再インデックスが高コストになることがあります。大きな変更前後で EXPLAIN ANALYZE を使って実際のクエリ時間を測ってください。数値なしでは、Postgres FTS を「直す」つもりが別の形で悪化させてしまいがちです。
クイックチェックリスト:クエリとインデックスの健全性
PostgreSQL FTS を責める前に、次のチェックを走らせてください。ほとんどの「Postgres 検索が遅い/関連性が低い」バグは基本が欠けているだけです。
データ + インデックスの健全性チェック
本物の tsvector を構築する:生成列か維持された列に保存し(クエリで毎回計算しない)、適切な言語設定(english, simple 等)を使い、フィールドを混ぜるなら重み付けを適用する(title > subtitle > body)。
インデックス化の対象を正規化する:ID、定型文、ナビゲーションテキストのようなノイズなフィールドは tsvector から外し、ユーザーが検索しない巨大な BLOB は切り詰める。
正しいインデックスを作る:tsvector 列に GIN インデックスを追加し、EXPLAIN で使われていることを確認する。検索対象が一部の行だけなら部分インデックスでサイズを削れますが、クエリが常にそのフィルタを含む場合に限ります。
テーブルを健康に保つ:デッドタプルはインデックススキャンを遅くします。頻繁に更新されるコンテンツでは定期的な vacuum が重要です。
再インデックス計画を持つ:大規模なマイグレーションや膨張したインデックスは制御されたウィンドウで再インデックスが必要になることがあります。
データとインデックスが正しくなったら、クエリ形状に注力してください。PostgreSQL FTS は候補セットを早く絞れると速く動きます。
クエリ + 実行時の健全性チェック
先にフィルタし、その後ランキング:厳しいフィルタ(tenant, language, published, category)を先に適用してからランク付けする。後で破棄する何千行ものランク計算は無駄です。
安定した順序付け:rank の後に updated_at や id のようなタイブレーカーを順序に入れて、リフレッシュで結果が跳ねないようにする。
「クエリですべてやろうとしない」:曖昧マッチや誤字許容が必要なら意図的に行い(そして計測する)、誤ってシーケンシャルスキャンを強いるような実装は避ける。
実際のクエリでテストする:トップ 20 の検索を収集し、手で関連性を確認し、小さな期待結果リストを保持して回帰を検出する。
遅い経路を監視する:遅いクエリをログし、EXPLAIN (ANALYZE, BUFFERS) を確認し、インデックスサイズやキャッシュヒット率を監視して成長が挙動を変えるときに気づけるようにする。
例: 基本的なサイト検索から成長の必要へ
SaaS のヘルプセンターは出発点として良い例です。目的は単純:その質問に答える記事を見つけること。数千の記事があり、それぞれにタイトル、短い要約、本文がある。訪問者は通常 "reset password" や "billing invoice" のような 2〜5 単語を入力します。
PostgreSQL FTS でこれを驚くほど速く「完了」扱いにできます。結合したフィールドの tsvector を保存し、GIN インデックスを追加し、関連性でランク付けします。成功は次のように見えます:結果が 100 ms 未満で返り、トップ 3 が通常正しく、システムを常時監視して世話をする必要がない状態です。
その後プロダクトが成長します。サポートは製品領域、プラットフォーム(web, iOS, Android)、プラン(free, pro, business)でフィルタしたがり、ドキュメント作成者は同義語や「did you mean」、誤字の扱いを望みます。マーケティングは「0 件の結果の多い検索」の分析を求めます。トラフィックが増え検索が最も負荷のかかるエンドポイントの一つになります。
これらが専用検索エンジンを検討すべき信号です:
- 同じページで多数のフィルタとファセットが必要。
- ファーストクラスの曖昧マッチや誤字許容、オートコンプリートが必要。
- 検索分析や関連性のためのフィードバックループが必要。
- 検索トラフィックが多く、プライマリ DB から切り離す必要がある。
実用的な移行パスは、専用エンジン追加後も Postgres をソースオブトゥルースとして保持することです。まず検索クエリやノーヒットケースをログし、非同期同期ジョブで検索対象フィールドだけを新しいインデックスにコピーします。しばらくは両方を並行運用し、段階的に切り替えるのが安全です。一斉に賭けるのは避けましょう。
次のステップ: ベースラインを出荷し、計測してから判断
検索が主に「これらの単語を含む文書を見つける」もので、データセットが巨大でなければ、PostgreSQL FTS で十分なことが多いです。まずそこから始め、動かしてから専用エンジンが必要かどうか判断してください。
まとめとして持っておくべき点:
tsvectorを保存し、GIN インデックスを追加し、ランク要件が基本的なら Postgres FTS を使う。- ひとつのスタータークエリとインデックス設定を出荷し、実際のレイテンシと「ユーザーが見つけられたか」を計測する。
- 関連性のチューニングは小さく明白な変更(重み、言語設定、クエリパース)で行い、大規模な書き換えは避ける。
- オートコンプリート、強い誤字許容、ファセット、あるいはサイズや負荷の成長といった明確なギャップが出たときだけ検索エンジンを計画する。
実用的な次の一歩:前節のスタータークエリとインデックスを実装し、1 週間ほど簡単なメトリクスをログしてください。p95 クエリ時間、遅いクエリ、そして「検索 → クリック → すぐ離脱しない」などの粗い成功シグナルを追いましょう(基本的なイベントカウンタで十分です)。それで、より良いランキングが必要か、単に UX(フィルタ、ハイライト、スニペット)が必要かがすぐ見えてきます。
強力なオートコンプリートやキーストロークごとの即時検索のような要件、強い誤字許容やスニペットが必須、複数フィールドに跨る高速集計・ファセット、学習型ランキングやクエリごとのブーストが必要、あるいは持続的な高負荷で大きなインデックスを高速に保てない、というケースになったら専用エンジンを計画してください。
アプリ側を素早く進めたいなら、Koder.ai (koder.ai) は検索 UI と API をチャットでプロトタイプするのに便利です。スナップショットとロールバックを使って安全に反復し、ユーザー行動を計測しながら改善できます。
よくある質問
What does “enough search” actually mean for a real app?
PostgreSQL のフルテキスト検索が「十分」なのは、同時に次の三つを満たせるときです:
- 関連性の高い結果: 明らかな一致が上位に表示される。
- 高速な応答: 通常負荷で結果が速く返る。
- 低い運用負荷: 二つ目のシステムや同期パイプラインを運用する必要がない。
ストアされた tsvector と GIN インデックスでこれらが満たせるなら、通常は良い状態です。
Should I start with Postgres search or add a search engine right away?
まずは PostgreSQL のフルテキスト検索をデフォルトにしましょう。導入が速く、データと検索が同じ場所にあり、別のインデクシングパイプラインを作って維持する必要がありません。
Postgres が苦手な明確な要件(高品質な誤字許容、リッチなオートコンプリート、大量のファセット、あるいは検索負荷が本番 DB を圧迫する場合)が出てきたら専用エンジンへ移行を検討してください。
What’s the quickest decision rule for Postgres FTS vs a dedicated engine?
簡単な判断ルールは次の三点を満たすかどうかです。すべて満たすなら Postgres に留まるべきです:
- 関連性の必要度: 「十分に良い」順位付けで問題ないか。
- 負荷 + レイテンシ: 検索がプライマリ DB を圧迫していないか。
- 複雑さ: 数件のテキスト列と少数のフィルタで済むか。
一つでも大きく欠けるなら、専用の検索エンジンを検討してください(特に誤字/オートコンプリートや高トラフィックの場合)。
What kinds of search problems are a good fit for PostgreSQL full-text search?
Postgres FTS は、タイトル/本文/メモといった数カ所のフィールドで「正しいレコードを見つける」用途に向きます。単純なフィルタ(テナント、ステータス、カテゴリ)と組み合わせる検索が該当します。
ヘルプセンター、社内ドキュメント、チケット、ブログ記事検索、SaaS ダッシュボードでのプロジェクト名やノート検索などに強く適しています。
What’s a good “starter query” shape for Postgres full-text search?
良いベースラインのクエリ形は通常:
websearch_to_tsqueryでユーザ入力を解析。- 先に安価な制約(テナント/ステータス/日付)で絞る。
- ストアされた
tsvectorに対して@@でマッチ。 ts_rank/ts_rank_cdとupdated_at, idのような安定したタイブレーカーでソート。
こうすることで、関連性・速度・ページングの安定性が保てます。
How do I make Postgres search fast without adding more infrastructure?
プリビルドの tsvector を保存し、GIN インデックスを追加してください。毎回 to_tsvector(...) を計算するのを避けることで速度が劇的に改善します。
実務的なセットアップ:
- 検索対象の
tsvectorは照会する同じテーブルに置く。 - その列に GIN インデックスを作る。
- クエリで
tsvector_column @@ tsqueryが使われていることを確認する。
検索が遅いと感じたときの最も一般的な修正はこれです。
Should I use a generated column or a trigger to maintain the tsvector?
同じ行の列から検索ドキュメントを作るなら 生成列(generated column)が最もシンプルで安全です。更新時に自動で正しく保たれます。
関連テーブルを組み合わせるなど複雑なロジックが必要な場合は、トリガーで tsvector を維持してください。トリガーは可動部分が増えるので、小さくしてテストを行いましょう。
How can I improve relevance without overengineering ranking?
過度な工学化をせずに関連性を改善するコツ:
- フィールド重み付け: タイトルの一致は本文の一致より強くする。
- ブーストは小さく: 新しさや人気を加える場合は、テキスト関連性を覆さない程度に留める。
- 言語設定を正しく: ステミングやストップワードは
run/runningのような一致に影響する。
変更後は少量の実際のユーザークエリで期待される上位結果と比較して検証してください。
Why doesn’t Postgres FTS behave like “contains” search (LIKE %term%)?
FTS は単語ベース(語彙)で動作するため、LIKE '%term%' のような部分一致とは挙動が異なります。商品コードや部分 ID のようなサブストリング検索が必要なら、トリグラムインデックスなど別の手段で処理する方が適切です。FTS に無理やりやらせるべきではありません。
What are the red flags that mean I should add a dedicated search engine?
Postgres FTS を使い続けるべきでない兆候:
- 高品質な誤字許容、スニペット付きの強いオートコンプリート、あるいは大規模な同義語管理が必須。
- 多数のファセットや集計を高速に行う必要がある。
- 検索トラフィックがプライマリ DB に負荷を与えている。
- 学習型ランキングやクリックフィードバックのような高度な関連性ツールが必要。
実務的には、要件が明確になったら Postgres をソースオブトゥルースのまま非同期でインデックスを作る手順へ移行することが多いです。