Analyze設定のマイフェイバリット(Elasticsearch)

◯◯たるもの、嗜みの一つとして、Elasticsearchの日本語関連のAnalyze設定のフェイバリットのひとつ、ふたつはお持ちかと思います。

検索要件次第のところもありますが、そこがはっきりしない場合など、自分の脳内基本設定の軸があることで、それとの比較でトレードオフがあぶり出されることになると思いますので、スポンサーから特に指定がない場合は、この設定でまずはやってみようというものがあった方がなにかと効率的です。

2018年12月現在の自分の好みの初期設定的なものを自分の頭の整理ということで書き出してみます&背景を述べてみます。 (なんらかの知見っぽいものを炙り出せれば、あるいは勘違いなどあればそれはそれで誰かの役に立つと思って書いていますが、思ったより膨らまないかもしれません。その場合はごめんなさい。)

動作確認したバージョンは、Elasticsearch 6.4です。

この記事のスタンスとしては、性能はそこまで論じないことにします。もちろん性能が重要ではないという意味ではなくむしろ最も大事ですが、ここでは頭の整理を重視します。

初期設定のスタート版

先に推奨設定の例を貼り付けておきます。

PUT hogeindex
{
  "mappings": {
    "_doc": {
      "dynamic_templates": [
        {
          "my_dynamic_ja_analyzer_conf": {
            "match_mapping_type": "*",
            "match": "*_ja",
            "mapping": {
              "analyzer": "my_ja_analyzer_conf"
            }
          }
        }
      ]
    }
  },
  "settings": {
    "analysis": {
      "analyzer": {
        "my_ja_analyzer_conf": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "mode": "search",
          "char_filter": [
            "icu_normalizer",
            "kuromoji_iteration_mark"
          ],
          "filter": [
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "ja_stop",
            "lowercase",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        }
      }
    }
  }
}

ここで、

POST /my_ja_map/_analyze
{
  "analyzer" : "my_ja_analyzer_conf",
  "text" : 
"関西国際空港発の羽田空港行きの便に乗ります。
JANaの10時33分の便です。
ターミナルの前のタクシーをつかまえてください。"
}

でアナライズを通すと、

"関西", "関西国際空港", "国際", "空港", "発",  "羽田空港",  "行き",    "便",  "乗る",
"jana",  "10", "時",   "33", "分の",  "便",       
"ターミナル",   "前",  "タクシ",  "つかまえる",  ”くださる"

というトークンが帰ってきます。

単語を中心に助詞などが取り除かれました。また英数字が半角に統一されています。「タクシ」というのもあります。

以下、おもな勘所についてそれぞれ気まぐれに述べてみます。

トークナイザーについて

初期設定の最終系(少々意味不明な言い方ですが...)では、富豪プログラミング的発想で、形態素解析N-gramのハイブリッド(前者をブースト、スコアリング高め)にします。

が、ここでは同時に説明するのは(私のキャパからすると)難しいので、形態素解析に絞ります。

以下、しばらく、tokenizerに形態素解析のkuromojiを使う前提で説明しています。

modeについて

形態素解析のライブラリでは、素直に単語に分割するのではとイメージされますが、基本はそのようであるものの、実用の話で言うと、特に固有名詞などで発生しやすい未知語の扱いについて少々工夫ができるようです。

例えば、関西国際空港であれば、「空港」と「関西国際空港」を同じ文書位置にインデックスしておきたいということもあると思います。

このような設定の加減は「mode」プロパティで可能です。

設定としては、normal、search、extendedの3種ができますが、上記の複数配備の例に近く、ある程度当たりやすさの幅が確保できる(ような気がする)searchが好みです。

現在のデフォルトのmodeはsearchのようですが、これは明示的に指定しておくのが良いと思います。

なお、「大田区」は、【大田,区,大田区】になることを期待しましたが、手元の環境だと【大田,区】になりました。

品詞指定によるインデックス不要な語の除外(part_of_speech ...)

特に検索エンジンにあかるくなくともなんとなく分かる(?)話としては、

「にあたり」「として」「けれど」「ところが」といったつなぎの単語は検索インデックスに入れなくても良いのでは、ヒットしなくても良いのではという気になりませんか。

品詞で言うと、「助詞」は最たるもののように思われます。

「助詞」だけでなく、やろうと思えば「名詞」さえも除去できますが、

"filter": [
  "kuromoji_baseform",
  "kuromoji_part_of_speech",
  "ja_stop",
  "kuromoji_number",
  "kuromoji_stemmer"
]

のようにkuromoji_part_of_speechをfilterに指定するだけのデフォルト除去対象の設定で良いと思います。 (やろうと思えば、公式のコレ............のようにひとつずつ設定できます。)

さて、デフォルト除去対象がどれなのかというと、↓でコメントアウトが外れているもの(行頭が#でないもの)が除去対象になるようです。

https://github.com/apache/lucene-solr/blob/master/lucene/analysis/kuromoji/src/resources/org/apache/lucene/analysis/ja/stoptags.txt

前項のmodeとは裏腹ですが、下手に指定して、除去したいパターンを漏らしてしまうのは避けたいので、こちらについてはデフォルト=ベストプラクティスと考えることにします。 (実際ベストプラクティスだと思いますし、実のところここの設定を1つずつやるのはめんどくさい... 本音)

なお、デフォルトをうまく使おうというところはそれはそれとして、例えばお店の検索サイトや多少変わった名前が付くことのある商品サイトの場合は、kuromoji_part_of_speechは設定しない方が安全かもしれません。

全角と半角(icu_normalizer)

続いて、

  • アルファベット
  • 数字
  • 半角カナ

の正規化です。

これは私の中では必須です。データソースが何とかは基本関係ありません。絶対設定しておきましょう。「絶対」は言い過ぎだとしても、少なくともスタートはこのchar_filterをかましておく設定から入っておいて、なんかあったらチューニングするのがリスクが少ないように思います。

記事を書くのに、うかつなことを言わないように、下記の方がまとめられている内容を参考にさせていただきました。

https://christina04.hatenablog.com/entry/2016/08/03/123000

踊り字の置き換え(kuromoji_iteration_mark)

これも私の中では特に設定しない理由がないのでオンにします。一件単純そうでも、仮に自前で実装するとかなりめんどくさいですよね。そうでもないかな。

アルファベットの大文字と小文字を統一(uppercase、lowercase)

日本語が中心のドキュメントでは、これも対応しておくのが無難だと思います。私はlowercase派です。

ストップワードの除去(ja_stop)

これも対象にしておきます。実のところpart_of_speechの時点でかなり削られているので、この設定がどれくらい効いているかは特に確かめたことがありません。

逆に、ja_stopで満足できない場合は、Stop Token Filterで個別指定すると良さそうです。

 https://www.elastic.co/guide/en/elasticsearch/reference/6.5/analysis-stop-tokenfilter.html

ja_stopに限りませんが、全体的に余計な単語は除去しておく方が、サジェストや特別なクエリ(significant term など)でうまい値が出やすくなると思います。

カタカナ文字の後ろの長音を正規化(除去)(kuromoji_stemmer)

「サーバー」→「サーバ」にします。ちなみに、デフォルトだと4文字以上のワードの場合が対象で、「コピー」は「コピ」にはなりません。確かに元が3文字ぐらいだと表記揺れ自体は減りそうです。

ここでふと気になりましたので、試してます。

サーバ- サーバ- サーバ﹣ サーバ− サーバ‐ サーバ⁃ サーバ‒ サーバ〜 サーバ〰

をアナライズしてみたところ、全て「サーバ」に統一されました。※一緒に設定してあるストップワード除去などのアナライザーの効果によるものもあると思います。

コピペ間違いなどしているかもしれませんが、上記は、この順に

  • HYPHEN-MINUS
  • FULLWIDTH HYPHEN-MINUS
  • SMALL HYPHEN-MINUS
  • MINUS SIGN
  • HYPHEN
  • HYPHEN BULLET
  • NON-BREAKING HYPHEN
  • 全角チルダ
  • 波ダッシュ

です。

漢数字をアラビア数字に正規化(kuromoji_number)

これも設定しない理由はありません。...というと言い過ぎですが、他のものより優先度は低いものの、検索対象にゆる系のドキュメントが多いことが明らかであれば、設定しておくのかなと思います。

逆に、元のドキュメントが正規化されていたりライティングルールが適切に運用されている、もしくはRDBが元ネタというような場合は、無理に設定しない方が良い場合もチラチラ。

というのも(と偉そうに言う話ではないですが)、これらの厳格な運用がされている・されなければならないデータについては、漢数字とアラビア数字をあえて別物と捉えて管理している可能性があるためです。

検索ユーザー層と照らし合わせてどちらか選ぶことになるのかなと思いました。

この他、ゆる系のドキュメントがデータソースだとしても、場合によっては、トレードオフが出そうなものを頭の片隅においておくと良いかもしれません。 正規化したことによるトレードオフのアルアルのものとしては「住所」、次に固有名詞でしょうか。

原形化(アカデミックな言い方はこれでよかったけ?)(kuromoji_baseform)

これも有効にしておきましょう。名詞が中心かつなんらかの理由でできるだけ製品デフォルトで扱いたいというような場合以外は、有効にしておく方が思わぬものに悩まされずにすむと思います。

Mapping Char Filterの使いっぷり

上記の初期設定のスタート版には入れていませんが、人名をなんらか扱うものの場合、

渡辺 渡邊 渡邉

問題を解決してやる必要があるかもしれません。

人名や固有名詞がそれなりに登場するドキュメントの検索の場合、同音異字体の扱いについてポリシー決めが必要になり、それに合わせて、異字の置き換えの必要があります。

Mapping Char Filter | Elasticsearch Reference [6.5] | Elastic

拗音と促音

調査不足かもしれませんが、拗音や促音(キャキュキョや、ラッパ、カッパ、スタッカートなど)については、正規化するものは無いようでした。

最近の検索利用者はPCやスマホでの検索になれているため、拗音や促音の入れ間違いは少ないような気がしますが、古いシステムだとデータソースの方の「カナ」項目について、「キヤキユキヨ」などが格納されているものも見るような見ないような気もしますので、そんな時には考えどころですが、検索結果が0件の時に、Term suggesterで「もしかして」表示する方が良いかもしれませんね。

漢字ひらがなを読み(カナ)に変換するか(kuromoji_readingform)

上記の初期設定のスタート版には入れていませんが、kuromoji_readingformというToken filterがあります。

これを適用すると、インデックスには問答無用で(おそらくkuromojiの辞書の)読み仮名に従いカタカナでインデックスされます。

ここまで頭の整理をした大半のものが「除去」や「同じ意味」の表記揺れのカバーでしたが、このフィルターを通すと意味は同じであるものの、インデックス格納されるデータはがらりと変わってしまいます。

ここで、少しいやらしいのが、kuromojiの辞書に従うということになります。通常は検索時にも同じアナライザーを通すでしょうから、辞書がどうこうというところはそれほど関係ないような気がします。 ただし、読み仮名で当てさせたい場合は、もともと読み仮名フィールドを持っていることが多いため(というのは少し言い過ぎなものの)、読み仮名フィールドを検索対象としてやるぐらいで良いのかなということで、私はkuromoji_readingformはあまり使わないで良いんじゃない派です。

と言いつつ、「読み仮名」ならではというか、名称どおり、読みを意識したインデックスを作るとどのようなケースにヒットさせることができるので嬉しいかというところに絞って考察してみて、次のようなものをあげてみました。

  • 素直に? フィールド派生するなどして、元のもののままのものとカナフィールドを設ける(やっぱり両方持っておこうという話)
  • 自前のオートコンプリートなどに利用するフィールドに適用する
  • 店舗名など、漢字名称とカナ(よみがな)が管理されている中、漢字名称の部分に対し、元のもののままと、kuromoji_readingformを適用したものを別にもうけて、これも平行して検索することで、読み間違いしている人の検索を救済できる(カモしれない)
  • Term suggesterのユースケースなど、一文字違いの単語を間違えて入力してしまった時に
  • 前項で示した、異体字のことをできるだけ考えなくてよい検索を提供したい(異体字として扱いたいものが多く、analyzerの設定で指定したくない、ファイル指定もしたくない/できない)
  • VOIのバックエンドの検索エンジンとして使う

3番目の例ですが、例えばこういうことです。

渡部という名前は、「ワタナベ」さんと「ワタベ」さんがそれなりにいらっしゃると思います。調べたわけではありませんが、どちらかといえば、前者の方が多数のように思います。

今ドキュメントには、「渡部」で実際の読みはややレアな方の「ワタベ」さんで、事実どおりにデータソースに登録されています。

インデックス格納時には、それぞれのフィールドのまま格納しつつ、漢字名称の「渡部」については、形態素解析の辞書を使ってkuromoji_readingformでカナを派生させて「ワタナベ」でインデックスに登録しておきます。

これにより、ユーザーが「ワタナベ」(実際カナのまま入力)さんで検索しても、一応、このドキュメントがヒットすることになります。

例のための例になってしまったところはありますが、多数派の読みに引っ張られてそちらで検索する人が多いという場合をイメージしました。

検索時とインデックス時のアナライザーを別条件にするケース

最後に本件の話に近いところで、検索時とインデックス時のアナライザーを別条件にするケースを整理。

  • オートコンプリートでedge_ngramを使うような場合(公式Rでのコメント)
  • おもにCharacter FilterかToken Filtersのケースの「除去」を行うタイプのものに当てはまると思われるが、  インデックス時には除去するFilterを控えめにしておき(インデックスに蓄えるワードを増やしておく方向で将来に備えておくが)、検索時には除去することで、直近の仕様を確保。ケースバイケースだけど、不要な品詞の除去、ストップワードの除去のパターンがあてはまりそう。長音の除去はあてはまらないかな。

もう少しだけユースケースがありそうですが、ひとまずはこれぐらいでしょうか。

まとめ

偉そうに語ってみましたが、実のところkuromoji pluginのものはほぼ全て使う。それぞれのパラメータ設定は特にしないという例に落ち着きました。

毎度のことですが、先人に感謝です。

まとめとして、もう一度、推奨設定での例を貼り付けると

f:id:azotar:20181218221751p:plain

です。

ひとまず以上ですが、よくみると途中でうんちくを語りかけておいて、特にそれ以上ふれずじまいのものがいくつかあります。

例えば、店舗名、人名、住所といったものです。自分のあたまの整理と勉強をかねてどこかで時間を見つけて考察の上、自説をたれてみたいと思います。

参考リンク

Elasticsearchを日本語で使う設定のまとめ - Qiita Elasticsearch Analyze APIでkuromoji形態素解析を試す - Qiita 自作カナ変換プラグインでElasticsearchの日本語検索をいい感じにする - Qiita Elasticsearch 日本語で全文検索 その2 – Hello! Elasticsearch. – Medium