Taste of Tech Topics

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

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