全文検索
Docsbookの検索は、ページ全体を対象とするPostgresの全文インデックスで、ヘッダーの検索ボタンとサイドバーの検索ボックスから利用できます。クエリごとの費用はかかりません — モデルは呼び出されません — そして、何も返さなかったクエリは、ドキュメントが生み出す最も有用な2つのシグナルのうちの1つです。
得られるもの#
- ヘッダー、サイドバー、またはその両方に検索ボックス。 両方を同時に動作させることもできますが、通常は一方で十分です。
- 一致箇所を強調表示したスニペット付きの検索結果 — ページの先頭200文字ではなく、一致箇所の周辺にあるページ本文から抜き出したコンテキストウィンドウです。
- 見出しへの正確なディープリンク。 セクション内の一致箇所には、そのセクション本来のアンカーが付くため、読者は長いページの先頭ではなく、該当する段落に移動できます。
- 翻訳済みページを優先。 読者が翻訳済みのロケールにいる場合は翻訳済みの行が優先されます。未翻訳のページも表示されるため、翻訳が途中のサイトでもサイト全体を検索できます。
- 検索結果が何も返さなかったすべての検索の記録(
search.no_resultswebhookおよび検索失敗レポート)。
インデックスはどのように構築されますか?#
プッシュ時ではなく、レンダリング時に書き込みます。 ページはDocsbookがレンダリングした時点でインデックスに追加されます。レスポンスが送信された後に行われるため、インデックス作成によってページの表示が遅れることはありません。翻訳されたページも、翻訳がキャッシュされた時点で同じ方法でインデックスに追加されます。手動で再構築する必要はなく、再インデックスのボタンもありません。
知っておく価値のある結果があります。公開後に誰も開いていないページは、まだインデックスに含まれていません。 そのため、新しいサイトでは、ページが一度訪問されるまで検索結果が少なくなります。まだ行が1件も存在しない間は、検索ボックスはページ一覧からファイル名を照合する処理にフォールバックするため、検索結果が完全に空になることはありません。
1行に含まれる内容:
| フィールド | 内容 |
|---|---|
| タイトル | フロントマターの title、それがなければページのH1、それもなければファイル名 |
| 本文 | ページのプレーンテキスト:フロントマター、見出しマークアップ、画像、リンク構文、フェンス付きコードブロック、インラインMarkdownの句読点を除去したもの |
| セクション | h2/h3 ごとに1エントリ。見出しのレンダリングされたアンカーとテキストを含み、<pre> ブロックは除去 |
| 言語 | 元のページでは空、各翻訳ではロケールコード |
検索は、生成された tsvector に対して実行されます。この列では、タイトルの重みが A、本文の重みが B です。PostgreSQLの重みラベルは、「ドキュメントの異なる部分に含まれる単語を、ランキング関数によって異なる重みで評価できる」ようにするために存在します(PostgreSQL:追加のテキスト検索機能)。この列にはGINインデックスが付与されます。
クエリはどのように回答されますか?#
- ストップワードが除去されます。
websearch_to_tsqueryは「引用符で囲まれていないテキスト用語を&(AND)演算子で結合する」(PostgreSQL: テキスト検索の制御)ため、完全な文ではページにすべての機能語も含まれている必要があります。45語からなる英語のストップワードリストが、引用符の外側のみで最初に除去されます。そのため、"exact phrase"、OR、-wordは引き続き機能します。 - 英語のクエリは語幹処理されます。 英語および未翻訳のコンテンツでは、クエリとドキュメントは
english設定に基づいて照合されます。この設定は、Snowballステマーを使用して「単語の一般的な変化形を基本形、つまり語幹のつづりに縮約する」(PostgreSQL: 辞書)ものです。これがない場合、「served」と記載されたページは「serve」というクエリには一致しません。 - その他の言語では保存されたインデックスが使用されます。 英語以外のロケールでは、保存された
simpleベクトルに対して照合が行われます。これは「入力トークンを小文字に変換することで動作」し、語幹処理は行いません。英語の語幹処理規則をラテン文字以外のテキストに適用すると意味をなさない結果になるため、意図的に適用していません。 - 英語の処理では長さに応じてランクが正規化されます。 英語のクエリでは、
ts_rankが正規化フラグ1で実行されます。これは「ランクを1にドキュメント長の対数を加えた値で割る」処理です。これがないと、すべてのクエリ用語に軽く言及している76 KBの変更履歴が、実際にはその質問について書かれた短いページより上位になります。英語以外の処理では、ts_rankが正規化フラグなしで呼び出されるため、それらのロケールでは長いページでも長さによるペナルティを受けません。詳しくは「制限」を参照してください。 - ページごとに1行です。 原文と翻訳は1つの結果にまとめられ、読者の言語が優先され、次にランクが優先されます。タイトルに一致する結果は、本文のみに一致する結果より上位に表示されます。
- スニペットはサーバー側で生成されます。
ts_headlineは一致箇所の前後5~18語からなる1つの断片を返し、クライアントが用語を再度ハイライトします。
タイプミスがあるとどうなりますか?#
何も一致しません。 Docsbook の検索には、あいまい一致、トライグラム類似度、編集距離によるフォールバックはありません。ステミングは語形変化をカバーするため、serve で served が見つかりますが、スペルミスには対応しません。documnetation では何も見つかりません。
これは見落としではなく、意図的なトレードオフです。その代わりとなる仕組みとして、結果がゼロ件のクエリはすべて通知されます。 通知はまとめられるため、1回の検索操作が1つのシグナルとなり、キー入力のたびに通知されることはありません。
- ブラウザーは、読者が入力を止めてから1.5秒待ってから検索失敗を通知します。また、検索途中でダイアログを閉じた場合はすぐに送信します。実際のワークスペースで測定したところ、1語を入力する読者は文字入力の間に0.9~1.3秒 pause しており、この仕組みがなければ1語につき8件の通知が生成されていました。
- サーバーは、同じ読者から短い時間内に送られた、直前の検索結果の厳密な前方一致拡張となる検索失敗を、独立して抑制します。エンドポイントは公開されているため、クライアント側で何らかのデバウンス処理が行われたとは限らないからです。
したがって、documnetation が検索失敗レポートに届くことは、検索のバグではありません。これは、読者がページを見つけられなかったことを検索が伝えているのであり、そこから対策を講じることができます。繰り返し発生するスペルミスは、読者が実際に入力する用語をコンテンツ内で使用することで修正するのが最善です。
検索ボックスの配置場所#
| 配置場所 | 最適な対象 | トレードオフ |
|---|---|---|
| ヘッダーボタン | 最初にトップバーを見る初回訪問者 | ヘッダーリンクとスペースを取り合う |
| サイドバーボックス | すでにツリーでナビゲーションしている読者 | サイドバーが折りたたまれる狭い画面では非表示になる |
Float Widget → デザイン → ヘッダー → 検索ボタンでヘッダーボタンをオンにします。Float Widget → デザイン → 左サイドバー → サイドバーで検索でサイドバーボックスをオンにします。どちらもすべてのプランで無料です。
これが正しい方法である理由(根拠)#
| ルール | 有効な理由 | 出典 |
|---|---|---|
| セマンティック検索を利用できる場合でも、字句インデックスを保持する | 18を超える検索データセットで、ゼロショット設定において「BM25は堅牢なベースライン」である一方、密な検索器は「しばしば性能が劣る」ことが示されています。コーパスはどの埋め込みモデルにとってもドメイン外であり、技術的な読者が入力するのは正確な用語です | Thakur et al., 2021 — BEIR |
| クエリがPostgresに到達する前にストップワードを除去する | websearch_to_tsquery は引用符で囲まれていない用語をANDで結合するため、ページに存在しない補助語が1つあるだけで、マッチ全体が失敗します |
PostgreSQL: テキスト検索の制御 |
| 英語にはステミングを適用し、それ以外の言語には適用しない | simple 設定は小文字化のみを行います。Snowballステマーは言語ごとに用意されており、異なる語形を1つの語幹にまとめます |
PostgreSQL: 辞書 |
| 文書の長さに応じてランキングを正規化する(英語の処理経路) | フラグ1は「文書の長さの対数に1を加えた値でランキングを割る」ため、話題について短くまとめたページよりも、長い文書内で偶然言及された内容が上位になることを防ぎます | PostgreSQL: テキスト検索の制御 |
| タイトルに本文より高い重みを付ける | 重みラベルを使用すると、ランキングで文書内の異なる部分にある単語を区別して扱えます | PostgreSQL: その他のテキスト検索機能 |
同じインデックスはAIチャットを支える2つの検索器のうちの1つでもあります。そこでも、より劣るフォールバックというわけではありません。
制限#
- タイプミスは許容されません。 上記を参照してください。表記ゆれが読者にとって重要な場合は、その用語をページに追加してください。
- コードブロックは検索対象になりません。 フェンスで囲まれたコードはインデックス作成前に除外され、
<pre>ブロックはセクション本文から削除されます。コードサンプル内にしか登場しない関数名を検索しても、読者は見つけられません。ここは要確認です。以前のドキュメントでは、コードブロックもインデックス化され「本文より下位にランク付けされる」と説明されていましたが、インデクサーはコードブロックを完全に削除します。 - 網羅性はリポジトリではなく、トラフィックに依存します。 ページは初回のレンダリング時にインデックスに登録されます。公開済みでも一度も開かれていないページは、誰かが開くまで検索結果に表示されません。
- レイテンシの数値は公開していません。 英語のクエリでは、保存済みの
simpleインデックスを読み取るのではなく、クエリ時にtsvectorを計算します。つまり、コーパスが大きくなるほど、英語の経路ではクエリごとの処理量が増えます。ベンチマークは公開しておらず、公開できるまでは数値を提示しません。 - 長さの正規化は英語にのみ適用されます。 英語のクエリ経路では
ts_rankの正規化フラグ 1 を渡しますが、他のロケールで使用される経路ではフラグなしでts_rankを呼び出すため、長さの正規化は行われません。そのため、英語以外のサイトでは、クエリとの関連性がより高い短いページよりも、非常に長いページが上位になる可能性があります。2 つの経路が統合されるまでは要確認です。 - 検索はプロジェクト単位です。 インデックスの対象は 1 つのワークスペースに限定され、プロジェクトをまたいだ検索はできません。
- 検索失敗シグナルは正確なものではなく、まとめられます。 入力途中の一連の検索で最初に見つからなかった時点のクエリがライブ webhook に配信されるため、配信されたクエリは、読者が最終的に確定したものより短い接頭辞になる場合があります。履歴レポートでは、確定したクエリを復元できます。
関連#
- AIチャット — このインデックスを検索手段の1つとして使用するアシスタント。
- 回答品質 — 字句検索とベクトル検索の結果を統合する方法。
- アナリティクス:読者が検索した内容 — 何も結果が返されなかったクエリ。
- ページへのフィードバック — ページが存在しない、または名前が間違っていることを示すもう1つのシグナル。