Elasticsearch の analyzer 関連の設定で知ってることを全て書く
詳しい人から見れば大した内容じゃないと思うけど、調べたり試行錯誤した結果をまとめる。(間違いなどがあれば、ご指摘頂けるとありがたいです。)
Elasticsearch を何に使っているか
他サービス → API/webhook → 自サーバーの Elasticsearch
以前触れたと思うけど、開発プロジェクトのデータを全部1箇所にまとめて検索出来るようにしていて、そこで Elasticsearch を使っている。
自分の仕事内容としては、時間の4割位を受託開発にあてている。受託開発では、お客様や発注元の開発ベンダーに合わせて色んなツールを使わざるを得なくて、具体的には ChatWork, Backlog, Google Drive とかを使うことが結構多い。それに対して、自分達の開発チーム内部では自分達用のSlackチームがあるし、それ以外にも別のツールを使ってたりしているので、色んなところに情報が分散しがち。
なので、各ツールのAPI(あるいはwebhook)経由で自分達のサーバーにデータを送って、それを Elasticsearch に流し込んでいる。
検索対象
元データの種類は以下の通り。
- テキスト
- GitHub/Bitbucket/Backlog の issue (チケット), PR での議論
- Slack, ChatWork の会話
- wiki ページ
- ファイル
- Backlog のファイル置き場にあるファイル
- Google Drive のファイル
- Slack に貼られたファイル
言語は、日本語がメインだけど、海外の人とのやりとりや、英語のwebページ(StackOverflowとか)からのコピペもあるので、英語もある程度使われている。
全般的な話
Elasticsearch のインストールとか基本的な設定とかは、色んなサイトで書かれているので、ここでは触れない。今回は、analyzer の設定周りについて書く。
その前に、参考にしたサイト
- 本家のリファレンス: かなり詳しく書いてあるので、疑問点はまずはこちらをあたるべき
- Hello! Elasticsearch: 検索するとこのサイトが結構ひっかかるのでお世話になっている方も多いはず
- その他、色々検索して、色んな方のブログ記事とかを読んだ
環境とか
- Elasticsearch 2.3.5
- 使用しているアナライザー
- Japanese (kuromoji) Analysis Plugin
- Elasticsearch Analysis Kuromoji Neologd (kuromoji +新しい語彙が大量に入った辞書、みたいなやつ)
- ICU Analysis Plugin
- Mapper Attachment Type Plugin (tika でテキストを抜き出してインデックスに登録)
- elastic4s というライブラリ経由で Scala から使用
データ量もユーザー数も少ないので、負荷とかはあまり考慮していない。
実際の設定
まずは設定内容などから晒して、その後、調べたこととかについて書いていく。
analyzer の設定
elasticsearch.yml ではいくつか analyzer を定義しているが、メインで使っている analysis-kuromoji-neologd を使ったものを以下に書く。
analyzer: ja_kuromoji_neologd: type: custom tokenizer: kuromoji_neologd_tokenizer char_filter: [ icu_normalizer, html_strip, kuromoji_neologd_iteration_mark ] filter: [ kuromoji_neologd_stemmer, kuromoji_part_of_speech, kuromoji_neologd_baseform, icu_normalizer, ja_stop ]
各 char filter, (token) filter については、このページに分かりやすく説明してある。
で、この analyzer の設定は、同サイトのこちらのページの内容をベースにしているが、若干違う点もあるので、後ほど説明。
スキーマ定義
現状は、インデックス作成時に言語を固定している。今後は多言語化したいが、それも後述する。
以下、elastic4s でのスキーマ定義からの抜粋だが、DSL なのでまぁ大体内容は分かると思う。
val messageMapping = mapping("message").fields( "title" typed StringType analyzer "ja_kuromoji_neologd", "body" typed StringType analyzer "ja_kuromoji_neologd", "attached_files" nested ( "attached_file" typed AttachmentType fields ( "content" typed StringType analyzer "ja_kuromoji_neologd" termVector ("with_positions_offsets") store (true) ) ) includeInRoot (true) )
テキストフィールドの検索
以下のような検索文字列があったとする。
“google drive” 連携 設定
それを以下の3つに分解し、
- 「google drive」の phrase query
- 「連携」の match query
- 「設定」の match query
それを boolean query でまとめてる。
添付ファイルの検索
添付ファイルの検索も、基本的にはテキストフィールドの検索と同じ。ただ、添付ファイルは上述のスキーマ定義の通り nested なので、クエリーの投げ方が若干違う。詳しくは、リファレンスを参照。
添付ファイルのハイライト
検索にヒットした場合は、ヒットした部分をハイライト表示したいが、添付ファイルの場合(かつ nested の場合?)、普通にやるとうまくいかなかったので、highlight query というのを使った。
調べた事とか
使用している char filter, token filter に関して
上の analyzer 設定は、こちらのページの内容と若干異なる。主な違いは以下の通り。
- kuromoji_* -> kuromoji_neologd_*
- lowercase, cjk_width (共に token filter) を使わないで icu_normalizer char filter を使用
- kuromoji_neologd_baseform を使用
- icu_normalizer token filter を最後にもう一度使用
最初の kuromoji_neologd_* に関しては、特に説明することもないので飛ばして、残りの3つについて書く。
lowercase, cjk_width (token filter) -> icu_normalize char filter
ドキュメントに、「CJK Width Token Filter の処理は NFKC/NFKD Unicode 正規化のサブセットなので、analysis-icu plugin を使うと良いよ」(超訳)と書いてあるので、cjk_width ではなく icu_normalize char filter を使用。
また、 icu_normalize は、大文字 → 小文字にもしてくれるので、lowercase token filter も不要。
kuromoji_neologd_baseform を使用
単語を原型にしてくれる。これを使うかどうかは要件によって変わると思うけど、自分達の用途だと、単語やフレーズ単位で検索することが殆どなので、使用したほうが良いと判断。
icu_normalizer token filter を最後にもう一度使用
icu_normalizer は char filter と token filter のどちらとしても使用できる。char filter を使ってるのに、最後でもう一度 token filter として使っている理由は、その前段の kuromoji_neologd_baseform token filter が正規化されていない文字列を返す場合があるので、その対策。
具体的には、linux や LINUX といった、辞書に載っているけど原型でない単語の場合、kuromoji_neologd_baseform が Linux という原型(先頭が大文字)にして返してくれるが、Linux という原型を渡した場合は、kuromoji_neologd_baseform は処理をせずにそのまま linux を後続に渡す。つまり、入力 token と filter 後の token の関係は以下の通り。
- linux -> Linux
- LINUX -> Linux
- Linux -> linux
これだと困るので、最後に icu_normalizer を再度かけている。
以上、かなりはしょった説明。
kuromoji_neologd_baseform が正規化された形式のものを返してくれればそれがいいのかもしれないけど、baseform や、(今回は触れていないけど)synonym の展開とか、正規化された文字列が返されないケースも考慮して、最後に入れといたほうが安心かなぁという感想。
添付ファイルの highlight について
nest された attachment タイプのドキュメントのハイライトをうまくやるのに、ちょっと時間がかかった。
elasticsearch-mapper-attachments の以下の issue を見ると、どうも出来ない?
Highlighting in nested document · Issue #153 · elastic/elasticsearch-mapper-attachments
もうちょい検索したら、じょーたにさんが作成した本家の以下の issue が見つかった。
Does not return stored field in nested object · Issue #5245 · elastic/elasticsearch
nested objects の highlight は inner hits を使えば出来るっぽいんだけど、attachment の場合はそれで良いのかよく分からなかった。結局、上述の通り highlight query を使用した。
phrase 検索は必要
形態素解析をベースとした analyzer の場合、例えば「自然言語処理」と入力すると、「自然言語処理」だけでなく「自然」「言語」「処理」の検索結果も返ってきてしまうので、phrase 検索出来るようにしておくと、余分な結果を弾けて便利。
explain=true がめちゃ便利
analyzer の挙動が思った通りにならない時に、Analyze API に explain=true をつけると、詳細な情報が出てきてめちゃ便利。このページが丁寧に説明してあった。
ちなみに、この機能、johtani さんが作ったらしい(どっかのページに書いてあったけど、URL見つからず)。いやー、素晴らしい。ちなみに、 twitter で何度か質問に回答していただいたりとか、個人的には大谷さんに足を向けて寝られない。どちらにお住まいかは存じませんが。
今後やりたい事、気になる事
ある程度英語向けの設定も混ぜる?
最終的には、次項で書くように多言語化したい。ただ、自分達のような用途だと、あるテキストの中に現れる言語って、母国語(日本語やその他の言語)+英語という形が殆どなので、analyzer の設定で、
- 母国語向けの設定(今回の設定)
- 基本的な英語向けの設定 (baseform に揃える、とか)
を1つの analyzer にまとめてしまっても良いんじゃないか、というアイディアを持ってるので、今度試してみようと思う。
多言語化
前述の Hello! Elasticsearch の著者の方が Elasticsearch 勉強会で発表した資料が参考になりそう。この方が開発されている Siba というサービスの宣伝も兼ねた発表なので、肝心なところは結構ボカされているけど、ある程度参考になった。
使用用途として、例えばフィリピンでの開発チームだと、タガログ又はビサヤ語+英語だけど、そこに日本語が交じることは殆どないので、
- 使用するチーム単位で異なるインデックス
- インデックス単位で各チームの母国語+英語が使える設定
- どの「母国語」を使うかは自動判別
みたいな形にしようかと考えている。
ingest なにそれ?
Mapper Attachments Plugin が、Elasticsearch 5.0.0 で deprecated になり、Ingest Attachment Processor Plugin になるとのこと。Elasticsearch って便利だけど、バージョンアップが速いし、仕様変更もめちゃ多いので、ついていくのが大変。
Ingest に関するドキュメントはこちらを参照。
まとめ
まとめらしい話は特に無いけど、Elasticsearch の日本語に関する情報って結構バラバラなので、調べたことをまとめた。気が向けば(多分向かない)追記予定。
追記:多言語化に関してちょっと書いてみた。