DocusaurusのAlgolia検索を単一インデックス構成で精度改善した備忘録
HKdocsのサイト内検索で「新しい記事はヒットしないのに、無関係な記事は誤爆する」という精度低下が発生しました。原因は Algolia のチューニング不足ではなく、フロントエンドが検索するインデックスと、クローラーが更新するインデックスが食い違っていたという構造的な問題でした。
この記事では、その原因の切り分けから、単一インデックス構成へ統合して精度を回復させるまでの手順を記録します。多言語サイトでも言語別インデックスへの分割は不要であり、contextualSearch の正しい挙動を理解することが解決の起点になります。
contextualSearch の正しい挙動Docusaurusの contextualSearch: true は indexName を変更しません。単一インデックスに対し、クエリ時に facetFilters(language:<locale> など)を付与して言語を絞り込む仕組みです。したがって多言語サイトでも言語別インデックスへの分割は不要で、単一インデックス + facet で言語の出し分けが成立します。言語別のチューニングも ignorePlurals: ["en"] のような言語配列指定によって、単一インデックス内で完結できます。
診断:精度低下の切り分け
検索精度が低下したときは、細かなチューニングに入る前に、まずフロントの検索先とクローラーの更新先が一致しているかを疑うのが近道です。具体的には次の3点を確認します。
docusaurus.config.tsのalgolia.indexName(フロントエンドが検索する先)。- Crawlerの
actions[].indexName(クローラーが更新する先)。 - 各インデックスの最終更新日時とレコード数(Algoliaダッシュボード)。
indexName が一致していない、あるいはフロントの検索先の更新が止まっている場合、フロントエンドは古い(stale な)インデックスを検索し続けています。「新しい記事がヒットしない/古い無関係な記事が誤爆する」という症状は、まさにこのサインです。
言語別インデックスへ分割したまま docusaurus.config.ts の indexName を分割前の名前に据え置くと、クローラーは分割後のインデックスを更新し続ける一方で、フロントエンドは誰も更新しない旧インデックスを検索し続けます。クローラー自体は正常に走り、ダッシュボードのレコード数も健全に見えるため、「フロントが参照していない」という一点だけがブラックボックス化し、精度低下として遅れて顕在化します。
解決方針:単一インデックスへの統合
方針はシンプルです。クローラーを単一インデックスに統合し、フロントエンドの検索先と更新先を一致させます。 言語の出し分けは contextualSearch: true(language facet)が担うため、docusaurus.config.ts 側の変更は不要です。この統合と同時に、ノイズ除去と言語チューニングもまとめて実施しました。
変更内容の全体像は次のとおりです。
| 項目 | 推奨設定 | 狙い |
|---|---|---|
| indexName | 単一名に統合 | 参照先と更新先を一致させ鮮度を回復 |
| exclusionPatterns | tags / authors / category / page / search 等を除外 | 薄いページのノイズ除去 |
| content | article p, li, td に article pre を追加 | コードブロック内も検索対象に |
| ignorePlurals | ["en"] | 単一インデックスで英語のみ複数形を同一視 |
| indexLanguages / queryLanguages | ["ja", "en"] | 日本語の辞書分割(複合語の精度向上) |
| minWordSizefor1Typo / 2 | 4 / 8 | 日本語での過剰なタイポ一致を抑制 |
実装ファイルと手順の詳細
以下、実際に適用した設定と手順のハイライトです。
1. クローラー設定(単一インデックスへの統合)
actions を単一インデックスに集約し、exclusionPatterns でノイズページを除外します。言語の判別は recordExtractor 内で lvl0 のデフォルト値を出し分けるためだけに使い、インデックス自体は分割しません。
new Crawler({
appId: "YOUR_APP_ID",
apiKey: "YOUR_CRAWLER_API_KEY", // クローラー用の管理者APIキー
maxUrls: 5000,
indexPrefix: "",
rateLimit: 8,
renderJavaScript: true,
startUrls: [
"https://your-docusaurus-site.com/",
"https://your-docusaurus-site.com/en/",
],
discoveryPatterns: ["https://your-docusaurus-site.com/**"],
sitemaps: ["https://your-docusaurus-site.com/sitemap.xml"],
// --- ノイズページの除外 ---
exclusionPatterns: [
"https://your-docusaurus-site.com/**/tags/**",
"https://your-docusaurus-site.com/**/authors/**",
"https://your-docusaurus-site.com/**/page/**",
"https://your-docusaurus-site.com/docs/category/**",
"https://your-docusaurus-site.com/en/docs/category/**",
"https://your-docusaurus-site.com/search/**",
"https://your-docusaurus-site.com/en/search/**",
],
schedule: "at 21:10 on Tuesday",
maxDepth: 10,
// --- 単一インデックスへ統合 ---
actions: [
{
indexName: "yoursite", // 1本に統合
pathsToMatch: ["https://your-docusaurus-site.com/**"],
recordExtractor: ({ url, $, helpers }) => {
const isEn =
url.pathname === "/en" || url.pathname.startsWith("/en/");
const lvl0 =
$(
".menu__link.menu__link--sublist.menu__link--active, .navbar__item.navbar__link--active",
)
.last()
.text() || (isEn ? "Documentation" : "ドキュメンテーション");
return helpers.docsearch({
recordProps: {
lvl0: { selectors: "", defaultValue: lvl0 },
lvl1: ["header h1", "article h1"],
lvl2: "article h2",
lvl3: "article h3",
lvl4: "article h4",
lvl5: "article h5, article td:first-child",
lvl6: "article h6",
// コードブロック(article pre)も検索対象に含める
content:
"article p, article li, article td:last-child, article pre",
},
aggregateContent: true,
recordVersion: "v3",
});
},
},
],
ignoreCanonicalTo: true,
safetyChecks: { beforeIndexPublishing: { maxLostRecordsPercentage: 10 } },
});
helpers.docsearch が自動付与する language フィールドだけで、contextualSearch の facet 絞り込みは成立します。lang フィールドを自作する必要はありません。
2. インデックス設定(言語チューニング)
言語チューニングは、単一インデックス内で言語配列を指定して行います。indexLanguages: ["ja", "en"] を指定すると日本語が辞書分割され、複合語の検索精度が向上します。
{
ignorePlurals: ["en"], // 英語のみ複数形を同一視
indexLanguages: ["ja", "en"], // 日本語の辞書分割を有効化
queryLanguages: ["ja", "en"],
minWordSizefor1Typo: 4, // 日本語での過剰なタイポ一致を抑制
minWordSizefor2Typos: 8,
attributesForFaceting: ["type", "language", "docusaurus_tag"],
searchableAttributes: [
"unordered(hierarchy.lvl0)",
"unordered(hierarchy.lvl1)",
"unordered(hierarchy.lvl2)",
"unordered(hierarchy.lvl3)",
"unordered(hierarchy.lvl4)",
"unordered(hierarchy.lvl5)",
"unordered(hierarchy.lvl6)",
"content",
],
customRanking: [
"desc(weight.pageRank)",
"desc(weight.level)",
"asc(weight.position)",
],
removeWordsIfNoResults: "allOptional",
}
initialIndexSettings は既存インデックスに自動適用されないinitialIndexSettings はインデックスの新規作成時にのみ適用されます。既存のインデックスへ設定を反映するには、setSettings(RESTでは PUT .../settings)で直接投入する必要があります。
3. フロントエンド設定(変更不要)
contextualSearch: true を有効にしておけば、indexName は単一名のままで問題ありません。言語の出し分けは facet によって自動で行われます。
themeConfig: {
algolia: {
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_SEARCH_ONLY_API_KEY', // 検索専用キー
indexName: 'yoursite', // 単一名でよい
contextualSearch: true, // language facet で言語を絞り込む
},
},
4. Crawler REST API での適用
管理UIの "Review and publish" は、Domain Verificationが無効(DocSearch Statusが空欄)の場合に使用できないことがあります。その際は、Crawler REST APIから本番configを直接更新します。この経路はUIのゲートを通らず、既存の定期クロールもこのAPI設定で動作します。
# 認証: --user "CRAWLER_USER_ID:CRAWLER_API_KEY"
# 設定取得
GET https://crawler.algolia.com/api/1/crawlers/{crawlerId}?withConfig=true
# 設定更新(actions を単一インデックスへ統合 + exclusionPatterns 追加)
PATCH https://crawler.algolia.com/api/1/crawlers/{crawlerId}/config
# 索引設定(言語チューニングを直接投入)
PUT https://{APP_ID}.algolia.net/1/indexes/yoursite/settings
# ヘッダ: X-Algolia-API-Key: <索引用の管理APIキー>
# 再クロール(設定適用の前後で実行)
POST https://crawler.algolia.com/api/1/crawlers/{crawlerId}/reindex
recordExtractor は {"__type":"function","source":"..."} の形式で送る必要があります。プレーンな文字列で送ると validation_error になります。また、設定変更後は必ず reindex を実行し、公開検索キーで実クエリしてBefore / Afterを検証してください。
運用上の注意
安全に運用するため、次のルールを守っています。
まず、UIからは publish しません。UIエディタには分割時代の旧コードが残っている場合があり、そこから publish すると改善が巻き戻ってしまうためです。設定変更はすべてAPI経由で行います。
次に、作業に使ったCrawler APIキーと索引用の管理APIキーは管理権限を持つため、作業後にローテーションします。最後に、変更後は公開検索キーで代表的なクエリを投げ、意図した記事が上位に来ること、および除外パターンが0件になることを確認します。
まとめ
検索精度の低下に直面したときは、細かなチューニングよりも先に、フロントの参照先とクローラーの更新先が一致しているかを確認するのが有効でした。contextualSearch は indexName を変更しないため、多言語サイトでも単一インデックス + language facet で言語の出し分けが成立し、言語別インデックスへの分割は基本的に不要です。言語別のチューニングも ignorePlurals: ["en"] や indexLanguages: ["ja", "en"] のような言語配列指定によって、単一インデックス内で十分に実現できました。
関連記事
- DocusaurusにAlgolia DocSearchを組み込む方法 — Algolia検索の導入手順。
- Docusaurus多言語サイトでAlgolia検索を言語別に最適化する方法 — 本記事で訂正した「言語別インデックス分割」の当初手順。
参考文献
- Docusaurusソース:
theme-search-algolia(useAlgoliaContextualFacetFilters) - Docusaurus公式: Contextual search
- Algolia:
facetFilters/indexLanguages/setSettings - Algolia: Crawler REST API