RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

【Elasticsearch】Analyzerを手動で設定する方法 -Analyzerを理解する-

初めまして。今年度新卒入社のmako_makokです。最近実家に帰って水族館でペンギンを見てきました。
今回は全文検索エンジンのコア機能の一つであるAnalyzerについて書いていきたいと思います。

はじめに

私は現在、個人的に全文検索エンジン学習をしています。
以前までは諸事情でApache Solrをやっていたのですが、以下の理由からElasticsearchの学習に切り替えました。

  • シェアとそれに伴うドキュメントの充実
  • KibanaをはじめとしたElastic Stackの存在
  • クエリの書き方覚えたらいい感じにクエリ書けそう

Apache Solr及びElasticsearchではApache LuceneいうOSS全文検索ライブラリがコアになっております。
LuceneにはAnalyzerという機能があり、全文検索エンジンにおいて非常に重要な機能です。
今回は実際にElasticsearchでAnalyzerを設定しながら、Analyzerの仕組みを見ていきたいと思います。

検索エンジンの仕組み

まずは簡単に検索エンジンの仕組みを説明します。
検索エンジンではあらかじめドキュメントのインデックスを作成しておき、そこへクエリが投げられるとそれにマッチするものがヒットするというのが基本になります。
Luceneでは転置インデックスという方式が採用されています。
転置インデックスは以下のような形で作成されます。

f:id:mako_makok:20190930180706p:plain
転置インデックス

何らかの方法でドキュメントのテキストを分割し、それに対応するドキュメントIDがリストになります
クエリが投げられた際も同様、クエリのテキストを分割し、インデックスのキーにマッチした文章が返るという仕組みです。

Analyzerとは

先ほどは検索エンジンの仕組みについて説明しました。
何らかの方法で分割するとありましたが、この部分がAnalyzerの仕事になります。
正確には、Analyzerは目的のインデックスを作成するために、テキストの分割や正規化などを行います。
Analyzerは以下の3つの機能で構成されています。

  • Char filter
  • Tokenizer
  • Token filter

Analyzerの処理フローです。
Char filter → Tokenizer → Token filterの順に処理されます。Tokenizerは必須・かつ一つしか設定できません。

f:id:mako_makok:20190930163751p:plain
Analyzerの処理手順

前準備

今回はElasticsearchで動作確認を行います。
Analyzerがどのような動きをしているかどうかだけ知りたい方は飛ばしてください。

Java 8以上の環境が必須です。

  • Elasticsearchをインストール
# brew
brew install elasticsearch

# yum
yum install elasticsearch
  • 日本語関連の機能と、テキストの正規化を強化する2つのプラグインをインストールします
    • analysis-kuromoj
    • analysis-icu
cd {ES_HOME}
bin/elasticsearch-plugin install analysis-kuromoji
bin/elasticsearch-plugin install analysis-icu
  • Elasticsearchを起動して起動確認も行います
# 起動
bin/elasticsearch
      
# 起動確認
curl http://localhost:9200/
      
  "name" : "1GpZYN9",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "hoge",
  "version" : {
    "number" : "6.8.3",
    "build_flavor" : "oss",
    "build_type" : "tar",
    "build_hash" : "0c48c0e",
    "build_date" : "2019-08-29T19:05:24.312154Z",
    "build_snapshot" : false,
    "lucene_version" : "7.7.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
  • これで準備完了です。これから実際にAnalyzerを作成しながら、Char filter, Tokenizer, Token filterについてそれぞれ解説します。

Char filter

Char filterのポイントは以下の通りです。

  • テキストに対して機械的に前処理を行う
  • 必須ではない
  • いくつでも設定できる

機械的な前処理というのは、例えば文字の全角↔半角処理だったり、正規表現による抽出などが挙げられます。

ここからいよいよAnalyzerの設定を行っていきます。 Elasticsearchでは様々な機能がREST APIで操作できます。
今回はChar filterに以下を設定します。

  • icu_normalizer
  • kuromoji_iteration_mark
    • 踊り字を正規化
      • 踊り字とは々、ヽ、ゝのような前置の単語によって読み方が変化する単語のことです

Analyzerを設定するにはインデックスを保管しておくスペースを作成しなければなりません。
今回はインデックス名をanalyzer_handsonとして、Analyzerを設定していきます。
以下のjsonをmy_kuromoji_analyzer.jsonと保存し、POSTします。

{
      "settings": {
          "analysis": {
              "analyzer": {
                  "my_kuromoji_analyzer": {
                      "type": "custom",
                      "char_filter" : ["icu_normalizer", "kuromoji_iteration_mark"],
                      "tokenizer": "keyword"
                  }
              }
          }
      }
  }

jsonlocalhost:9200/analyzer_handson/にPUTします。

curl -XPUT localhost:9200/analyzer_handson/ -H "Content-type: application/json" -d @my_kuromoji_analyzer.json

これでmy_kuromoji_analyzerというAnalyzerが設定されました。
早速テキストをアナライズしていきます。
~/_analyzeがエンドポイントになっており、こちらで特定のAnalyzerの挙動を確認することができます。 そこに以下のjsonをPOSTします。

  {
    "analyzer": "my_kuromoji_analyzer",
      "text": "コウテイペンギンは体格のいいものは130㌢あるという。僕は度々、コンピューターでそれを見て和んでいる"
  }
curl -XPOST localhost:9200/analyzer_handson/_analyze -H "Content-Type: application/json" -d @query.json

以下のような結果が返ってきます。

  { 
     "tokens":[ 
        { 
           "token":"コウテイペンギンは体格のいいものは130センチあるという。僕は度度、コンピューターでそれを見て和んでいる",
           "start_offset":0,
           "end_offset":50,
           "type":"word",
           "position":0
        }
     ]
  }

無事正規化されています。

  • 130は全角数字→半角数字
  • ㌢→センチ
  • 度々→度度

に変換されています。

このような正規化の処理は自然言語処理の前処理として非常に重要です。
内部的には半角数字と全角数字などは違う文字として扱われるため、検索のノイズになることや、逆に欲しいドキュメントがヒットしないなどの問題が発生します。
踊り字などは主に古典などでノイズになることが多いです。

他にもChar filterはたくさんあるので、色々試していきたいところです。


Tokenizer

Tokenizerはテキストを分かち書きします。分かち書きとは、特定の規則に乗っ取ってテキストを分割することです。
ではどのように分かち書きするのかですが、日本語の場合は形態素解析もしくはN-gramを使用することが多いです。 形態素解析N-gramのTokenizerには以下のようなものがあります。

  • kuromoji_tokenizer
    • 日本語用の形態素解析器であるKuromojiを使用して形態素解析を行う
    • 辞書は2007年からメンテナンスされていないため、新語(芸能人の名前や流行語)などに弱い*1
  • N-gram Tokenizer
    • 文字数で機械的に区切り、分かち書きを行う
    • 1-gramをuni-gram, 2-gramをbi-gram, 3-gramをtri-gramと呼ぶ

形態素解析では、テキストを品詞単位で分解し、分割された単語のことをトークンと呼びます。 分解したトークンには品詞の情報はもちろん、活用形などの情報が付与されます。 ElasticsearchではKuromojiという日本語形態素解析器が用いられています。

もう一つの分かち書きの手段のN-gramについてです。
例えばtri-graだとこのように分解されます

f:id:mako_makok:20190930163805p:plain
N-gram

今回はtri-gramなのでテキストを3つに区切りながら一つずつ横にずらしていく感じです。
これら2つの手法はそれぞれ利点・欠点があります

  • 形態素解析
    • 辞書ベースで区切るため、辞書に載っている単語については比較的高い精度の検索結果を得やすい
    • 逆に辞書に載っていない単語などは1文字区切りで分かち書きされるため、未知の単語に弱い
  • N-gram
    • 機械的に区切られるため、未知の単語などでもドキュメントをヒットさせることができる
    • "京都で遊ぶ"のような単語で検索されたとき、上記の画像のように"東京都"を含むドキュメントがヒットする
      • 検索ノイズが増えやすい

どちらも一長一短なので、検索要件によって適切にTokenizerを設定する必要があります。

今回は形態素解析を用いて単語分割していきたいと思います。

  • Analyzerの設定を更新するために、まずは一旦インデックスをcloseします。
curl -XPOST localhost:9200/analyzer_handson/_close
  • 次に、Char filterの時に使用したmy_kuromoji_analyzer.jsonのtokenizerをkuromoji_tokenizerに編集しPOSTします。
{
    "settings": {
        "analysis": {
            "analyzer": {
                "my_kuromoji_analyzer": {
                    "type": "custom",
                    "char_filter" : ["icu_normalizer", "kuromoji_iteration_mark"],
                    "tokenizer": "kuromoji_tokenizer"
                }
            }
        }
    }
}
# 更新する際は/_settingsにPOSTします
curl -XPUT localhost:9200/analyzer_handson/_settings -H "Content-type: application/json" -d @my_kuromoji_analyzer.json
  • 最後にopenして完成です
curl -XPOST localhost:9200/analyzer_handson/_open

早速テキストを投げてみると以下のように形態素解析されていることがわかります。

{ 
   "tokens":[ 
      { 
         "token":"コウテイペンギン",
         "start_offset":0,
         "end_offset":8,
         "type":"word",
         "position":0
      },
      { 
         "token":"",
         "start_offset":8,
         "end_offset":9,
         "type":"word",
         "position":1
      },
      { 
         "token":"体格",
         "start_offset":9,
         "end_offset":11,
         "type":"word",
         "position":2
      },
            ...
            ~
            ...
      { 
         "token":"和ん",
         "start_offset":45,
         "end_offset":47,
         "type":"word",
         "position":21
      },
      { 
         "token":"",
         "start_offset":47,
         "end_offset":48,
         "type":"word",
         "position":22
      },
      { 
         "token":"いる",
         "start_offset":48,
         "end_offset":50,
         "type":"word",
         "position":23
      }
   ]
}

無事Kuromojiを使用して分かち書きをすることができました。
最後はToken filterです。


Token filter

Token filterではTokenizerで分かち書きされたトークンに対して様々な変換処理を行います。
以下はToken filterの一例です。

  • Lower case Token filter
    • トークンを全て小文字に変換する
  • Stop Token filter
  • Stemer Token filter
    • 語幹ごとに定義されたステミング処理を行う
    • ステミングとは語形の変化をなくし、表現を統一すること
  • Synonym Token filter
    • 類義語の展開を行う
    • 表記揺れ(引越し、引っ越し、引越)や類義語(パソコン、PC、コンピュータ)など
  • Kuromoji-Analysisに付属しているToken FIlter
    • kuromoji_baseform
    • 動詞・形容詞を原型に戻します。活用形は表記揺れの原因になります。
  • kuromoji_part_of_speech
    • 特定の品詞を削除します。検索において、助詞や助動詞などは必要でないケースがあります。
    • デフォルトでは {助詞-格助詞-一般, 助詞-終助詞}を削除します。
      • 形態素解析を行うことによって、単語に品詞情報が付与されます
  • kuromoji_stemmer
    • 日本語に特化したステミング処理用のToken filter。カタカナの伸ばし棒を削除します。

今回は上記のKuromojiのToken filter3種と、Synonym Token filter, Stop Token filterを使用していきます

  • "コウテイペンギン"で一単語であり、"ペンギン"で検索した時にヒットしない
    • 二つの単語を一つのシノニムグループ*2として扱うmy_synonym_penguin_filterを新しく作成し、filterに追加
  • 動詞や形容詞であるが、いい、もの、ある、いるなど様々な文章で頻出しそう。文章の特徴を表さないのでなくても構わなそうな単語がある*3
    • 頻出しそうな単語をstopwordsに追加
{
    "settings": {
        "analysis": {
            "analyzer": {
                "my_kuromoji_analyzer": {
                    "type": "custom",
                    "char_filter" : ["icu_normalizer", "kuromoji_iteration_mark"],
                    "tokenizer": "kuromoji_tokenizer",
                    "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "kuromoji_stemmer", "my_synonym_penguin_filter", "my_stop_filter"]
                }
            },
            "filter": {
                "my_synonym_penguin_filter": {
                    "type": "synonym",
                    "synonyms": ["コウテイペンギン,ペンギン"]
                },
                "my_stop_filter": {
                    "type": "stop",
                    "stopwords": ["いい", "もの", "ある", "いう", "それ", "いる"]
                }
            }
        }
    }
}

設定を適用したら同じクエリを投げていきます。
すると以下のような結果になりました。

{ 
   "tokens":[ 
      { 
         "token":"コウテイペンギン",
         "start_offset":0,
         "end_offset":8,
         "type":"word",
         "position":0
      },
      { 
         "token":"ペンギン",
         "start_offset":0,
         "end_offset":8,
         "type":"SYNONYM",
         "position":0
      },
      { 
         "token":"体格",
         "start_offset":9,
         "end_offset":11,
         "type":"word",
         "position":2
      },
      { 
         "token":"130",
         "start_offset":17,
         "end_offset":20,
         "type":"word",
         "position":7
      },
      { 
         "token":"センチ",
         "start_offset":20,
         "end_offset":21,
         "type":"word",
         "position":8
      },
      { 
         "token":"",
         "start_offset":27,
         "end_offset":28,
         "type":"word",
         "position":12
      },
      { 
         "token":"度度",
         "start_offset":29,
         "end_offset":31,
         "type":"word",
         "position":14
      },
      { 
         "token":"コンピュータ",
         "start_offset":32,
         "end_offset":39,
         "type":"word",
         "position":15
      },
      { 
         "token":"見る",
         "start_offset":43,
         "end_offset":44,
         "type":"word",
         "position":19
      },
      { 
         "token":"和む",
         "start_offset":45,
         "end_offset":47,
         "type":"word",
         "position":21
      }
   ]
}

しっかり設定したToken filterの効果が表れています

  • kuromoji_baseform(原型へ変換)
    • 見て→見る
    • 和ん→和む
  • kuromoji_part_of_speech(助詞-格助詞-一般, 助詞-終助詞の削除)
  • kuromoji_stemmer(語幹の統一)
    • コンピューター→コンピュータのように伸ばし棒が除去されています
  • Synonym Token filter
  • Stop Token filter
    • 設定した"いい", "もの", "ある", "いう", "それ", "いる"がそれぞれ除去されています。

おわりに

実際にAnalyzerを設定してみました。

Analyzerだけではなく、他にも様々な機能がElasticsearchにはあります。

次は検索ネタを話せたらと思います。

*1:MeCab用の新語対応辞書mecab-ipadic-neologdを適用した、Kuromojiのプラグインがあります

*2:双方向展開

*3:実際に使用する際は公開されているストップワードリストをメンテナンスするのが良い

Copyright © RAKUS Co., Ltd. All rights reserved.