Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Elastic Stackを導入することでRDB検索を高速化する

こんにちは、Elastic Certified EngineerHiroshi Yoshioka です。この記事は Elastic stack (Elasticsearch) Advent Calendar 2019 の16日目の記事になります。

はじめに

本日のテーマは「Elastic Stackを活用したRDB検索の高速化」です。RDBを用いたシステムにおいて、データサイズが肥大化すると一般に検索性能が劣化します。特にLike演算子を用いたテキスト検索は、劣化の度合いが顕著です。

システム構成を変更せずに性能改善を行う場合、データスキーマSQLを見直すことである程度の性能改善が可能ですが、スキーマ変更に伴うアプリケーション改修量が多く、またそれに見合う性能改善を得られない場合もあります。一方で、Elastic Stackを導入し、検索処理部分のみをElasticsearchに移行するアプローチは、アプリケーション改修量は小さく、また飛躍的な性能改善を実現することが可能です。

検索性能改善:Before/After

以下は、過去Elastic Stackで性能改善した事例の性能数値です。

事例(1)

  • RDBMySQL
  • 蓄積データ:3200万件
検索時間(Before) 検索時間(After) 検索速度
30~60秒 50~200ミリ秒 300~600倍に向上

事例(2)

検索時間(Before) 検索時間(After) 検索速度
60~220秒 100~200ミリ秒 600~1000倍に向上

コメント

既存システムが遅すぎでは?という指摘があるかもしれませんが、データ設計/データサイズ/SQLによっては検索に数十秒かかるシステムをよく見かけます。これをElastic Stackを導入することで、検索時間を100ミリ秒前後に抑えることができました。

システム構成

Before/Afterのシステム構成は以下のようになります。

既存システム構成
既存システム構成
Elastic Stack導入後システム構成
Elastic Stack導入後システム構成

「Elastic Stack導入によるRDB検索高速化」のポイント

  • データは非正規化してElasticsearchに持たせる(Nested、Parent/Childは使わない)
  • 非正規化によりインデックスサイズがRDBのデータサイズよりも大きくなるので、適宜チューニングをしてサイズのダイエットをする
  • Elasticsearchへのデータ同期はLogstashを利用する
  • Elasticsearchには、検索機能として最低限必要な情報のみをデータ移行しインデックスサイズを抑える
  • Elasticsearchは検索結果のIDのみを返却し、画面に必要なデータはRDBから取得する。
  • 画面に必要なすべてのデータをElasticsearchに持たせるアプローチの方がシンプルですが、インデックスサイズが膨らみ、それに伴って必要なデータノード数も増えることに注意

最後に

Elastic Stackを導入する上で、インデックス/クエリ設計、データ同期(初期移行、定期同期、障害復旧)、運用設計など、検討事項は多々ありますが、書くと長くなりすぎるので、今回はこの辺で終ります。 RDB検索の高速化に興味がある方は、以下ページにてぜひお問い合わせください。 www.endosnipe.com


Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

ユーザに最高の検索体験を提供したいエンジニアWanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com

Logstash の conf ファイルをリグレッションテストする

こんにちは。アキバです。

この記事は、 Elastic Stack (Elasticsearch) Advent Calendar 2019 の14日目です。
qiita.com

今年の8月にElastic Certified Engineerになることができました。
ということで、今年の Advent Calendar は Elastic Stack のカレンダーにお邪魔します。

構築したシステムの「正しさ」

突然ですが、Elastic Stackを使って構築する/したシステムの「正しさ」を検証していますか?
きちんと検証できていないと、インフラ/ミドルウェアの障害は影響も大きく、問題になりがちです。

そもそも、「正しさ」を検証するべき箇所は何があるでしょうか?


一つは、「設定の正しさ」があるでしょう。

  • OS環境
  • Elasticsearchのパラメータ(elasticsearch.ymlの記載や _settings API など)
  • Index Mapping

など。

これらについては、静的なファイルチェックに加えて、構築後の環境に対する Serverspecや、Karateを使ったAPIテストなどが有効そうです。

※Karateを使ったAPIテストについては、以前にshin0higuchiが書いた以下の記事も見てください
acro-engineer.hatenablog.com


もう一つは、「ロジックの正しさ」がありますね。

JavaPythonなど、Elasticsearchを使うアプリケーションのロジックはユニットテストをすることを考えますが、Logstashのconfはどうでしょうか?

書いてみたことがある方は分かると思いますが、Logstashのconf(つまりパイプライン)は、れっきとしたロジックであると言えます。
すなわち、入力(input)があり、処理(filter)があり、出力(output)があり、filter部分にロジックがあるわけです。

今回は、この「Logstash conf (の filter) をテストする」ことを考えてみたいと思います。

logstash-test-runner

Logstash の conf それ自体は、定義ファイルのような形で作成・デプロイするので、このファイルを単体で試験しようとする人は多くないようです。

しかしながら、少し検索してみたら、以下のような、node.js ベースのTest Runnerを作っている方がいました。

github.com

原理はシンプルで、以下の3種類

  1. 入力となるログデータ
  2. テストしたいconfファイル
  3. 出力として期待する正解データ

を1セットとして用意すると、実際に 2. の conf を実行して得られたデータと 3. の正解データを比較して合否を判定してくれるというものです。

Logstashの実行は、Elastic社の純正Dockerイメージを取得して行うため、実行環境にあらかじめLogstashをインストールしておく必要はありませんし、バージョンの切り替えも簡単に行えます。

テストの仕組みとしては手軽で面白そうです。これを実際に使ってみましょう。

環境の準備

実行する環境は、Linuxbash シェルスクリプトが動作する環境)にします。

今回は、Azure上にVM(OSはUbuntu 18.04.3 LTS)を立てましたが、以下の要件を満たせるLinux環境を作れるなら、オンプレでも問題ないと思います。

※logstash-test-runner のページから抜粋

  • NodeJS > v8
  • Docker
  • Bash > v4

なお、前述の通り、ツール動作時にElastic社のDockerイメージを取得するため、インターネットに接続できる環境にしてください。

今回、私が構築した環境は以下の通りです。
本当は、node.jsはv11とかを入れておくべきなのでしょうけれども、手抜きで済ませております。

$ node -v
v8.10.0

$ docker -v
Docker version 19.03.5, build 633a0ea838

$ bash --version
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)

早速動かしてみる

まずは、git を使って、テストツールをクローンします。

$ git clone https://github.com/agolo/logstash-test-runner.git
$ cd logstash-test-runner

デフォルトで用意されているテスト(__tests__)を実行してみます。

テストは、test.sh というスクリプトから実行します。(お試しで作ったスクリプトではありません)
test.shの第1引数にディレクトリを指定すると、その配下のディレクトリをテストケースとして認識するようです。

今回の環境では、dockerコマンドはrootで実行する必要があるため、test.shもsudoをつけて実行します。

また、実行の際には、明示的にbashで実行する必要がありました。(この点はディストリビューションにもよるでしょう)

$ sudo bash test.sh __tests__

f:id:acro-engineer:20191215070512p:plain

通りました。簡単ですね!
初回はDockerイメージを取得するのに少々時間がかかりますが、2回目以降は各テスト30~60秒で通ります。
(テストにかかる時間の半分ぐらいは、Logstashの起動時間ですが)

Logstashのバージョン指定

デフォルトではlogstashのバージョンが5.5.1なのですが、第2引数を指定することで変えることができます。

せっかくなので、最新バージョンである7.5で試してみましょう。

$ sudo bash test.sh __tests__ docker.elastic.co/logstash/logstash:7.5.0

f:id:acro-engineer:20191215070515p:plain

おや、mongoのテストが失敗しました。
差分が出ると、上記のような diff結果が表示されます。

どこから失敗するようになるのでしょうか?

ということで、それぞれのバージョンで試してみました。
(これがコマンドラインオプションだけで試せるのはとても便利ですね)

バージョン 結果
5.5.1 成功
6.0.0 成功
6.5.0 成功
6.8.0 成功
7.0.0 失敗
7.5.0 失敗

上の結果から、6.x → 7.0 にバージョンアップするときに Logstash の動作仕様が変わったということがわかります。

差分を修正する

前述の差分は、よく見ると mongo テストの出力データの先頭2行が入れ替わっていることがわかります。

本来は、「なぜ入れ替わったのか」の原因を特定し、Logstash conf を修正するなどして解決するべきですが、
今回の入れ替わりがなぜ発生するのかは、少し調べてみただけではわかりませんでした。

ということで、(本来は良くない対処ですが)正解データを修正して、テストが通るようになるか試してみましょう。

mongo テストの正解データは、 __tests__/mongo/output.log です
これを開いて編集します。

{"conn_type":"COMMAND","querytime_ms":109,"@message":"command production.feeds command: find { find: \"feeds\", filter: { _id: \"abcdefg\" }, limit: 1, shardVersion: [ Timestamp 0|0, ObjectId('000000000000000000000000') ] } planSummary: IDHACK keysExamined:1 docsExamined:1 cursorExhausted:1 numYields:0 nreturned:1 reslen:2031 locks:{ Global: { acquireCount: { r: 2 }, acquireWaitCount: { r: 1 }, timeAcquiringMicros: { r: 108897 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_command","@timestamp":"2017-08-24T18:16:02.110Z","mongo_message":"command production.feeds command: find { find: \"feeds\", filter: { _id: \"abcdefg\" }, limit: 1, shardVersion: [ Timestamp 0|0, ObjectId('000000000000000000000000') ] } planSummary: IDHACK keysExamined:1 docsExamined:1 cursorExhausted:1 numYields:0 nreturned:1 reslen:2031 locks:{ Global: { acquireCount: { r: 2 }, acquireWaitCount: { r: 1 }, timeAcquiringMicros: { r: 108897 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_command","loglevel":"I","@version":"1","host":"testing_host","context":"conn88987","message":"2017-08-24T18:16:02.110+0000 I COMMAND  [conn88987] command production.feeds command: find { find: \"feeds\", filter: { _id: \"abcdefg\" }, limit: 1, shardVersion: [ Timestamp 0|0, ObjectId('000000000000000000000000') ] } planSummary: IDHACK keysExamined:1 docsExamined:1 cursorExhausted:1 numYields:0 nreturned:1 reslen:2031 locks:{ Global: { acquireCount: { r: 2 }, acquireWaitCount: { r: 1 }, timeAcquiringMicros: { r: 108897 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_command 109ms","tags":["_jsonparsefailure"],"timestamp":"2017-08-24T18:16:02.110+0000"}
{"conn_type":"ASIO","message":"2017-08-24T21:55:01.204+0000 I ASIO     [NetworkInterfaceASIO-ShardRegistry-0] Successfully connected to mydatabase.com:27019, took 66ms (1 connections now open to mydatabase.com:27019)","tags":["_jsonparsefailure"],"querytime_ms":66,"@message":"Successfully connected to mydatabase.com:27019, took (1 connections now open to mydatabase.com:27019)","@timestamp":"2017-08-24T21:55:01.204Z","mongo_message":"Successfully connected to mydatabase.com:27019, took (1 connections now open to mydatabase.com:27019)","loglevel":"I","@version":"1","host":"testing_host","context":"NetworkInterfaceASIO-ShardRegistry-0","timestamp":"2017-08-24T21:55:01.204+0000","mongo_message2":" (1 connections now open to mydatabase.com:27019)"}
{"conn_type":"NETWORK","@message":"received client metadata from 40.70.67.250:43270 conn99615: { driver: { name: \"NetworkInterfaceASIO-TaskExecutorPool-1\", version: \"3.4.7\" }, os: { type: \"Linux\", name: \"Ubuntu\", architecture: \"x86_64\", version: \"16.04\" } }","@timestamp":"2017-08-24T22:01:36.933Z","mongo_message":"received client metadata from 40.70.67.250:43270 conn99615: { driver: { name: \"NetworkInterfaceASIO-TaskExecutorPool-1\", version: \"3.4.7\" }, os: { type: \"Linux\", name: \"Ubuntu\", architecture: \"x86_64\", version: \"16.04\" } }","loglevel":"I","@version":"1","host":"testing_host","context":"conn99615","message":"2017-08-24T22:01:36.933+0000 I NETWORK  [conn99615] received client metadata from 40.70.67.250:43270 conn99615: { driver: { name: \"NetworkInterfaceASIO-TaskExecutorPool-1\", version: \"3.4.7\" }, os: { type: \"Linux\", name: \"Ubuntu\", architecture: \"x86_64\", version: \"16.04\" } }","tags":["_jsonparsefailure"],"timestamp":"2017-08-24T22:01:36.933+0000"}

これを、以下のように変えます。(つまり、COMMAND と ASIO の2行を入れ替えただけです)

{"conn_type":"ASIO","message":"2017-08-24T21:55:01.204+0000 I ASIO     [NetworkInterfaceASIO-ShardRegistry-0] Successfully connected to mydatabase.com:27019, took 66ms (1 connections now open to mydatabase.com:27019)","tags":["_jsonparsefailure"],"querytime_ms":66,"@message":"Successfully connected to mydatabase.com:27019, took (1 connections now open to mydatabase.com:27019)","@timestamp":"2017-08-24T21:55:01.204Z","mongo_message":"Successfully connected to mydatabase.com:27019, took (1 connections now open to mydatabase.com:27019)","loglevel":"I","@version":"1","host":"testing_host","context":"NetworkInterfaceASIO-ShardRegistry-0","timestamp":"2017-08-24T21:55:01.204+0000","mongo_message2":" (1 connections now open to mydatabase.com:27019)"}
{"conn_type":"COMMAND","querytime_ms":109,"@message":"command production.feeds command: find { find: \"feeds\", filter: { _id: \"abcdefg\" }, limit: 1, shardVersion: [ Timestamp 0|0, ObjectId('000000000000000000000000') ] } planSummary: IDHACK keysExamined:1 docsExamined:1 cursorExhausted:1 numYields:0 nreturned:1 reslen:2031 locks:{ Global: { acquireCount: { r: 2 }, acquireWaitCount: { r: 1 }, timeAcquiringMicros: { r: 108897 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_command","@timestamp":"2017-08-24T18:16:02.110Z","mongo_message":"command production.feeds command: find { find: \"feeds\", filter: { _id: \"abcdefg\" }, limit: 1, shardVersion: [ Timestamp 0|0, ObjectId('000000000000000000000000') ] } planSummary: IDHACK keysExamined:1 docsExamined:1 cursorExhausted:1 numYields:0 nreturned:1 reslen:2031 locks:{ Global: { acquireCount: { r: 2 }, acquireWaitCount: { r: 1 }, timeAcquiringMicros: { r: 108897 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_command","loglevel":"I","@version":"1","host":"testing_host","context":"conn88987","message":"2017-08-24T18:16:02.110+0000 I COMMAND  [conn88987] command production.feeds command: find { find: \"feeds\", filter: { _id: \"abcdefg\" }, limit: 1, shardVersion: [ Timestamp 0|0, ObjectId('000000000000000000000000') ] } planSummary: IDHACK keysExamined:1 docsExamined:1 cursorExhausted:1 numYields:0 nreturned:1 reslen:2031 locks:{ Global: { acquireCount: { r: 2 }, acquireWaitCount: { r: 1 }, timeAcquiringMicros: { r: 108897 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_command 109ms","tags":["_jsonparsefailure"],"timestamp":"2017-08-24T18:16:02.110+0000"}
{"conn_type":"NETWORK","@message":"received client metadata from 40.70.67.250:43270 conn99615: { driver: { name: \"NetworkInterfaceASIO-TaskExecutorPool-1\", version: \"3.4.7\" }, os: { type: \"Linux\", name: \"Ubuntu\", architecture: \"x86_64\", version: \"16.04\" } }","@timestamp":"2017-08-24T22:01:36.933Z","mongo_message":"received client metadata from 40.70.67.250:43270 conn99615: { driver: { name: \"NetworkInterfaceASIO-TaskExecutorPool-1\", version: \"3.4.7\" }, os: { type: \"Linux\", name: \"Ubuntu\", architecture: \"x86_64\", version: \"16.04\" } }","loglevel":"I","@version":"1","host":"testing_host","context":"conn99615","message":"2017-08-24T22:01:36.933+0000 I NETWORK  [conn99615] received client metadata from 40.70.67.250:43270 conn99615: { driver: { name: \"NetworkInterfaceASIO-TaskExecutorPool-1\", version: \"3.4.7\" }, os: { type: \"Linux\", name: \"Ubuntu\", architecture: \"x86_64\", version: \"16.04\" } }","tags":["_jsonparsefailure"],"timestamp":"2017-08-24T22:01:36.933+0000"}

これで、もう一度実行してみましょう。

$ sudo bash test.sh __tests__ docker.elastic.co/logstash/logstash:7.5.0

f:id:acro-engineer:20191215070519p:plain

成功するようになりました。

まとめ

logstash-test-runner を用いることで、conf ファイルをリグレッションテストする仕組みが比較的簡単に作れることがわかりました。

このツールは、ツール出力を正解データと差分比較することで合否判定をするため、正解データを予め用意しておくことが必要になります。
通常、Logstashの出力で正解データを手動で作成して用意することは現実的ではありませんので、「一回実行してうまくいったログ」を正解データとして使用することになるでしょう。

そういった意味で、このツールは「リグレッションテスト」用途に向いていると言えるでしょう。
それでも、Logstash の conf に対して、変更による破壊が起きていないかを早い段階で見つけられるようになるので、安心感が得られると思いました。


さて、そうすると、CIを回したくなりますね。
このツールの結果をJenkinsなどでレポート表示するには、多少解析する必要がありそうです。

この辺りの仕組みが作れないか、いずれ検討してみたいと思います。


では。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com

全文検索で文書の新しさを考慮したスコアリング

皆さんこんにちは。@Ssk1029Takashiです。
この記事はElastic Stack (Elasticsearch) Advent Calendar 2019の13日目になります。
qiita.com

何か調べ物をしているとき、見つけた記事が古く使えないということが良くあります。
例えばElasticsearchのクエリを調べていたら、ver5時代の記事ですでに仕様が変わっていたりなど。。。
検索結果としては基本的には時系列が新しいものを優先して出してほしいことが多いです。

このように、検索システムでは基本的には新しい記事、なおかつ検索キーワードと関連度が高い記事を優先して出してほしいということがあります。
この時、単純に時系列でソートすると関連度を考慮できないため、検索スコアにいい感じに時系列情報を組み込む必要があります。
Elasticsearchでは、この問題をscript score queryを使って解決できます。

script score queryとは

Elasticsearch ver7から追加されたクエリで、scoreの値をpainless scriptで書くことができます。
www.elastic.co

例えば、以下のクエリで検索するとします。

GET /_search
{
    "query" : {
        "script_score" : {
            "query" : {
                "match": { "message": "elasticsearch" }
            },
            "script" : {
                "source" : "doc['likes'].value / 10 "
            }
        }
     }
}

このクエリでは、各検索結果のscoreはlikeフィールドの値を10で割った値になります。
このように検索クエリの中で、scoreの値をカスタマイズできるのがscript score queryです。

script score queryで時系列を考慮するには

script score queryでは、いくつかデフォルトで使用できる関数があるのですが、今回はその中のdecay関数を使用します。

decay関数の簡単な説明

decay関数とは名前の通り、特定の値に基づいて検索スコアを減衰させる関数です。
decay関数は適用できるデータ型によって3種類に分けられます。
1. 数値に基づきスコアを減衰させる関数
2. 地理的な距離に基づきスコアを減衰させる関数
3. 時系列に基づきスコアを減衰させる関数

また、それぞれのdecay関数には減衰値の計算方法が3種類(線形、ガウス分布正規分布)用意されています。
例えば、時系列に基づくdecay関数は以下の3つです。
1. decayDateLinear
2. decayDateExp
3. decayDateGauss

それぞれどのように値が減衰していくかのイメージが公式ドキュメントにあります。
https://www.elastic.co/guide/en/elasticsearch/reference/7.5/images/decay_2d.png
Function score query | Elasticsearch Reference [7.5] | Elastic
この中から、システム要件に合ったものを選びます。

実際のクエリは以下のような形になります。

{
  "query": {
    "script_score": {
      "query": {
        "match_all": {}
      },
      "script": {
        "source": "decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['timestamp'].value)",
        "params": {
          "origin": "2019-12-13T00:00:00Z",
          "scale": "60d",
          "offset": "0",
          "decay": 0.99
        }
      }
    }
  }
}

クエリ中で使用している引数は以下の4つになります。
origin: 減衰を計算する原点となる日時。この日時とドキュメントの日時がどれくらい離れているかでスコアが計算されます。
scale: 原点から距離を計算するときの単位。
offset: スコアを減衰させない範囲。
decay: どれくらいスコアを減衰させるか。

このようにscriptとして関数を呼び出すことで、scoreを調整することができます。

実際に試してみる。

データは以下の3つを投入します。

timestamp title content
2019-12-08 12:00:00 Micrometerで取得したデータをKibanaで可視化してみました Elastic Stackを活用しているAcroquestとしてはせっかくElasticsearchにも保存できるのにこれはもったいない…なら、うちで作るしかない!ということでMicrometer用のKibanaダッシュボードを作ってみました。
2018-12-20 12:00:00 ElasticsearchのRanking Evaluation APIについて整理してみた Ranking Evaluation APIは、検索クエリに対する検索結果の妥当性を評価するためのAPIです。Elasticsearchのバージョン6.2以降で利用することができます。
2018-05-08 12:00:00 Elasticsearchの圧縮方式の比較 Elasticsearchを使っているとストレージの使用量を節約したいと思う方は多いのではないでしょうか。Elasticsearchはデータを格納するときにデフォルトでLZ4という圧縮方式でデータ圧縮を行っていますが、実はLZ4よりも圧縮率の高いbest_compressionという圧縮方式を利用することもできます。

まずは、普通に検索してみます。

GET datetime_docs/_search
{
  "query": {
    "multi_match": {
      "query": "elasticsearch",
      "fields": ["title", "content"]
    }
  }
}

結果

{
        "_index" : "datetime_docs",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.5543933,
        "_source" : {
          "title" : "Elasticsearchの圧縮方式の比較",
          "content" : """
  Elasticsearchを使っているとストレージの使用量を節約したいと思う方は多いのではないでしょうか。
Elasticsearchはデータを格納するときにデフォルトでLZ4という圧縮方式でデータ圧縮を行っていますが、
実はLZ4よりも圧縮率の高いbest_compressionという圧縮方式を利用することもできます。
""",
          "timestamp" : "2018-05-08T12:00:00"
        }
      },
      {
        "_index" : "datetime_docs",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.46122766,
        "_source" : {
          "title" : "ElasticsearchのRanking Evaluation APIについて整理してみた",
          "content" : "  Ranking Evaluation APIは、検索クエリに対する検索結果の妥当性を評価するためのAPIです。\nElasticsearchのバージョン6.2以降で利用することができます。",
          "timestamp" : "2018-12-20T12:00:00"
        }
      },
      {
        "_index" : "datetime_docs",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.150402,
        "_source" : {
          "title" : "Micrometerで取得したデータをKibanaで可視化してみました",
          "content" : "  Elastic Stackを活用しているAcroquestとしてはせっかくElasticsearchにも保存できるのにこれはもったいない…なら、うちで作るしかない!\nということでMicrometer用のKibanaダッシュボードを作ってみました。",
          "timestamp" : "2019-12-08T12:00:00Z"
        }
      }

結果としては、キーワードが頻繁に出てくるドキュメントが上位に来ていますが、古いものが上位に来てしまっています。
これを、新しいドキュメントがより上位に来やすくします。

GET datetime_docs/_search
{
  "query": {
    "script_score": {
      "query": {
        "multi_match": {
          "query": "elasticsearch",
          "fields": ["title","content"]
        }
      },
      "script": {
        "source": "_score + decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['timestamp'].value)",
        "params": {
          "origin": "2019-12-13T00:00:00Z",
          "scale": "30d",
          "offset": "0",
          "decay": 0.99
        }
      }
    }
  }
}

script内の_score変数にはquery内で実行した単語検索によるスコアが入ります。
つまり、このクエリでは単語検索のスコア+時間で減衰したスコアの値を最終的なスコアにしています。
こちらの結果は以下のようになります。

      {
        "_index" : "datetime_docs",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.1501759,
        "_source" : {
          "title" : "Micrometerで取得したデータをKibanaで可視化してみました",
          "content" : "  Elastic Stackを活用しているAcroquestとしてはせっかくElasticsearchにも保存できるのにこれはもったいない…なら、うちで作るしかない!\nということでMicrometer用のKibanaダッシュボードを作ってみました。",
          "timestamp" : "2019-12-08T12:00:00Z"
        }
      },
      {
        "_index" : "datetime_docs",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.7012034,
        "_source" : {
          "title" : "ElasticsearchのRanking Evaluation APIについて整理してみた",
          "content" : "  Ranking Evaluation APIは、検索クエリに対する検索結果の妥当性を評価するためのAPIです。\nElasticsearchのバージョン6.2以降で利用することができます。",
          "timestamp" : "2018-12-20T12:00:00"
        }
      },
      {
        "_index" : "datetime_docs",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.57671785,
        "_source" : {
          "title" : "Elasticsearchの圧縮方式の比較",
          "content" : """
  Elasticsearchを使っているとストレージの使用量を節約したいと思う方は多いのではないでしょうか。
Elasticsearchはデータを格納するときにデフォルトでLZ4という圧縮方式でデータ圧縮を行っていますが、
実はLZ4よりも圧縮率の高いbest_compressionという圧縮方式を利用することもできます。
""",
          "timestamp" : "2018-05-08T12:00:00"
        }
      }

新しいドキュメントを上位に出すことができました。
あまりにも新しいものが上位に来すぎてしまう場合には、scaleやdecayの値を調整することで、検索結果をチューニングすることができます。

まとめ

script score queryのdecay関数を使用して、文書の新しさを考慮した検索を簡単に実現しました。
クエリで簡単にスコアを調整できるのは、検索システムの運用上非常に便利ですね。
script score queryにはほかにも便利な関数がいくつか実装されているので、ぜひ活用してみてください。
それではまた。

明日は、mac_akibaさんの記事になります。ぜひお楽しみに!

Normalizerを利用してkeyword型のデータを加工する

こんにちは、ノムラです。
この記事はElastic Stack (Elasticsearch) Advent Calendar 2019の9日目の記事になります。

はじめに

データを可視化、集計する際以下のようなデータが別々のデータとして扱われ困ったことはないでしょうか?

  • 〇〇(株) と 〇〇株式会社
  • 斎藤 と 齋藤
  • Elasticsearch と elasticsearch
(株) と 株式会社で困った例

f:id:acro-engineer:20191209001620p:plain

本来であれば、アクロクエストテクノロジー(株)とアクロクエストテクノロジー株式会社は同一の会社名として扱いたいです。
一見、ElasticsearchにはAnalyzer機能があるため、上記問題は簡単に解決することができるように思えます。
しかし上記のAnalyzer機能はText型にのみ適用可能な機能です。
そのため、上の例のような会社名や苗字の集計時によく設定されるkeyword型には適用することができません。
※Text型として扱うと、〇〇株式会社 ⇒ 〇〇 + 株式会社 の2単語として集計されてしまうため集計で不都合が生じます

そのような場合は、Normalizerを使いましょう。

Normalizerについて

Normalizerとは

Normalizerはkeyword型に適用可能な、Analyzerに相当する機能になります。
www.elastic.co

Normalizerには、char_filterとfilterを定義することができ、
異体字の変換や、大文字化小文字化等の加工ができます。
tokenizerは、keyword tokenizerが適用されます。
※Normalizerはインデキシング時だけでなく、検索時にも入力キーワードへ適用されます。

Normalizerの設定方法

Index Mappingのsettingsに下記のように設定します。

PUT sample_index
{
  "settings": {
    "analysis": {
      "normalizer": {
        "my_normalizer": {
          "type": "custom",
          "char_filter": [
            "my_char_filter"
          ],
          "filter": [
            "lowercase"
          ]
        }
      },
      "char_filter": {
        "my_char_filter": {
          "type": "mapping",
          "mappings": [
            "(株) => 株式会社",
            "齋 => 斎"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "company_name": {
        "type": "keyword",
        "normalizer": "my_normalizer"
      },
      "first_name": {
        "type": "keyword",
        "normalizer": "my_normalizer"
      },
      "product_name": {
        "type": "keyword",
        "normalizer": "my_normalizer"
      }
    }
  }
}

char_filterのmappingsで具体的にどのように変換するかを定義します。
ここでは、

  • (株) を 株式会社 へ
  • 齋 を 斎 へ

と変換を定義しています。
また、filterでlowercaseへの変換を定義しています。

データの登録

サンプルデータとして下記のデータを登録します。

# (株) ⇒ 株式会社
PUT sample_index/_doc/1
{
  "company_name": "アクロクエストテクノロジー(株)"
}
PUT sample_index/_doc/2
{
  "company_name": "アクロクエストテクノロジー株式会社"
}

# 齋 ⇒ 斎
PUT sample_index/_doc/3
{
  "name": "斎藤"
}
PUT sample_index/_doc/4
{
  "name": "齋藤"
}

# 大文字小文字
PUT sample_index/_doc/5
{
  "product_name": "Elasticsearch"
}
PUT sample_index/_doc/6
{
  "product_name": "elasticsearch"
}

適用結果

それぞれ以下のように、

でそれぞれ統一されて集計されています。

(株) ⇒ 株式会社

# 会社名で集計
GET sample_index/_search
{
  "size": 0, 
  "aggs": {
    "company_name": {
      "terms": {
        "field": "company_name",
        "size": 10
      }
    }
  }
}

# 集計結果
{
  "took" : 1045,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "company_name" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "アクロクエストテクノロジー株式会社",
          "doc_count" : 2
        }
      ]
    }
  }
}

齋 ⇒ 斎

# 名前で集計
GET sample_index/_search
{
  "size": 0, 
  "aggs": {
    "name": {
      "terms": {
        "field": "name",
        "size": 10
      }
    }
  }
}

# 集計結果
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "name" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "斎藤",
          "doc_count" : 2
        }
      ]
    }
  }
}

大文字 ⇒ 小文字

# 製品名で集計
GET sample_index/_search
{
  "size": 0, 
  "aggs": {
    "name": {
      "terms": {
        "field": "product_name",
        "size": 10
      }
    }
  }
}

# 集計結果
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "name" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "elasticsearch",
          "doc_count" : 2
        }
      ]
    }
  }
}

おわりに

今回はNormalizerを利用して、keyword型にchar_filterとfilterを適用し異体字や、大文字小文字を統一する方法について書きました。
私自身、Normalizerについて知るまではLogstashやスクリプト側で加工する必要があると思っており、
その点NormalizerはElasticsearchの設定のみで可能なので簡単で、とても便利だと感じました。

上記の例以外にも様々なことに利用可能だと思うので皆さんも是非触ってみてください。

10日目はhttps://qiita.com/froakie0021さんです。お楽しみに。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com

Micrometerで取得したデータをKibanaで可視化してみました

こんにちは、oogiです。

この記事はElastic Stack (Elasticsearch) Advent Calendar 2019の8日目の記事になります。

はじめに

Spring Bootアプリケーションの監視にMicrometerを使うことは多いと思います。
収集したデータはレジストリの切り替えによりPrometheusやElasticsearchなど保存先を選んで利用することができます。

で、収集したら当然可視化してみたいわけですが、残念ながらMicrometerで収集したデータをKibanaで可視化するためのダッシュボードが存在していないため、結局のところPrometeus&Grafanaが選ばれることが多いです。

Elastic Stackを活用しているAcroquestとしてはせっかくElasticsearchにも保存できるのにこれはもったいない…なら、うちで作るしかない!
ということでMicrometer用のKibanaダッシュボードを作ってみました。

Micrometer用Kibanaダッシュボード

早速ですが、作ったダッシュボードがこちらです。

Spring Boot Statistics

f:id:acro-engineer:20191203101247g:plain

Spring Boot Application

f:id:acro-engineer:20191203102105g:plain

作成にあたってはGrafanaの画面を参考にし、同様に以下の情報が見えるようにしました。

Spring Boot Statistics
  • 起動時刻、CPU使用率
  • メモリ使用量
  • GC
  • DBコネクション
  • HTTPリクエス
  • Tomcatのメトリクス
  • ログ出力
Spring Boot Application
  • リクエストおよびログ出力
  • URL別のリクエスト数およびレスポンスタイム
  • 別サービスへのHTTPアクセス

それぞれホスト名やアプリケーション名でフィルタできます。

今回作ったKibanaのダッシュボードを、利用したい環境でインポートすれば使えるように以下で公開していますので、ElasticsearchでMicrometerのデータを可視化してみたい方はお試しください。
Micrometer Kibana Dashboard

Elastic APM

さて、ここまでMicrometerで収集したデータをKibana上で可視化するダッシュボードを紹介しましたが、Elastic Stackを用いたアプリケーション監視と言えばElastic APMがあります。

Elastic APMはその名の通りElastic社が出しているAPMで、OSSで利用できます。
Javaを始めとするさまざまな言語に対応したAgentが存在し、Java Agentの場合は適用する際にソースコードの変更を必要としません。

また、RUMというフロントエンド向けのAgentも提供されており、Angularインテグレーションを使うことでフロントエンドの監視もできます。
しかもバックエンド側のエージェントとトレースIDを共有することができ、フロントエンドとバックエンドの分散トレーシングが可能です。

さらにElastic APM 7.4からはJavaのLogging libraryが提供されています。
これを既存のログ設定に追加することでAPMのAgentが設定するトレースIDがログにも連携され、APMとLogs UIとの連携もできるようになります。

これらについても試してみたかったので、今回はMicrometer用ダッシュボードに加えてElastic APMも同時に試してみることのできるデモ環境をSpring Petclinicベースで作成してみました。
Micrometer & Elastic APM Demo

REST APIに対するMicrometerでの監視に加えて、上記のフロントエンドとバックエンドの分散トレーシングやLogsとの連携も見ることができます。

f:id:acro-engineer:20191203102759p:plain

f:id:acro-engineer:20191203102826p:plain

ぜひMicrometerとElastic APMによる監視を試してみてください。

おわりに

今回はMicrometerで収集したデータをKibanaで可視化するダッシュボードと、そのMicrometer用ダッシュボードおよびElastic APMを利用したアプリケーション監視のサンプルを紹介しました。

最近はJavaよりも新しい言語がいろいろと出てきていますが、Javaにはこのように言語自体の機能だけでなく周辺ツールを含めた開発・運用環境が充実しているという特長があります。
その点を考慮すれば、開発環境から運用まで考慮したシステム開発においてJavaを選択するというのは十分なメリットがあるのではないかと思います。


9日目はbob_nomuさんです。お楽しみに。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com

GiNZA+Elasticsearchで係り受け検索の第一歩

急に冷え込んできてお布団が恋しい季節になってきました。
こんにちは。@Ssk1029Takashiです。
この記事は自然言語処理 Advent Calendarの6日目の記事になります。
qiita.com

全文検索システムは単語検索であることが多いですが、単語検索だけだと困ることもあります
症例検索を例にとって見てみましょう。
検索エンジンに以下の2つの文章が登録されているとします。
「ずっと胃がキリキリと痛い。ただ、熱は無く平熱のままだ。」
「昨日からとても頭が痛い。おまけに胃がむかむかする。」
この時、「胃が痛い」と検索したとき、通常の単語検索の場合だと両方ともヒットしてしまいますが、下の文章は意味としては異なる文章のためゴミになります。

この記事では、GiNZAとElasticsearchを使って意味的に正しい上の文章だけを拾ってくる仕組みを簡単に実現してみようと思います。

どうやって解決するか

概要は以下の図のようになります。

f:id:acro-engineer:20191203000159p:plain

1. 投入時には文章を係り受け解析をして、主語・述語のペアを抜き出します。
 例えば、「昨日から胃がキリキリと痛い。ただし、熱はない」という文章からは、「主語:胃、述語:痛い」「主語:熱、述語:ない」という2つのペアを抽出します。
 また、今回は修飾語や目的語などの係り受け関係は考慮せず、主語述語のみとして、修飾語は次回の課題とします。

2. 検索時には入力として、文章を想定します。
 入力された文章を係り受け解析して、投入時と同じく主語・述語のペアにして、同じペアを持つ文書を検索するようにします。
 また、同時に主語・述語以外の単語でも絞るために、単語検索も同時に実行します。

GiNZAとは

詳細に入る前にGiNZAについて簡単に説明します。
megagonlabs.github.io

GiNZAはリクルートさんと国立国語研究所さんが共同で開発した自然言語処理用のライブラリです。
欧米でよく使用されている自然言語処理ライブラリであるspaCyを日本語に対応させたものになります。

特長は以下の3点だと思います。
1. 形態素解析・依存構造解析・固有表現抽出・埋め込みベクトル等一つのライブラリで様々なタスクに対応している。
2. pip install一行でインストールが可能。
3. 高精度なモデルがプリセットとして提供されている。

簡単に言うなら、 一行でインストールできる高精度・高機能な日本語処理ライブラリです。

個人的には形態素解析にSudachiが使用されており、表記揺れなどにもデフォルトで対応できるのがとてもありがたいです。

係り受け解析

それでは、実際に文書から主語・述語を抽出していきます。
主語・述語を抽出する手順は以下のようになります。
1. 日本語用モデルをロードする
2. 入力された文章を解析する。(この段階で形態素解析・依存構造解析を行う)
3. 解析結果から主語・述語のペアを抜き出す。

準備

まずは、GiNZAを使う準備からです。
以下のコマンドでインストールします。

pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"

モデルのロード・日本語文章の解析

まず試しに、モデルのロード・解析だけをやってみましょう。

import spacy

nlp = spacy.load('ja_ginza')
doc = nlp('昨日から胃がキリキリと痛い。')

for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')

出力は以下のようになります。

0 昨日 昨日 NOUN 名詞-普通名詞-副詞可能 nmod 6
1 から から ADP 助詞-格助詞 case 0
2 胃 胃 NOUN 名詞-普通名詞-一般 nsubj 6
3 が が ADP 助詞-格助詞 case 2
4 キリキリ きりきり NOUN 名詞-普通名詞-一般 nmod 6
5 と と ADP 助詞-格助詞 case 4
6 痛い 痛い ADJ 形容詞-一般 ROOT 6
7 。 。 PUNCT 補助記号-句点 punct 6

表示している項目は、左から単語・見出し語・品詞タグ・品詞情報・依存関係ラベル・係り先の単語インデックスとなっています。
これらの項目をトークンとごとに出力しています。
この中にある、NOUN・ADPや、nmod・nsubjという文字列はUniversal Dependencyというプロジェクトで定義されているラベルになります。
係り受け関係を可視化すると次のようになります。
f:id:acro-engineer:20191203013929p:plain

Universal Dependencyとは

軽くUniversal Dependency(以下UD)について説明しておきたいと思います。
UDとは、構文構造を多言語間で統一しようという世界的な活動のことを差します。
その中で、NOUN(名詞)やVERB(動詞)などの品詞ラベルや、 nsubj(主語述語関係)・dobj(目的語関係)などの係り受け関係を定義しています。
GiNZAはデフォルトではUDで定義されたラベルで依存構造解析を行います。
日本語でのUDについては以下の論文で詳しく解説されています。図を眺めるだけでもイメージを掴めるためぜひ読んでみてください。
https://www.anlp.jp/proceedings/annual_meeting/2015/pdf_dir/E3-4.pdf

主語述語のペアを抜き出す

それでは、主語述語のペアを抜き出していきます。
上で述べたように、nsubjというラベルが主語述語の係り受けなので、nsubjのラベルを持っているトークンと係り受け先のトークンをペアとします。
ただし、今報告されている問題として、依存関係のラベルがnsubjのものがiobjになってしまう課題があるので、ここではiobjも対象にします。

実装は以下のようになります。

def parse_document(sentence, nlp):
    doc = nlp(sentence)
    tokens = []

    ## 参照しやすいようにトークンのリストを作る
    for sent in doc.sents:
        for token in sent:
            tokens.append(token)

    ## 主語述語ペアのリスト
    subject_list = []

    for token in tokens:
        ## 依存関係ラベルがnsubj or iobjであれば「<見出し語>:<係り先の見出し語>」をリストに追加する。
        if token.dep_ in  ["nsubj", "iobj"]:
            subject_list.append(f"{token.lemma_}:{tokens[token.head.i].lemma_}")
    
    return subject_list

試しに「昨日から胃がキリキリと痛い。ただし、熱はない」という文章を引数にすると以下のようになります。

nlp = spacy.load('ja_ginza')
print(parse_document("昨日から胃がキリキリと痛い。ただ、熱は無い。", nlp))

出力

['胃:痛い', '熱:無い']

主語述語のペアが取れていますね。

Elasticsearchでの準備

次はElasticsearchへの投入と検索の部分を作っていきます。
バージョンとプラグインは以下のようになっています。
バージョン:7.4
プラグイン:analysis-sudachi

残り必要なことは以下の3つです。
①Mappingを決めて、データを投入する
②クエリを決める
③入力された検索文章からクエリに変換する

Mapping

Mappingについては、元文章は単語検索の対象になるためtext型で、主語述語のペアは完全一致のためkeyword型の配列で持つようにします。
また、形態素解析を使った単語検索のためにはtext型のフィールドにはanalyzerを設定しておきます。

クエリ

次に検索クエリを作成します。
例えば「胃が痛い」という入力のときには、以下のようなクエリになります。

{
  "query": {
    "bool": {
      "must": [
        {
          "query_string": {
            "default_field": "content",
            "query": "胃が痛い",
            "analyzer": "sudachi_analyzer"
          }
        },
        {
          "terms": {
            "subjects": [
              "胃:痛い"
            ]
          }
        }
      ]
    }
  }
}

クエリとしては、単語検索と主語述語の組み合わせをAND条件で取得するようにしています。

試してみる

それではサンプルデータを入れて検索してみましょう。

今回はサンプルのデータとして以下の2件をElasticseachに登録しておきます。(サンプルなので若干意図的な文章にしていますが)
「ずっと胃がキリキリと痛い。ただ、熱は無く平熱のままだ。」
「昨日からとても頭が痛い。おまけに胃がむかむかする。」

例として「胃が痛い」という文章で単純な単語検索と今回作成した検索を比べてみましょう。

単語検索の場合
クエリはquery_stringクエリを使用して、contentフィールドをします。

GET content/_search
{
  "query": {
    "query_string": {
      "default_field": "content",
      "query": "胃が痛い",
      "analyzer": "sudachi_analyzer"
    }
  }
}

結果としては、2件ともヒットします。
ただ、2文目のほうは検索意図からは外れているので、結果としてほしいものではありません。

次に今回作成した検索の結果を見てみましょう。
結果

{
    "hits": [
        {
            "_id": "DDzRwW4BGThQze84g2Hm",
            "_index": "content",
            "_score": 1.3560746,
            "_source": {
                "content": "ずっと胃がキリキリと痛い。ただ、熱は無く平熱のままだ。",
                "subjects": [
                    "胃:痛い",
                    "熱:侭"
                ]
            },
            "_type": "_doc"
        }
    ],
    "max_score": 1.3560746,
    "total": {
        "relation": "eq",
        "value": 1
    }
}

今回は一つ目の文書、つまり結果としてほしい結果のみがヒットしています。
このように、GiNZAの依存構造解析と検索エンジンと組み合わせることで、より意味的に欲しいものを検索することができます。

まとめ

GiNZAとElasticsearchを使って、簡易的にですが、係り受け検索を実現してみました。
GiNZAを使うことで、少ないコードで簡単に実現することができるので本当に便利だと思います。
今回は主語述語の関係のみに注目しましたが、他にも形容詞や否定語など考慮することは多くあるので次回への課題としようと思います。
皆さんもぜひGiNZAで日本語処理ライフを。

明日はg-kさんの記事になります。
それではまた。


Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com

Elastic Stack 7.5 リリース、注目の Kibana Lens が新登場!

こんにちは、@shin0higuchiです😊

本日Elastic Stackの7.5がリリースされました。
今回は個人的に興味をひかれた新機能に絞って紹介します。
どんな機能が追加されたのか、早速見てみましょう。

リリースノートはこちら
https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-7.5.0.htmlwww.elastic.co

Kibana Lens

Kibanaのvisualizationをより感覚的に作成するための機能、Lensがbetaリリースされました。
データをドラッグ&ドロップで追加しながら、グラフの種別切り替えも簡単です。
これは今回の目玉機能と言えると思います。

f:id:acro-engineer:20191203030132p:plain:w600
Kibana Lens画面

画面左のフィールド一覧から、画面右のエリアにドラッグすることで作成するようなUIになっています。

f:id:acro-engineer:20191203030441p:plain:w600
Kibana Lens画面

種別を切り替えた時に、どのような見た目になるのかも一覧でき、Kibanaの操作に慣れていないユーザーでも簡単に利用できそうです。

data frame analysisにclassificationが追加

classificationおよび、その評価を行うevaluation APIが実装されました。
※今の所、Elasticsearch側のAPIは実装されていますが、Kibanaの画面から指定できるのはoutlier_detectionとregressionのみのようです。

簡単に試すために、titanicのデータセットを取り込みます。

f:id:acro-engineer:20191203032038p:plain:w600
csv import
f:id:acro-engineer:20191203032157p:plain:w600
取り込み完了

data frame analyticsのジョブを作成します。
titanic-dataを分析し、結果はtitanic-data-predictionに入ります。
今回のポイントは、analysisの中に"classification"を指定できるようになった点です。

PUT _ml/data_frame/analytics/titanic-data
{
  "source": {
    "index": "titanic-data" 
  },
  "dest": {
    "index": "titanic-data-prediction" 
  },
  "analysis":
    {
      "classification": { 
        "dependent_variable": "Survived",
        "num_top_classes": 2,
        "training_percent":80
      }
    }
}


ジョブをスタートし...

POST _ml/data_frame/analytics/titanic-data/_start

trainingに利用されたデータを省いて結果を取得します。

GET titanic-data-prediction/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "ml.is_training": false
        }
      }
    }
  }
}

Hitsは次のようになります。
ml以下に分析結果が格納されていることがわかります。

{
        "_index" : "titanic-data-prediction",
        "_type" : "_doc",
        "_id" : "07LNx24BGrho6pjYL9EM",
        "_score" : 0.0,
        "_source" : {
          "Survived" : 0,
          "Pclass" : 1,
          "Siblings/Spouses Aboard" : 0,
          "Parents/Children Aboard" : 0,
          "Sex" : "male",
          "ml__id_copy" : "07LNx24BGrho6pjYL9EM",
          "Age" : 64,
          "Name" : "Mr. Arthur Ernest Nicholson",
          "Fare" : 26,
          "ml" : {
            "top_classes" : [
              {
                "class_probability" : 0.7037370235841935,
                "class_name" : "0"
              },
              {
                "class_probability" : 0.2962629764158065,
                "class_name" : "1"
              }
            ],
            "Survived_prediction" : "0",
            "is_training" : false
          }
        }
      }

evaluate APIについても試してみます。

POST _ml/data_frame/_evaluate
{
   "index": "titanic-data-prediction",
   "evaluation": {
      "classification": { 
         "actual_field": "Survived", 
         "predicted_field": "ml.Survived_prediction.keyword", 
         "metrics": {
           "multiclass_confusion_matrix" : {} 
         }
      }
   }
}

評価結果は下記の通り。

{
  "classification" : {
    "multiclass_confusion_matrix" : {
      "confusion_matrix" : [
        {
          "actual_class" : "0",
          "actual_class_doc_count" : 545,
          "predicted_classes" : [
            {
              "predicted_class" : "0",
              "count" : 483
            },
            {
              "predicted_class" : "1",
              "count" : 62
            }
          ],
          "other_predicted_class_doc_count" : 0
        },
        {
          "actual_class" : "1",
          "actual_class_doc_count" : 342,
          "predicted_classes" : [
            {
              "predicted_class" : "0",
              "count" : 71
            },
            {
              "predicted_class" : "1",
              "count" : 271
            }
          ],
          "other_predicted_class_doc_count" : 0
        }
      ],
      "other_actual_class_count" : 0
    }
  }
}

ちなみに、リリースノートでmulti classと書いてありましたが、下記のドキュメントによると、現状は2クラス分類のみがサポートされているようです。
Data frame analytics job resources | Elasticsearch Reference [7.5] | Elastic

今後の更新が楽しみな機能の一つですね。

enrich processor

ingest pipelineにenrich processorが追加されました。
このprocessorは、他のindexにあるdataを使って、情報を付加することが可能です。
logstashでのtranslate filterに近いものですね。

他のprocessorに比べると使い方が複雑になっています。
事前にenrich policyというenrich設定を登録しておき、processor側でそれを呼び出す形になります。

次のenrich policyは公式ドキュメントの例ですが、emailフィールドでマッチしたドキュメントの、["first_name", "last_name", "city", "zip", "state"] フィールドを取得する設定になります。

PUT /_enrich/policy/users-policy
{
    "match": {
        "indices": "users",
        "match_field": "email",
        "enrich_fields": ["first_name", "last_name", "city", "zip", "state"]
    }
}


processorは下記のように指定します。

"processors" : [
    {
      "enrich" : {
        "policy_name": "users-policy",
        "field" : "email",
        "target_field": "user",
        "max_matches": "1"
      }
    }
  ]

個人的に割と欲しかった機能なので、非常に嬉しいです😊

詳細については、
Enrich your data | Elasticsearch Reference [7.5] | Elastic
をご参照ください。

SLM(Snapshot Life cycle Management)でsnapshotの管理が可能に

Kibanaの画面から、snapshotの自動取得および削除を設定できるようになりました。
Managementのタブから設定することができます。

f:id:acro-engineer:20191203113938j:plain:w600
SLM

まとめ

今回も大きな機能追加がいくつもありました。
ここで扱わなかったものも数多くあります。詳しくはリリースノートをご覧ください。
お読みいただきありがとうございました。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com