Taste of Tech Topics

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

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

皆さんこんにちは。@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

SORACOM S+ Cameraであんパンの品切れをチェックしてみた

機械学習エンジニアのhayakawaです。
弊社には社内で菓子パンを扱うミニ販売コーナーがございます。
私は好物のあんパンをよく買うのですが、他の社員にも人気があるのですぐ売り切れます。早い。
あんパンを多めに仕入れてもらいたいのですが、
欠品よりも売れ残りを嫌うパン購入担当社員を説得するには、
パンにかける熱意よりも、パンがいつ売り切れたかの統計が必要です。たぶん。

そこで
SORACOM S+ Camera(サープラスカメラ) (以下、S+ Camera)
という製品を使って簡単な在庫管理システムを組んでみました。
※弊社はS+ CameraのAIパートナーです。

f:id:acro-engineer:20191119110256g:plain f:id:acro-engineer:20191119140643j:plain

S+ Cameraとは

S+ Cameraはソラコム社のエッジ処理カメラです。

soracom.jp

この筐体に

が入っています。
これにプログラムをデプロイすると、
電源を入れると撮影した画像を処理してSORACOM プラットフォームに送ったり、
何か別の通信(アラートとか)をすることができます。
カメラ向けにプログラムをデプロイするのもプラットフォーム経由でできます。

構成

構成、というほどの内容もないですが、こんな感じです。

f:id:acro-engineer:20191121181552j:plain

一番上の
Torrentio Videoは、弊社の画像・映像解析AIソリューションです。
www.site.torrentio.tech
特定の物体検出などを、学習モデルの作成から推論まで、簡単に実現できるようになっています。

カメラ内にはRaspberry Piが入っており、
そこにはインストールされる"SORACOM Mosaic(プライベートベータ)"というエッジプロセッシングサービス上で
アプリケーションを動かすことになります。

流れとしては次のようになります。

  1. S+ Cameraがカメラで撮影
  2. Torrentio Videoカメラ画像を取得
  3. Torrentio Videoが画像内からパンの欠品を検知
  4. 欠品していたらSORACOMのデータ通信越しに通知

プログラム

まずデータサイエンスの基本作業、アノテーションのお時間です。
今回やりたいのは物体検知のタスクなので、画像内からパンの位置を長方形で指定する作業をひたすらにやります。
f:id:acro-engineer:20191122145917j:plain

だんだんおなかがすいてきました。夕食前にやる作業ではない。

それが終わったらRaspberry Piでも動作可能な軽量モデルで転移学習。
プログラムから呼べるようにします。

また弊社は社内チャットにMS Teamsを採用していますので、
このプログラムの通知もMS Teamsに投げるようにしました。

f:id:acro-engineer:20191121194859j:plain

最初、投稿を5分間隔にしたのですが、
「あんパンが欠品しています」「あんパンが欠品しています」「あんパン…」「あ…」
と1時間に10回以上のペースで延々とつぶやき続けることになってしまいました。呪いか。
作りたいのは在庫管理システムでありスパムBotではないので、
在庫に変化があったときだけ投稿するように改修しました。

物理

f:id:acro-engineer:20191119140351j:plain

電源ケーブルをつなげばとりあえず動きます。
現在はトライアルパッケージ提供のため、
SORACOM ユーザーコンソールの方でSIMカードを登録する必要がありますが、
それさえ済んでいればデータ通信の準備が整います。

筐体が横倒しになっているのは、パンの棚が縦長なのに対して、カメラの撮影画像が横長だったからです。雑ですね。
なお取り付け台は、会社の倉庫にあったUSBディスプレイを載せる台にブックエンドをワイヤーでしばりつけた物です。雑ですね。

結果

で、冒頭のGIFのようになりました。

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


下のキャプションは、実際はMS Teamsに投稿されている内容です。
毎回、売り切れたものすべてがメッセージに載る仕様です。

なお、判定処理は正面からパンの有無だけを見るようにしました。
本来は在庫管理ですので残り個数をカウントしたかったのですが、
現状の社内のパンのキャビネですとパンと上の棚の隙間が狭すぎて上からの撮影できず、
今回はカウントはあきらめました。

実際のMS Teamsへの投稿はこんな感じです。

f:id:acro-engineer:20191119131404j:plain

MS TeamsはWebhookをかんたんに構成できるので、楽に接続できました。
あとはこのチャネルをパン購入担当にフォローしてもらえば、売り切れ状況をすぐ把握してもらえます。
良い時代になりましたね。

感想

エッジ機器+カメラ+データ通信が1つの筐体に収まっているので、
電源さえつながれば場所を選ばないので便利です。

また、SORACOMプラットフォーム経由でS+ Cameraにデプロイする仕組みがあるので、
TorrentioVideoで学習したモデルを、遠くの機器ですぐ試せるのは良いと思いました。

今回は弊社のTorrentioVideoをインストールしましたが、
何をさせるかは自由に入れ替えられるので、
「会議室に予約時間を越えて居座っている社員を警告音で追い出すシステム」
「御手洗いの清掃中の札が消えたら教えてくれるシステム」
など、ニッチすぎて市販されていない用途のものでも気軽に自作できそうです。

色々と使ってみましょう。

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

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

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

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

JJUG CCC 2019 Fallに登壇しました

こんにちは、しんどーです。
最近、トロとパズルにハマっています。この前覚えさせた言葉は「rm -rf /」です。

さて、先日11/23に行われたJJUG CCC 2019 Fallに登壇してきました。

JJUG CCCは日本Javaユーザグループ主催のカンファレンスで、参加者は登録数で1200名を超えたそうです。 自分は参加は9回目で、そのうち登壇は3回目になります。

資料

https://speakerdeck.com/rshindo/jjug-ccc-2019-fall

内容について

今回Java初心者を対象とした「ステップアップセッション」枠でソースコードリーディングについて話しました。

自分も1、2年目のころはOSSのコードを読むのにハードルを感じていて、同じ感覚を持っている人が結構いるんじゃないかなと思ったのがこのネタにした理由です。また、自分の中でコードを読むときに重要視していることを言語化したかったというのもあります。

裏話

ヒイヒイ言いながら資料頑張って作ったところ作り過ぎてしまい、発表では時間が足りなくなり一部カットになってしまいました。聞きに来ていただいた皆さん、すみませんでした。事前練習の繰り返しホント大事ですね!

今回取り上げた2つのライブラリ「Commons DBUtils」と「Javalin」を選んだ理由ですが、

  • コードが大きくない
  • 抽象化が控えめ = 読むのが難しくない
  • DBアクセス、Webといった多くのひとになじみのある領域

といった大体この3点です。

実際、これらのライブラリは機能としてはプリミティブですが、逆を言えばMyBatisやSpring MVCといった高機能なライブラリがコアでやっていることを抑えているとも言えます。大きなライブラリを読むための肩慣らしとしては最適かなーと思っています。ただDBUtilsを知ってる人が皆無だったのは誤算でしたw

f:id:acro-engineer:20191125101347j:plain
満員御礼!

まとめ

次回予告

12/18(水)に行われるSpring Fest 2019で登壇します!

springfest2019.springframework.jp

Spring Boot ActuatorとMicrometerでの運用監視についてお話しします。ぜひ来てね!

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

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

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
Java/PythonでモダンなWeb開発をしたいエンジニアWanted! - Wantedlywww.wantedly.com