distributed-search について
1台のマシンを超えるコーパスでランキング検索を構築するエンジニア向けの、分散全文検索設計スキルです。インデックスパイプライン、関連度、オートコンプリート、シャード、レプリカ、運用上のトレードオフを扱います。
主な機能
- 01バッチおよびニアリアルタイムのインデックス作成
- 02ブール、BM25、ハイブリッドランキング
- 03オートコンプリート方式の選択
- 04シャードとレプリカの計画
- 05検索とインデックスの障害分析
活用シーン
- 01大規模な文書コーパスへのランキング検索の追加
- 02更新文書の検索可能になる時期の選択
- 03入力中の候補表示の設計
- 04検索トラフィックに応じたシャードとレプリカの設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill distributed-searchインストール後、Claude Code で/distributed-searchを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.md詳細
分散検索
フリーテキストのクエリに最もよく一致するドキュメントを、関連度順に、1 台のマシンが保持できる量を超えるデータから高速に見つけます。これに失敗すると、プライマリデータベースに負荷をかける遅い LIKE '%term%' スキャンが発生するか、誤った結果を返してユーザーの信頼を損なう検索ボックスになってしまいます。どちらも、トラフィックやコーパスのサイズが問題を明らかにするまで気づかれません。
これを選ぶ場面
ユーザーが単語を入力し、完全一致のキー検索ではなく、関連度順に並んだ適切な一致結果を期待する場合です。コーパスはテキスト中心(ドキュメント、商品、ログ、メッセージ)で、クエリはアドホック(任意の単語、任意の組み合わせ)であり、結果にはランキング、ハイライト表示、ファセット、または入力補完が必要です。WHERE col LIKE や全テーブルスキャンがすでに読み取りのボトルネックになっている場合や、B-tree インデックスでは対応できないあいまい検索や部分一致検索が必要な場合に選びます。
選ばない場面
既知のキーによる取得や固定フィルターがアクセスパターンの場合は、プライマリデータベースのインデックスのほうがはるかに低コストで安定して処理できます。data-storage を使い続けてください。コーパスが小規模(数千行)の場合は、プロセス内のフィルターやデータベース組み込みの全文検索インデックスで十分です。別個の検索クラスターは運用上のオーバーヘッドにしかなりません(YAGNI)。検索はデータの派生コピーであり、最終的整合性を持つものです。決してシステムオブレコードにしてはいけません。
まず確認すること
- コーパスのサイズと増加量 — ドキュメント数、平均ドキュメントサイズ、インデックスの合計バイト数はどのくらいですか?(→
back-of-the-envelope)これによってシャード数が決まります。 - クエリの QPS と形状 — 読み取り中心ですか?単語、フレーズ、あいまい検索、ファセット、オートコンプリートのどれが必要ですか?レイテンシの目標(p99)は?
- インデックスの鮮度 — 新規または編集したドキュメントを数秒以内(ニアリアルタイム)に検索可能にする必要がありますか?それとも数分、数時間の遅延でも問題ありませんか?
- 関連度の基準 — 用語の完全一致で十分ですか?それともユーザーは「最適な」結果(ランキング、同義語、誤字許容)を期待しますか?
- 書き込みレート — 1 秒あたり何件のドキュメントが変更されますか?これによってインデックス作成パイプラインの規模が決まります。
選択肢
パイプライン(ほぼ常に存在します):ソースがドキュメントの変更を出力し、インデックス作成パイプラインがそれを変換・分析し、転置インデックスが用語→ドキュメントの転置リストを保存し、クエリパスが一致を判定してランキングします。クロールベースのシステム(ウェブ検索)では、クロール → 解析 → 重複排除を先行させます。このクローラーは、同じパイプラインにデータを供給する独立したサブシステムです。
インデックス構築モード
- バッチ/一括再インデックス: インデックス全体を定期的に再構築します。コーパスの変化が遅い場合や、鮮度が数時間程度で許容される場合に使います。
- ニアリアルタイム(増分): 変更を継続的に適用し、ドキュメントを数秒以内に検索可能にします。ユーザーが書いた内容をすぐに見つけられることを期待する場合に使います。
ランキングモデル
- ブール/フィルターのみ: 一致判定のみで、スコア付けはしません。順序が重要でない完全一致フィルタリング(タグ、ファセット)に使います。
- TF-IDF/BM25(語彙ベース): 用語の出現頻度と希少性に基づいてスコアを付けます。標準的な全文検索の関連度モデルで、低コストかつ説明可能です。
- ハイブリッド(語彙ベース + シグナル): BM25 に人気度、最新性、またはビジネス上のブーストを組み合わせます。「最適」が単語の重なりだけでは決まらない場合に使います。
オートコンプリート
- メモリ内プレフィックス Trie/FST: プレフィックスからサブミリ秒で入力補完します。入力中の候補提示に使います。
- Edge-n-gram インデックス: メインインデックス内でプレフィックス一致を行います。より高コストですが、候補にもフィルターや関連度を適用する必要がある場合に使います。
分散化:インデックスをシャード(それぞれがドキュメントの一部を対象とする独立した転置インデックス)に分割して容量を確保し、各シャードにレプリカを配置して読み取りスループットと耐障害性を高めます。シャーディングの理論については data-storage を参照してください。
トレードオフ
| 選択肢 | 解決すること | 悪化すること | 変更するタイミング |
|---|---|---|---|
| バッチ再インデックス | シンプルで、アトミックに切り替えられ、稼働中の書き込み処理が複雑にならない | 次のビルドまで古いデータのまま。全件再ビルドのコストが高い | ユーザーが最新の結果を必要とする場合 → ほぼリアルタイム |
| ほぼリアルタイム | 数秒以内に最新の状態になる。全件再ビルドが不要 | セグメントの入れ替わり、マージ負荷、書き込み時のリフレッシュコスト | 書き込み率やマージコストがノードの処理能力を超える場合 → バッチ/リフレッシュ間隔を長くする |
| ブール値/フィルター | 最も低コストで、決定論的 | 結果の「最適さ」を判断できない | ユーザーが結果の品質を重視する場合 → BM25 を追加 |
| BM25 | 関連性が高く、説明可能で、低コスト | 人気度/新しさ/意図を考慮しない | 単語の重なりだけでは不十分な場合 → ハイブリッドシグナル |
| ハイブリッドシグナル | ビジネス/ユーザーの意図に合致する | 複雑でデバッグが難しく、調整用データが必要 | 調整コストが効果を上回る場合 → BM25 に戻す |
| プレフィックストライ/FST | 入力補完が最速 | 構築/リフレッシュする別の構造が必要。フィルターを無視する | 候補にフィルター/関連性が必要な場合 → edge-n-gram |
| シャードを増やす | 並列処理が可能で、大規模なコーパスに対応できる | クエリごとのファンアウトとマージのオーバーヘッドが増える。小さすぎるシャードはリソースを浪費する | ファンアウトのレイテンシーが支配的な場合 → シャード数を減らし、サイズを大きくする |
| レプリカを増やす | 読み取り QPS の向上と高可用性 | RAM/ディスクの増加。書き込み時のレプリケーション遅延 | 書き込み増幅が問題になる場合 → レプリカを減らす |
高負荷時の動作
検索では、ファンアウトと派生データの遅延によって問題が増幅されます。
- クエリのファンアウトによるテールレイテンシー: すべてのクエリが全シャードに送られ、最も遅いシャードが応答時間を決めます。負荷の高いシャードや GC で一時停止したシャードが、すべてのクエリを遅延させます。 軽減策: シャードのサイズを均等にし、レプリカを追加し、結果の取得件数に上限を設け、タイムアウトと部分的な結果を利用します。
- インデックス作成とクエリ処理の競合: 書き込み/マージの急増(一括インポート、再インデックス)によって、クエリに使う CPU と I/O が奪われ、レイテンシーが急上昇します。軽減策: 一括インデックス作成を制限し、大規模なマージをオフピークにスケジュールし、インデックスノードとクエリノードの役割を分離します。
- ホットシャード/偏り: 不均等なシャードキーによって、ドキュメントや人気の高い用語が 1 つのノードに集中します。軽減策: ドキュメントをハッシュルーティングし、ホットシャードを再ルーティングするか分割します。
- 深いページネーション:
from=100000は、すべてのシャードに膨大な範囲のソートを強制します。軽減策: カーソル/search_afterを使い、ページの深さに上限を設けます。 - パイプラインの滞留: ソースがインデクサーの処理速度を上回る速さで変更を生成すると、鮮度の遅延が際限なく拡大します。軽減策: バックプレッシャーと永続的なバッファー(→
messaging-streaming)を導入し、スループットだけでなく遅延も監視します。 - 再起動後のキャッシュミス: ファイルシステム/ページキャッシュが空のため、キャッシュが温まるまでレイテンシーが急上昇します。軽減策: 重要なクエリでキャッシュを温め、トラフィックを段階的に増やします。
監視項目: シャードごとのクエリレイテンシーの p99、インデックス作成の遅延(ソースから検索可能になるまで)、セグメント/マージの数、ヒープ/GC、シャードのバランス、拒否されたリクエストとキューに入ったリクエスト。
適用方法
- 入力を明確にする — コーパスの規模、クエリの QPS と形状、鮮度の 目標、関連性の基準を決めます(最初に確認することを参照)。DB インデックスまたは組み込みの 全文検索でアクセスパターンに対応できる場合は、そこで終了します。検索クラスターは不要です。
- トレードオフ表から選ぶ — 構築モード(鮮度)、ランキング モデル(関連性の基準)、オートコンプリートに独自の構造が必要かどうかを選択します。
- 主要な設定値を決める — シャード数(インデックスのバイト数 ÷ 目標シャードサイズから算出)、 レプリカ数(読み取り QPS と HA から算出)、アナライザー/トークナイザー(言語、ステミング、 同義語)、リフレッシュ間隔(鮮度と書き込みコストのバランス)を設定します。
- 選択をストレステストする — 高負荷時の動作(ファンアウトのテールレイテンシ、 インデックス作成時の競合、ホットシャード、深いページネーション、パイプラインの滞留)を確認し、 トラフィックプロファイルによって発生しうる各事象への緩和策を確認します。
- 数値で規模を決める — シャードがコーパスを格納できること、シャードあたりのサイズが 健全な範囲内に収まること、レプリカがピーク QPS に対応できること、インデックス作成スループットが 鮮度の予算内に遅延を抑えられることを確認します(→ 重要な数値)。
- プロバイダーを選ぶ — 汎用的なレシピをデフォルトとし、ユーザーがクラウドを指定した場合にのみ プロバイダーのファイルを開きます(プロバイダーの選び方を参照)。
- 回答でトレードオフを示す — ここで行った各選択肢について、
Decision | Solves | Worsens | Change it when…の表にそれぞれ1行ずつ記載します(例: 選択肢、そのストア、障害時のポリシー)。最後の列には具体的な変更の契機(数値または観測可能な指標)を 記載します。次に、最初に問題が発生する箇所を示します。
やるべきこと・避けるべきこと
やるべきこと
- インデックスは再構築可能な派生コピーとして扱い、正本は
data-storageに保持し、そこから完全に再インデックスできるようにします。 - 起動前にシャードのサイズを決めます(コーパス ÷ 目標シャードサイズ)。ファンアウトのコストを 抑えるため、小さすぎるシャードではなく、均等なサイズを目指します。
- 書き込みの急増でインデクサーが追いつかなくなったり、変更が失われたりしないよう、永続バッファーを介して インデックス作成パイプラインにデータを送ります。
- ランキングは BM25 から始め、単語の重複だけでは意図を捉えられないことが明らかな場合にのみ、 人気度/新しさのシグナルを追加します。
- インデックス作成の遅延はクエリレイテンシとは別に監視します。鮮度の問題は気づかれないまま発生するためです。
避けるべきこと
- 検索エンジンをプライマリストアやトランザクション書き込み先として使わないでください。
- 小規模なコーパスを DB の全文検索インデックスまたはプロセス内フィルターで処理できる場合は、 別のクラスターを導入しないでください(YAGNI)。
- 深い
from/offsetページネーションを許可しないでください。代わりにカーソルを使います。 - 過剰にシャーディングしないでください。小さなシャードが多数あると、メリットなしにファンアウトと マージのオーバーヘッドが増えます。
- ライブクエリを処理中のクラスターに対し、大規模な一括再インデックスをスロットリングなしで実行しないでください。
重要な数値
設計を左右する量は、インデックス全体のバイト数(コーパス × ドキュメントあたりのオーバーヘッドを求め、
その後、目標シャードサイズで割ってシャード数を算出)、読み取り QPS × ファンアウト(各クエリが
すべてのシャードにアクセスするため、レプリカの規模を決める要素)、鮮度の遅延を予算内に抑えるための
インデックス作成スループットと変更率です。単一シャードが良好に動作するサイズ範囲には上限があります。
それを超えたら分割します。レイテンシ、QPS、ストレージの数値には back-of-the-envelope を使い、
ここでは繰り返さないでください。
インターフェースの概要
契約を構成するのは、ドキュメント、クエリ、転置リストです。ドキュメントは、分析対象のフィールドを持つ
型付きレコードです。例:{ "id": "p123", "title": "...", "body": "...", "tags": ["a","b"], "price": 9.99 }。クエリは、フィールド、
一致タイプ、フィルター、ソート、ページネーション用カーソルを指定します。例:q="wireless earbuds", fields=[title^2, body], filter={tag:audio}, sort=_score, search_after=<cursor>。
転置インデックスは、各用語を (doc_id, term_freq, positions) のポスティングリストに対応付けます。ランキングではこれを使って BM25 を計算します。アナライザー
(トークナイザー、小文字化、ステミング、同義語)は最初に決めてください。これは契約の一部であり、
変更する場合は再インデックスが必要です。
プロバイダーの選択
上記の汎用レシピ(Elasticsearch/OpenSearch、Lucene、Solr、セルフホスト可能)をデフォルトとして使用してください。ユーザーがクラウドを指定した場合は、マネージドサービスの対応関係、クォータ/制限、プロバイダー固有のトレードオフについて、references/providers/<provider>.md を参照してください。そのプロバイダー用のファイルがない場合は、汎用レシピを回答とします。
図
ソース → インデックス作成パイプライン → 転置インデックス(シャーディング+レプリケーション)→ クエリ/ランキングの経路、またはクロール→解析→重複排除のフロントエンドを視覚化するには、プラグイン内の architecture-diagram スキルを使用してください。インデックス作成の経路とクエリのファンアウトは別々のフローとして示し、各シャードの背後にレプリカを配置してください。
関連する構成要素
data-storage— テキスト検索における DB のLIKE/全テーブルスキャンの代替です。検索は転置インデックスを担い、シャーディング/パーティショニングとレプリケーションの理論はそちらで扱います。名前を挙げてリンクし、内容を再説明しないでください。messaging-streaming— インデックス更新パイプラインでこのスキルに依存します。永続バッファーがドキュメントの変更をインデクサーに届け、書き込みの急増を吸収します(配信、順序付け、バックプレッシャーはそちらで扱います)。caching— ホットクエリや繰り返されるクエリをキャッシュし、ファンアウトの負荷を軽減するため、このスキルと組み合わせて使用します(スタンピード、ホットキー、削除の処理はそちらで扱います)。back-of-the-envelope— このスキルに入力を提供します。コーパスのバイト数、QPS、鮮度の数値をもとに、シャード、レプリカ、パイプラインの規模を決定します。system-design— このオーケストレーターで扱う概念です。推論ループ、トレードオフの検討方法、10 個の障害モードを扱います。
参考資料
references/deep-dive.md— 転置インデックスの内部構造(ポスティング、セグメント、マージ)、クロール/インデックス作成/検索のパイプライン、BM25 スコアリング、ニアリアルタイム更新、オートコンプリート(トライ/FST/n-gram)、シャードルーティング、レプリカ読み取りについて説明しています。検索レイヤーを詳細に設計する際に参照してください。references/providers/{generic,aws,azure,gcp}.md— 各環境のサービス対応関係、制限、注意点について説明しています。