Elasticsearchで品詞分解① (テンプレート編) 【追記あり】

ELSには日本語解析プラグインが用意されています。
[Japanese (kuromoji) Analysis Plugin | Elasticsearch Plugins and Integrations [6.1] | Elastic](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)

Java等で品詞分解する事ももちろんできますが、ELSのテンプレートを利用する事で、Kibana上で品詞分解表示などが出来ます。
この記事では、まずELSのテンプレートで登録するところまでをやります。

# プラグインインストール

> Elasticsearchの日本語の形態素解析をする際に利用されるkuromojiは非常に便利ですが、その辞書であるIPADICは更新が止まっているためやや古い状態です。
その辞書を更新してくださった方がいらっしゃり、neologdとして公開されているためそれを導入して新語でもきちんと解析できるようにします。
 [neologdでkuromojiを新語に対応させる - Carpe Diem](http://christina04.hatenablog.com/entry/2016/05/18/193000)

[codelibs/elasticsearch-analysis-kuromoji-neologd: Elasticsearch's Analyzer for Kuromoji with Neologd](https://github.com/codelibs/elasticsearch-analysis-kuromoji-neologd)

という訳で、`elasticsearch-analysis-kuromoji-neologd`をインストールします。
手順は上記サイト等を参考にしてください。
基本的な動作確認も上記サイトに記載されています。

＃本当は`Sudachi`も試してみたかったのですが、うちのPCスペック貧弱で、辞書読み込むと`Out of Memory`になっちゃうんです……

# 文字列解析の定義方法

ELSの文字列解析は階層構造(？)になっています。
文字列解析方法をカスタマイズする場合、この構造に沿って定義を行っていきます。

[ElasticsearchのAnalyzer, Tokenizer, Token Filters, Char Filtersの一覧 - Qiita](https://qiita.com/wapa5pow/items/a11274d85a0359c423d9)
[Elasticsearch 日本語で全文検索 その２ - Hello! Elasticsearch. - Medium
](https://medium.com/hello-elasticsearch/elasticsearch-833a0704e44b
)

１．Tokenizer で、どのような形態素解析を行うかを定義する

```
  "tokenizer": {
    "ja_neologd_tokenizer": {
      "mode": "search",
      "type": "kuromoji_neologd_tokenizer"
    }
  }
```

上記の場合、`ja_neologd_tokenizer`という名前で、`search`モード（分解の粒度）
で`kuromoji_neologd_tokenizer`という形態素解析手法（プラグインに依る）を行う、と定義しています。

２．Filterで、どのようなフィルタ（文字列の変換・除外）を行うかを定義する

```
"greek_lowercase_filter": {
  "type": "lowercase",
  "language": "greek"
}
```

上記の場合、`greek_lowercase_filter`という名前で、アルファベットを小文字にする、という定義をしています。
`kuromoji_neologd_part_of_speech`というフィルターが品詞分解の時には重要になってくるのですが、ここでは、とりあえずフィルターは自前で定義することが出来る、という事を覚えていてください。

３．Analyzer で、どのTokenizerとFilterを用いるかを組み合わせて定義する

``` 
"ja_neologd_analyzer": {
  "filter": [
    "greek_lowercase_filter",
    "cjk_width"
  ],
  "char_filter": [
    "icu_normalizer"
  ],
  "type": "custom",
  "tokenizer": "ja_neologd_tokenizer"
}
```

上記の場合、`ja_neologd_analyzer`という名前で、`analyzer`を定義しています。
`analyzer`の内部で、どのような`tokenizer`(解析手法)を使用し、どのような`filter`（変換・除外）をかけるか、を組み合わせる事ができます。
これによって、一概に文字列解析、といっても、用途に応じて様々なパターンの解析を行う事ができるようになります。
`filter`の中には、`cjk_width`（全半角の統一）など、フィルタ定義を自前で行っていないものもあります。
これらは、ELSにバンドルされているものや、インストールしたプラグインにデフォルトで用意されているものなどです。

上記例を、日本語訳すると以下のような感じです。

```
新しい感じの日本語(kuromoji_neologd_tokenizer)を、
検索にちょうどいい感じで分解(search)してちょうだい。
アルファベットは小文字(greek_lowercase_filter)で、
全半角は統一(cjk_width)してほしいな。
後、いい具合に正規化(icu_normalizer)してね！
```

## テスト

上記analyzerを使って、文章の解析を行ってみましょう。

例文１）

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_analyzer",
  "text": "3センチのNOTEBOOKって、ｱﾘ？"
}
```

```
{
  "tokens": [
    {
      "token": "3センチ", <-- 半角数字+全角カタカナ
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0
    },
    {
      "token": "の",
      "start_offset": 4,
      "end_offset": 5,
      "type": "word",
      "position": 1
    },
    {
      "token": "notebook", <-- 小文字
      "start_offset": 5,
      "end_offset": 13,
      "type": "word",
      "position": 2
    },
    {
      "token": "って",
      "start_offset": 13,
      "end_offset": 15,
      "type": "word",
      "position": 3
    },
    {
      "token": "アリ", <-- 全角統一
      "start_offset": 16,
      "end_offset": 18,
      "type": "word",
      "position": 4
    }
  ]
}
```

例文２）

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_analyzer",
  "text": "３㌢のNotebookって、アリ？！"
}
```

```
{
  "tokens": [
    {
      "token": "3センチ", <-- 半角数字+全角カタカナ
      "start_offset": 0,
      "end_offset": 2, <-- 文字数は元の文章でカウント
      "type": "word",
      "position": 0
    },
    {
      "token": "の",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 1
    },
    {
      "token": "notebook", <-- 小文字
      "start_offset": 3,
      "end_offset": 11,
      "type": "word",
      "position": 2
    },
    {
      "token": "って",
      "start_offset": 11,
      "end_offset": 13,
      "type": "word",
      "position": 3
    },
    {
      "token": "アリ", <-- 全角統一
      "start_offset": 14,
      "end_offset": 16,
      "type": "word",
      "position": 4
    }
  ]
}
```

…と、このように、文章の表記ゆれも吸収して、形態素解析ができました。
＃句読点は`kuromoji_neologd_tokenizer`内の`discard_punctuation`オプションのデフォルト値として「句読点を除外する」という設定の為、除外されています。

# 品詞分解

上記のanalyzerでいい感じに形態素解析ができるようになりましたが、「の」や「って」など、それ単体では意味をなさない言葉も出力されています。
「名詞だけ抜き出したい」「動詞のホットワードが見たい」などといった要件にも対応すべく、ELS上のみで品詞分解を定義していきます。

##  kuromoji_neologd_part_of_speech (stoptags)

`kuromoji_neologd_part_of_speech`フィルターは、文字列を分解した後の各要素に対して処理を行うフィルターです。
オプションとして、`stoptags`というものがあり、ここに定義された品詞**のみ**除外する事ができます。

`stoptags`のデフォルト定義は以下のように、助詞とか記号が除外されています。
[stoptags一覧](https://qiita.com/miz21358/items/75fd28075ed8cfe2c937)

`stoptags`の設定で重要なのが、

 - 「階層構造指定っぽいけど、すべて並列で指定する」
 - 「カスタムで定義した場合、デフォルト設定は破棄される（継承されない）」

という事です。
「名詞」を`stoptags`に指定しても「名詞-一般」はそれに含まれません。
「助動詞」のみを`stoptags`に指定した場合、「助詞」等デフォルトで除外されていた品詞は解析結果に含まれるようになります。

## 品詞分解テンプレート

上記をふまえて、必要な品詞を抽出し、それを**省いた**除外リストを作成します。
で、品詞分解アナライザを加えたテンプレート作成用JSONがこちらになります。長いのでgithubに投げました。

[template.json](https://gist.github.com/miz21358/ef37c0c0ae0b16d6a4b5382bd816342a)

### アナライザ

ここで作成した品詞分解アナライザは以下の４つです。

 - ja_neologd_meishi_analyzer　…　名詞のみ抽出対象としたアナライザ
  - ja_neologd_pos_meishi_filter
 - ja_neologd_doushi_analyzer …　動詞のみ抽出対象としたアナライザ
  - ja_neologd_pos_doushi_filter
 - ja_neologd_keiyoushi_analyzer …　形容詞のみ抽出対象としたアナライザ
  - ja_neologd_pos_keiyoushi_filter
 - ja_neologd_fukushi_analyzer …　副詞のみ抽出対象としたアナライザ
  - ja_neologd_pos_fukushi_filter

それぞれのアナライザには、`kuromoji_neologd_baseform`フィルタを加えています。
これは、「動きます」→「動**き**」「ます」→「動**く**」「ます」と、分解後に要素の原型に変換するフィルタです。

### マッピング

マッピングでは、`_txt`が末尾にある項目に対して、各品詞分解アナライザをかけるように設定しています。

```
  "analyzed-meishi": {
    "type": "text",
    "fielddata": true,
    "store": true,
    "analyzer": "ja_neologd_meishi_analyzer",
    "search_analyzer": "ja_neologd_meishi_analyzer"
  }
```

- type …　項目の種別
- fielddata … 解析結果を、ソートやAggsに用いる場合、true。→ [公式](https://www.elastic.co/guide/en/elasticsearch/reference/current/fielddata.html)
- store … 解析結果を格納する場合、true。→ [公式](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html)
- analyzer … 登録時のアナライザ。→ [公式](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer.html)
- search_analyzer … 検索時のアナライザ。→ [公式](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-analyzer.html)

## テスト

### 名詞バージョン

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_meishi_analyzer",
  "text": "春の小川はさらさら流る。 岸のすみれやれんげの花に、: 匂いめでたく、色うつくしく: 咲けよ咲けよと、ささやく如く"
}
```

```
{
  "tokens": [
    {
      "token": "春の小川",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0
    },
    {
      "token": "岸",
      "start_offset": 13,
      "end_offset": 14,
      "type": "word",
      "position": 4
    },
    {
      "token": "すみれ",
      "start_offset": 15,
      "end_offset": 18,
      "type": "word",
      "position": 6
    },
    {
      "token": "れんげ",
      "start_offset": 19,
      "end_offset": 22,
      "type": "word",
      "position": 8
    },
    {
      "token": "花",
      "start_offset": 23,
      "end_offset": 24,
      "type": "word",
      "position": 10
    },
    {
      "token": "匂い",
      "start_offset": 28,
      "end_offset": 30,
      "type": "word",
      "position": 12
    },
    {
      "token": "色",
      "start_offset": 35,
      "end_offset": 36,
      "type": "word",
      "position": 14
    }
  ]
}
```

＃春の小川、で一単語になってますね。なんでだろ…

### 動詞バージョン

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_doushi_analyzer",
  "text": "春の小川はさらさら流る。 岸のすみれやれんげの花に、: 匂いめでたく、色うつくしく: 咲けよ咲けよと、ささやく如く"
}
```

```
{
  "tokens": [
    {
      "token": "流る",
      "start_offset": 9,
      "end_offset": 11,
      "type": "word",
      "position": 3
    },
    {
      "token": "咲く",
      "start_offset": 43,
      "end_offset": 45,
      "type": "word",
      "position": 16
    },
    {
      "token": "咲く",
      "start_offset": 46,
      "end_offset": 48,
      "type": "word",
      "position": 18
    },
    {
      "token": "ささやく",
      "start_offset": 51,
      "end_offset": 55,
      "type": "word",
      "position": 21
    }
  ]
}
```

### 形容詞バージョン

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_keiyoushi_analyzer",
  "text": "春の小川はさらさら流る。 岸のすみれやれんげの花に、: 匂いめでたく、色うつくしく: 咲けよ咲けよと、ささやく如く"
}
```

```
{
  "tokens": [
    {
      "token": "めでたい",
      "start_offset": 30,
      "end_offset": 34,
      "type": "word",
      "position": 13
    },
    {
      "token": "うつくしい",
      "start_offset": 36,
      "end_offset": 41,
      "type": "word",
      "position": 15
    }
  ]
}
```

### 副詞バージョン

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_fukushi_analyzer",
  "text": "春の小川はさらさら流る。 岸のすみれやれんげの花に、: 匂いめでたく、色うつくしく: 咲けよ咲けよと、ささやく如く"
}
```

```
{
  "tokens": [
    {
      "token": "さらさら",
      "start_offset": 5,
      "end_offset": 9,
      "type": "word",
      "position": 2
    }
  ]
}
```

いい感じに分解できました！`stoptags`は色々試してみると面白いです。
Kibanaでの表示も一緒に書こうと思ってましたが、長くなってしまったので、また次回。


### 1/17 追記

```
GET sample-analyze-qiita/_analyze
{
  "analyzer": "ja_neologd_doushi_analyzer",
  "explain" : true,
  "text": "春の小川はさらさら流る。 岸のすみれやれんげの花に、: 匂いめでたく、色うつくしく: 咲けよ咲けよと、ささやく如く"
}
```

`_analyze`の際に、`explain`オプションを`true`にすると、`analyzer`の内部で、`tokenizer`や`filter`がどのようにかかっていっているかを、ステップごとに確認することができます。

```
    "tokenizer": {
      "name": "ja_neologd_tokenizer",
      "tokens": [
        {
          "token": "春の小川",
          "start_offset": 0,
          "end_offset": 4,
          "type": "word",
          "position": 0,
          "baseForm": null,
          "bytes": "[e6 98 a5 e3 81 ae e5 b0 8f e5 b7 9d]",
          "inflectionForm": null,
          "inflectionForm (en)": null,
          "inflectionType": null,
          "inflectionType (en)": null,
          "partOfSpeech": "名詞-固有名詞-一般",
          "partOfSpeech (en)": "noun-proper-misc",
          "positionLength": 1,
          "pronunciation": "ハルノオガワ",
          "pronunciation (en)": "harunoogawa",
          "reading": "ハルノオガワ",
          "reading (en)": "harunoogawa"
        },
```

「春の小川」は、固有名詞として解析されたので、一区切りになっているようですね。
謎が解けてスッキリしました！

johtani様、コメントありがとうございました。