Qiitaの検索結果がおかしかったので、調査したらElasticsearchのハイライターに辿り着いた

## はじめに
QiitaとQiita Teamでは記事検索でElasticsearchを使っています。
今回はQiitaの検索機能に関して、サポートで不具合報告をいただき調査・修正を行いました。
その一部始終をまとめてQiita Teamに投稿したところ、「これQiitaにも投稿しましょう！」とPMに言ってもらったのでQiitaに記事を出させてもらいます。

## そもそも不具合とはどういうものだったのか
Qiitaで"[するために、1からReact](https://qiita.com/search?q=%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E3%80%811%E3%81%8B%E3%82%89React)"というフレーズで調べた時、[筋肉にちゃんと「お願いマッスル!」するために、1からReact学んでアプリをリリースした話](https://qiita.com/sararilfy/items/c623dff2c8ba77f394d1)という記事が結果に表示されます。
これ自体は全くもって正しい挙動なのですが、検索結果のページではタイトルが"するために、1からReact学んでアプリをリリースした話"になっています（↓の当時のスクリーンショット参照、現在は修正されています）。

![スクリーンショット 2021-06-08 5.54.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359721/b61b5dcb-b119-86f6-65f7-01ab902cc701.png)

調べてみると、"!"や"?"などの一部の半角記号で起きているようでした。

## ひとまず検索した時の返ってきたjsonを見てみる
まず、Qiitaの開発環境で"筋肉にちゃんと「お願いマッスル!」するために、1からReact学んでアプリをリリースした話"という記事を作成し、"するために、1からReact"というフレーズで調べてみました。
するともちろん、上記のスクリーンショットのような、タイトルが一部切れた記事が表示されます。
ひとまず、クエリを投げた後返ってくるjsonがおかしいのか、それともかえってきたjsonは正しくてその後何らかの処理でタイトルが切れてしまっているのか問題の切り分けをしたいと考え、開発環境で返ってきたjsonを覗いてみました。すると・・・（下記一部抜粋）

```json
"highlight":{
  "title":[
    "<em>する</em><em>ため</em>に、<em>1</em>から<em>React</em>学んでアプリをリリース<em>し</em>た話"
  ]
}
```

はい、Elasticsearchから帰ってきた時点でおかしくなっています。
この時点で問題は投げてるクエリが悪いか、データ（の入れ方）が悪いということになるなと当時の自分は考えていました。


## 逆にオミットされた部分がひっかかるように検索するとどうなるのか？
入ってるデータがおかしいのであれば、文字が切れる前、つまり先述の記事であれば"筋肉にちゃんと"で検索すれば結果がおかしくなるのではないかと思い、[実際にQiitaで調べてみました](https://qiita.com/search?sort=&q=%E7%AD%8B%E8%82%89%E3%81%AB%E3%81%A1%E3%82%83%E3%82%93%E3%81%A8)。

下記の画像が当時の検索結果なのですが、きちんと正しいタイトルで表示されています。
入ってるデータは正しそうです。この時点でクエリが悪いのだろうなと思いつつも、どれが悪いのかまだ見当がついていませんでした。

![スクリーンショット 2021-06-08 6.13.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359721/d83ef557-c6c8-3253-871a-3b515f1102fd.png)


## Qiita Teamの開発環境で再現するか試してみた
先述の通り、Qiitaと同様にQiita Teamでも記事検索機能を使っています。しかし、マッピングや検索のクエリにそこそこの差異があります。
そこで、Qiita Teamの開発環境にも同様に記事を作り、検索をしてみました。
すると、Qiitaの開発環境で先頭の文字が切れた状態でしたが、Qiita Teamでは正しく表示されていました（↓の画像参照）。

![スクリーンショット 2021-06-08 5.56.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359721/c75ceff1-63bd-b435-5c04-82991b469941.png)

QiitaとQiita Team、何で結果に違いが出たのか検索クエリの差分を取ってみると、どうやらハイライターオプションがおかしいぞということが分かってきました。

## ハイライターとは
今回の場合では、もちろん記事ページのどこがクエリに引っかかったのかわかりやすくするものですね。
下の図では"筋肉"と"ちゃんと"が太くなっています（ちなみにQiita Teamでは上の画像のように黄色く表示してくれます）。

![スクリーンショット 2021-06-08 6.13.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359721/d83ef557-c6c8-3253-871a-3b515f1102fd.png)

## Elasticsearchのハイライターオプション
ではElasticsearchのハイライターにはどのような種類があるのでしょう？
結論から言うと、下記の3種類が存在します。

- unified highlighter
- plain highlighter
- fast vector highlighter(fvh)

詳細は、[公式ドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/highlighting.html)や日本語であれば[こちらの記事](https://tech.mntsq.co.jp/entry/2021/03/30/141825)を読んでいただくと分かりやすいかと思います。

当時、記事検索でQiitaではplain highlighterを、Qiita Teamではfvhを利用していました。
Qiita Teamではkuromojiによる形態素解析マッチと、ngramを使ったマッチの二つを利用しているため、両方のハイライトを混ぜたものが欲しいとなるとfvhしか選択肢はありませんでした。(元々Qiita Teamではunified highlighterでしたが、[アップデートをした時](https://qiita.com/WakameSun/items/4d46f99a03f2abdc9eba)についでに変更しました)。

「じゃあQiitaもfvhに変えればいいじゃん」、と思われるかもしれませんが、少なくとも現時点ではマッピングの都合上fvhオプションを使うことはできませんでした。reindexすることが必要で、サッと直したいと考えた時には工数が大きそうです。
そこで、今回はここまで話には出てこなかった、もう一つのハイライターであるplain highlighterを使ってみることにしました。


## 実際に改善するか検証してみる
ではQiitaの開発環境で、実際にハイライターをplain highlighterにして先ほどと同じ検索をしてみます。
はい、下のスクリーンショットを見ていただくか、[実際に検索をしていただく](https://qiita.com/search?q=%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E3%80%811%E3%81%8B%E3%82%89React) とわかると思いますが、きちんと欲しかったハイライトが返ってきています。

![fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359721/278b4add-84d2-5818-7c44-5acceb3c0321.png)


plain highlighterは検索時間が他のハイライターよりかかってしまうため、本番に上げた時パフォーマンスの悪化が懸念でしたが現時点ではきちんと動いています。
将来的にはマッピング定義を変えて、fvhオプションを使うかunifiedオプションでも正しく動くようにしてパフォーマンスの懸念も問題がないようにしたいです。

## これで解決したのかと思いきや・・・
これで解決したーと思っていたら、タイトルがとても長い記事（200文字とか）だと文字が切れている不具合を発見しました。
調べていると、こちらの[Elasticsearchのissue](https://github.com/elastic/elasticsearch/issues/28089)や[エムスリーさんのテックブログ](https://www.m3tech.blog/entry/highlighter-internal)に到達しました。
どうやらfragment_sizeというオプションを触ることで、長くすることもできるようです（そもそも公式ドキュメントにもオプションとして書いてある・・・）。
タイトルに関しては切れることなく全て返して欲しいので、fragment_sizeをタイトルの最大の文字数である２５６に設定することにしました。
また、boundary_charsというオプションで文章を切る文字を設定できるようなので、こちらをタイトルのみ切らないような設定にすることでも回避できそうです。