ドキュメント検索にシノニム・ユースケースキーワードを追加し、コンポーネントの発見性を向上
WebAwesomeのサイト内検索に synonyms(同義語)と use-cases(ユースケース)フロントマターフィールドを追加し、ブーストウェイト付きでMiniSearchインデックスに組み込んだ。これにより「modal」でDialogが、「sidebar」でDrawerが見つかるなど、正式名称を知らないユーザーでも目的のコンポーネントにたどり着けるようになった。
背景
WebAwesomeのコンポーネントは独自の命名規則を持つが、開発者が直感的に思い浮かべる名称と一致しないことがある。たとえば、モーダルウィンドウを探したいユーザーが「modal」と入力しても、コンポーネント名が「Dialog」であるため検索結果に表示されなかった。
PRに添付された変更前後の比較表が問題の規模を示している:
| クエリ | 変更前 | 変更後 |
|---|---|---|
| sidebar | 結果なし | Drawer |
| accordion | Format Date | Details |
| lightbox | 結果なし | Dialog |
| combobox | Popup | Select |
| offcanvas | 結果なし | Drawer |
| loader | Installation | Spinner |
こうした検索ミスマッチは、コンポーネントの発見性を損ない、ユーザー体験に直結する問題だった。
技術的な変更
フロントマターへのキーワード定義
89のドキュメントファイルに、2つの新しいフロントマターフィールドが追加された。synonyms はコンポーネントの別名、use-cases はそのコンポーネントを使う動機・文脈を記述する。Dialogコンポーネントを例にとると、以下のように定義される:
synonyms:
- modal
- popup
- lightbox
- overlay
- modal dialog
use-cases:
- confirmation dialog
- alert dialog
- prompt
- login modal
- cookie consent
Drawerには sidebar、offcanvas、tray、sheet といった同義語が、Detailsには accordion、collapsible、disclosure が割り当てられている。
検索プラグインの改修(_plugins/search.js)
インクリメンタルビルドのキャッシュ機構に変更が加わった。従来、pagesToIndex マップは true(未処理)またはページデータそのもの(処理済み)を格納するシンプルな構造だったが、新フィールドを保持するためにオブジェクト形式に変更されている。
変更前:
// プリプロセッサ
pagesToIndex.set(data.page.inputPath, true);
// トランスフォーム(キャッシュ利用判定)
if (pagesToIndex.get(key) === true) {
pagesToIndex.set(key, value);
}
変更後:
// プリプロセッサ: フロントマターデータへのアクセス権があるここでキーワードをキャッシュ
pagesToIndex.set(data.page.inputPath, {
pending: true,
synonyms: data.synonyms || [],
useCases: data['use-cases'] || [],
});
// トランスフォーム(キャッシュ利用判定)
const current = pagesToIndex.get(key);
if (current?.pending) {
pagesToIndex.set(key, { ...value, synonyms: current.synonyms, useCases: current.useCases });
}
Eleventyのビルドパイプラインでは、プリプロセッサフックはフロントマターデータにアクセスできるが、トランスフォームフックはアクセスできない。そのため、プリプロセッサ段階でキーワードを pagesToIndex にキャッシュし、トランスフォームで HTML コンテンツとマージする2段階処理が導入された。インクリメンタルビルド時に全ファイルがトランスフォームを通過しない制約を回避するため、pending フラグによって「まだトランスフォーム未処理のページ」を識別し、キャッシュ済みデータにキーワードを上書きマージする仕組みになっている。
検索フロントエンドの改修(assets/scripts/search.js)
MiniSearchの設定に2フィールドを追加し、ブーストウェイトを設定している:
変更前:
const searchIndex = MiniSearch.loadJSON(JSON.stringify(searchData.searchIndex), {
fields: ['t', 'h', 'c'],
});
// ...
boost: { t: 20, h: 10 }
変更後:
const searchIndex = MiniSearch.loadJSON(JSON.stringify(searchData.searchIndex), {
fields: ['t', 'h', 's', 'u', 'c'],
});
// ...
boost: { t: 20, s: 14, h: 10, u: 6, c: 1 }
短縮フィールド名は t(title)、h(headings)、s(synonyms)、u(use-cases)、c(content)に対応する。
設計判断
ブーストウェイトの階層化が本PRの設計の核心である。5段階のウェイト(20→14→10→6→1)を設定することで、既存の検索ランキングを壊さずに新しいキーワードを適切な優先度で挿入している。
synonymsとuse-casesを別フィールドとして扱う点も重要な設計判断だ。synonymsのブースト14は「タイトル直接一致(20)」よりは低いが「見出し一致(10)」より高い位置に置かれ、「この語はまさにこのコンポーネントの別名である」という強い対応関係を表現する。一方、use-casesのブースト6は「このコンポーネントで解決できる問題」という間接的な関連性を反映し、本文コンテンツ(1)よりは高く評価される。この分離により、「modal」という検索ではDialogが上位に来るが、「cookie consent」という検索でも適切にDialogが見つかるという多層的な発見性が実現されている。
キーワードをフロントマターに定義する方式は、コンポーネントの担当者がドキュメントと一体として管理できるという利点がある。検索インデックスの設定ファイルと切り離すことで、新しいコンポーネントを追加する際も同じフローでキーワードを定義できる。
まとめ
本PRは検索エンジンの精度向上ではなく、「コンポーネントの意味的な発見性」をドキュメントのフロントマターとして定義可能にした変更である。プリプロセッサ・トランスフォームの2段階キャッシュ機構によりEleventyのインクリメンタルビルド制約を回避しつつ、MiniSearchのブーストウェイト階層化によって既存の検索体験を壊さずに新次元のキーワードマッチングを追加している。