@johtaniの日記 2nd

@johtani ‘s blog 2nd edition

Lucene Kuromoji for NEologdで指定した品詞の単語を抜き出すIngest Pluginを書いてみた #elasticsearchjp

久しぶりに、技術的なブログ書いてます。

Ingest Processorのプラグインを作ってみたくなったので、書いてみました。 ただ書いてみるんじゃ3番煎じになりそうなので、cookiecutterを使ってみました。

と言っても、同僚のAlexがcookiecutter-elasticsearch-ingest-processorと言うテンプレートを作ってくれているのを使っただけですが。(https://discuss.elastic.co に投稿された記事で、使い方がアニメgifで説明されててわかりやすいです)

cookiecutterとは、コマンドラインで質問に答えると、テンプレートからプロジェクトが生成できるツールです。 Elasticでは、カスタムBeatを作る時に利用する例がいつかの日本語ブログや発表資料で話題になっていました。 これのIngest Processorのプラグインバージョンです。

今回は、NEologdも使ってみたかったので、Lucene Kuromoji for NEologdを利用して 指定した品詞の単語だけを抽出するProcessorを作ってみました。

GitHubのプロジェクト:https://github.com/johtani/elasticsearch-ingest-kuromoji-pos-extract

Cookiecutterの使い方

Cookiecutterのインストールはサイトをご覧ください。

1
cookiecutter gh:spinscale/cookiecutter-elasticsearch-ingest-processor

あとは、出てくる以下の項目を指定するだけです。

  • processor_type : Ingest Processorのタイプ名です。kuromoji_part_of_speech_extractとしました。(Alexのだと_を使うとちょっと問題があるので後述)
  • description : readme.mdに利用されます。
  • developer_name : 名前を記載。Javaのファイルのヘッダに利用
  • elasticsearch_version : デフォルトで5.0.0-alpha4が指定されているので、特に指定せず

以上の質問に答えたら、プロジェクトのディレクトリ構造が出来上がってます。 プロジェクトのビルドなどにはGradleを利用します。

プロジェクトのIntelliJ IDEA用のファイルを生成

build.gradleファイルでGradleのideaプラグインがapplyされているので、以下のコマンドを叩けばIntelliJ IDEAのプロジェクトファイル(?)が生成され、IntelliJで開けばすぐに開発ができる状態にできます。

1
gradle idea

コーディング

あとは、必要処理をコーディングします。 実際にコーディングするクラスはorg.elasticsearch.plugin.ingest.kuromoji_part_of_speech_extractのパッケージにある以下の2つです。(パッケージ名にはprocessor_typeの名前が指定されている)

  • IngestKuromojiPartOfSpeechExtractPlugin
  • KuromojiPartOfSpeechExtractProcessor

IngestKuromojiPartOfSpeechExtractPlugin

Pluginというクラスは、プラグインをNodeのModuleとして登録する処理を書くクラスとなります。 生成してすぐは、次のような形になっています。(※importやクラス定義の部分は省略しています。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
    public static final Setting<String> YOUR_SETTING =
            new Setting<>("ingest.kuromoji_part_of_speech_extract.setting", "foo", (value) -> value, Setting.Property.NodeScope);

    @Override
    public List<Setting<?>> getSettings() {
        return Arrays.asList(YOUR_SETTING);
    }

    public void onModule(NodeModule nodeModule) throws IOException {
        nodeModule.registerProcessor(KuromojiPartOfSpeechExtractProcessor.TYPE,
                (registry) -> new KuromojiPartOfSpeechExtractProcessor.Factory());
    }
...

YOUR_SETTINGプロパティとgetSettings()メソッドはelasticsearch.ymlで指定したい設定を記述する場合の例になります。今回は特に必要ないので両方削除しました。 最終系はGitHubのコードをご覧ください。

KuromojiPartOfSpeechExtractProcessor

Processorは実際にIngest Nodeで行う処理を書くところです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    public static final String TYPE = "kuromoji_part_of_speech_extract";

    private final String field;
    private final String targetField;

    public KuromojiPartOfSpeechExtractProcessor(String tag, String field, String targetField) throws IOException {
        super(tag);
        this.field = field;
        this.targetField = targetField;
    }

    @Override
    public void execute(IngestDocument ingestDocument) throws Exception {
        String content = ingestDocument.getFieldValue(field, String.class);
        // TODO implement me!
        ingestDocument.setFieldValue(targetField, content);
    }

    @Override
    public String getType() {
        return TYPE;
    }

    public static final class Factory extends AbstractProcessorFactory<KuromojiPartOfSpeechExtractProcessor> {

        @Override
        public KuromojiPartOfSpeechExtractProcessor doCreate(String processorTag, Map<String, Object> config) throws Exception {
            String field = readStringProperty(TYPE, processorTag, config, "field");
            String targetField = readStringProperty(TYPE, processorTag, config, "target_field", "default_field_name");

            return new KuromojiPartOfSpeechExtractProcessor(processorTag, field, targetField);
        }
    }

TYPEIngest APIのPipelineでProcessorを指定するときに使う名前になります。ここは、cookiecutterの時にprocessor_typeに入力した文字列になっています。 kuromoji_part_of_speech_extractだと長いので、kuromoji_pos_extractに変えました。

execute()メソッドに// TODO implement me!とあります。 この部分に実際の処理を記述していきます。

あとは、FactoryクラスでIngest APIで指定された設定項目を読み込みます。 今回作成したelasticsearch-ingest-kuromoji-pos-extractでは品詞を指定する必要があるので、pos_tagsを指定できるように処理を追加しました。

私が実装したものの説明をするとちょっと長くなりそうなので、GitHubのコードをご覧ください。

テストのコーディング

テストのクラスもテンプレートで生成されています。

  • KuromojiPartOfSpeechExtractProcessorTests
  • KuromojiPartOfSpeechExtractRestIT

KuromojiPartOfSpeechExtractProcessorTests

Processorクラスのテストになります。生成直後は次のような感じです。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void testThatProcessorWorks() throws Exception {
    Map<String, Object> document = new HashMap<>();
    document.put("source_field", "fancy source field content");
    IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document);

    KuromojiPartOfSpeechExtractProcessor processor = new KuromojiPartOfSpeechExtractProcessor(randomAsciiOfLength(10), "source_field", "target_field");
    processor.execute(ingestDocument);
    Map<String, Object> data = ingestDocument.getSourceAndMetadata();

    assertThat(data, hasKey("target_field"));
    assertThat(data.get("target_field"), is("fancy source field content"));
    // TODO add fancy assertions here
}

テストメソッドも実装されていますが、パラメータの追加の設定処理やアサーションが書かれてません。 実装に合わせて、アサーションや設定処理を追加しましょう。

KuromojiPartOfSpeechExtractRestIT

こちらはIntegration Testになります。 実際にElasticsearchに対して外部からAPIを叩くような感じです。 APIを叩くときに利用するJSONの設定やアサーションはsrc/test/resourcesにyamlファイルがあります。

  • 10_basic.yaml
  • 20_kuromoji_part_of_speech_extract_processor.yaml

10_basic.yamlはプラグインがインストールされているかの確認のテストです。特に変更する必要はないです。

20_kuromoji_part_of_speech_extract_processor.yamlは実際にコーディングしたProcessorが動くかどうかのテストです。

テストの内容については、GitHubのコードをご覧ください。

テストの実行とZipの生成

テストの実行とZipの生成は次のコマンドを実行すればOKです。

1
gradle check

テストに問題があった場合は、コケますし、問題なければSUCCESSと表示が出ます。 成功した場合はbuild/distributions/というディレクトリにzipファイルができています。 これをElasticsearchのpluginコマンドでインストールすれば動きます。

1
bin/plugin install file:///path/to/elasticsearch-ingest-kuromoji-pos-extract/build/distribution/ingest-kuromoji_part_of_speech_extract-0.0.1-SNAPSHOT.zip

kuromoji_pos_extractの利用方法

Ingest APIには便利なSimulate Pipeline APIがあります。

ということで、mecab-ipadic-NEologdにあったサンプルの文章を使って、使い方の説明です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
POST _ingest/pipeline/_simulate
{
  "pipeline" : {
    "description" : "kuromoji neologd extract test",
    "processors" : [
      {
        "kuromoji_pos_extract" : {
        "field" : "body",
        "target_field" : "noun_field",
        "pos_tags" : [
          "名詞-固有名詞-組織",
          "名詞-固有名詞-一般",
          "名詞-固有名詞-人名-一般",
          "名詞-固有名詞-地域-一般",
          "名詞-固有名詞-地域-国"
          ]
        }
      }
      ]
  },
  "docs" : [
    {
      "_index": "index",
      "_type": "type",
      "_id": "id",
      "_source": {
        "body" : "10日放送の「中居正広のミになる図書館」(テレビ朝日系)で、SMAPの中居正広が、篠原信一の過去の勘違いを明かす一幕があった。"
      }
    }
    ]
}

結果はこちら。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "docs": [
    {
      "doc": {
        "_index": "index",
        "_id": "id",
        "_type": "type",
        "_source": {
          "noun_field": [
            "10日",
            "中居正広のミになる図書館",
            "テレビ朝日",
            "SMAP",
            "中居正広",
            "篠原信一"
          ],
          "body": "10日放送の「中居正広のミになる図書館」(テレビ朝日系)で、SMAPの中居正広が、篠原信一の過去の勘違いを明かす一幕があった。"
        },
        "_ingest": {
          "timestamp": "2016-07-22T06:18:49.007+0000"
        }
      }
    }
  ]
}

noun_fieldに固有名詞の単語が抜き出せているのがわかるかと思います。

Alexのテンプレートで困った点

テンプレートは便利だったのですが、processor_type_を使用したタイプ名を指定すると次のような問題(?)が発生しました。

  • クラス名がKuromoji_part_of_speech_extractProcessorとなってしまう

深刻な問題ではないのですが、JavaだとCamel Caseが普通なのでちょっと気になって。 ということで、プルリク作って出してみました。まだ取り込まれてないかな。

取り込み前に使いたい方は以下のコマンドを実行してください。 processor_class_nameという項目が増えています。 デフォルトだとprocessor_type_の部分を取り除きつつCamel Caseにしたものが入ります。

1
cookiecutter gh:johtani/cookiecutter-elasticsearch-ingest-processor

まとめ

ということで、とりあえず作ってみましたというものになります。 特徴的な単語(固有名詞だけ)を抜き出して、別のフィールドにできるので、タグみたいなものをこれを使って前処理で作れるようになるかなぁと。

参考ブログ(元ネタ?)

インスパイア元となったブログです。

Comments