mdBookを日本語検索に対応させたかった

# はじめに
mdBookとはRust製のドキュメントビルダーで、Markdownのドキュメントをいい感じのHTMLなどにレンダリングできます。
Rustの公式ドキュメント・非公式ドキュメントで広く使われているのですが、現状日本語での検索ができません。
なぜ日本語で検索できないかを少し調べてみたところ、各コンポーネントは微妙に日本語対応しているものの、
全体として整合性が取れる状態にはなっていない、という感じでした。
今回、いろいろなものにパッチを当てることで、「日本語検索できるように見える」状態まで持ってくることができました。
また、完全な日本語検索に向けての課題も見えてきたので共有しておきます。

# mdBookの検索

まず検索周りで使われているコンポーネントは以下のようになっています。

* [mdBook](https://github.com/rust-lang/mdBook): mdBook本体
* [elasticlunr-rs](https://github.com/mattico/elasticlunr-rs): mdBookから呼んでいるelasticlunr.js用のインデックス生成クレート
* [elasticlunr.js](https://github.com/weixsong/elasticlunr.js): JS製の全文検索エンジン

さらに日本語検索にかかわるコンポーネントが以下です。

* [lunr-languages](https://github.com/weixsong/lunr-languages): elasticlunr.jsの多言語対応モジュール

検索の流れは以下の通り。

* `mdbook build`にてmdBookがelasticlunr.js互換のインデックスファイル（searchindex.js）を生成する
* ブラウザで読んだときにsearcher.jsが上で生成したsearchindex.jsをロードして、elasticlunr.jsで検索する

# パッチを当てて日本語検索させる

## mdBook

まずmdBook本体からです。

```shell
$ git clone https://github.com/rust-lang/mdBook
```

として2つのファイルにパッチを当てます。
1つ目は以下の通りCargo.tomlにてelasticlunr-rsに`ja`フィーチャを追加します。

```diff
diff --git a/Cargo.toml b/Cargo.toml
index e0a2b90..2f28447 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,7 +46,7 @@ staticfile = { version = "0.5", optional = true }
 ws = { version = "0.9", optional = true}

 # Search feature
-elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
+elasticlunr-rs = { version = "2.3", optional = true, default-features = false, features = ["ja"] }
 ammonia = { version = "3", optional = true }

 [dev-dependencies]
```

2つ目は`elasticlunr::Index`を生成しているところで、`Language::Japanese`を指定します。

```diff
diff --git a/src/renderer/html_handlebars/search.rs b/src/renderer/html_handlebars/search.rs
index f184a59..9cc7519 100644
--- a/src/renderer/html_handlebars/search.rs
+++ b/src/renderer/html_handlebars/search.rs
@@ -2,7 +2,7 @@ use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
 use std::path::Path;

-use elasticlunr::Index;
+use elasticlunr::{Index, Language};
 use pulldown_cmark::*;

 use crate::book::{Book, BookItem};
@@ -13,7 +13,7 @@ use crate::utils;

 /// Creates all files required for search.
 pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
-    let mut index = Index::new(&["title", "body", "breadcrumbs"]);
+    let mut index = Index::with_language(Language::Japanese, &["title", "body", "breadcrumbs"]);
     let mut doc_urls = Vec::with_capacity(book.sections.len());

     for item in book.iter() {
```

この状態でビルドして`mdbook`コマンドをインストールします。
（ローカルで`mdbook`を使っている方は上書きしてしまうのでご注意ください）

```console
$ cargo install --path .
```

## ビルド

先ほど改造した`mdbook`を使って普通にビルドします。

```console
$ cd XXX
$ mdbook build
```

## lunr-languagesの導入と改造

lunr-languagesから以下のファイルを落としてきて、`mdbook`が生成した`docs`直下に置きます。

* [lunr.stemmer.support.js](https://github.com/weixsong/lunr-languages/blob/master/lunr.stemmer.support.js)
* [tinyseg.js](https://github.com/weixsong/lunr-languages/blob/master/tinyseg.js)
* [lunr.jp.js](https://github.com/weixsong/lunr-languages/blob/master/lunr.jp.js)

さらにlunr.jp.jsに以下のようにパッチを当てます。

```diff
99c99
<     lunr.Pipeline.registerFunction(lunr.jp.stemmer, 'stemmer-jp');
---
>     lunr.Pipeline.registerFunction(lunr.jp.stemmer, 'stemmer-ja');
109a110,113
>
>     lunr.jp['wordCharacters'] = '一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーｱ-ﾝﾞa-zA-Zａ-ｚＡ-Ｚ0-9０-９';
>     lunr.jp.trimmer = lunr.trimmerSupport.generateTrimmer(lunr.jp.wordCharacters);
>     lunr.Pipeline.registerFunction(lunr.jp.trimmer, 'trimmer-ja');
```

さらにindex.htmlの最後の方のelasticlunr.min.jsを読んでいる部分に以下のように追記します。

```html
        <script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="searcher.js" type="text/javascript" charset="utf-8"></script>
        <script src="lunr.stemmer.support.js" type="text/javascript" charset="utf-8"></script>
        <script src="tinyseg.js" type="text/javascript" charset="utf-8"></script>
        <script src="lunr.jp.js" type="text/javascript" charset="utf-8"></script>            
```

これでとりあえずindex.htmlから日本語検索できるようになりました。

## 検索結果の例

例えば手元にある"Rust by Example 日本語版"だと以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84296/8b08c3d6-a848-e053-155b-3a12f78bad25.png)

# いろいろな不具合

とりあえずこの状態で日本語検索できるように見えますが、いろいろと不具合があります。

* 検索結果のハイライトがおかしい。おそらく英語前提で半角スペース区切りを元にハイライトを決めているので。
* 3文字以上の漢字が検索できない。例えば「構造」はOKだが「構造体」はNG、といった感じ。詳しい条件は不明。
* 英語が検索できない。

特に3つ目はプログラミング系のドキュメントの検索としては割と致命的ですが、直すのは結構大変そうです。

# 完全な日本語検索に向けて

完全な日本語検索に向けて、以下のようなことをやればいいのではないかというリストを挙げておきます。

## lunr-languagesの[PR](https://github.com/weixsong/lunr-languages/pull/1)をマージ

lunr.jp.jsで当てたパッチは1年ほど前にPRが出ているものの放置されているものです。
すでにelasticlunr.js/lunr-languagesの作者の方はあまりアクティブではないようで、
elasticlunr.jsをOrganization管理にしたいという[Issue](https://github.com/weixsong/elasticlunr.js/issues/115)が立っています。
正規の手順を踏むなら、できたOrgにlunr-languagesも移動、当該PRをマージしてもらう、という感じがいいでしょうか。

## elasticlunr-rs と lunr-languagesの日本語名をそろえる

現状elasticlunr-rsで日本語は`ja`、lunr-languagesでは`jp`となっています。
lunr.jp.jsで当てたパッチの一部はこの不整合を解消するためでした。
分かれている経緯が不明なので、どちらに寄せるべきかはよく分かりません。

（追記：2020/6/23）
某所でご指摘いただいたのですが、言語コードとしては`ja`が適切なのではないか？とのことでした。
https://ja.wikipedia.org/wiki/ISO_639-1%E3%82%B3%E3%83%BC%E3%83%89%E4%B8%80%E8%A6%A7

そのあと気づいたのですが、`ja`に変更するのが正しいですね。
ここで指しているlunr-languagesはelasticlunr.js用にフォークしたものなのですが、
フォーク元の本家lunr-languages（こちらはlunr.js用）では`ja`と`jp`両方あって、
もともと`jp`だったのを言語コード的に正しい`ja`に変更した、となっています。（`jp`は互換性のために残してある）
なのでelasticlunr.js用lunr-languagesも本家に追従することでこの問題を解決するのが正しそうです。

## elasticlunr-rsに多言語対応を入れる

elasticlunr.jsとしては多言語（つまり英語＋日本語）対応しているようですが、
elasticlunr-rs側が（[Issue](https://github.com/mattico/elasticlunr-rs/issues/13)は立っているのですが）対応していません。
基本的には多言語時のelasticlunr.jsのインデックスの構造を調べて、そのように出力すればいいはずですが…。

## mdBookに検索言語選択機能を入れる

おそらくbook.tomlの設定にいれるのがいいでしょう。
ただし日本語検索のために結構依存クレートが増えるので、フィーチャは切った方がいいかもしれません。
変更箇所は、パッチを当てた`Index`の部分と、HTML/JSの生成部分です。

# まとめ

というわけでmdBookで日本語検索するにはいろいろとハードルがあることが分かりました。