Taste of Tech Topics

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

Elasticsearch、Logstash、Kibana、Kuromojiでタグクラウドを作る

突然ですが、我が家は2階にリビングがあるタイプの戸建てでして、天井が勾配していてカッコイイ感がすごいのですが、この季節は暖房の熱がどんどん登ってしまってなかなか部屋が暖まりません。
要するに寒いという話なのですが、皆さんいかがお過ごしでしょうか、@ です。って僕のジョークと家の寒さは関係ないですから💢


さて、このエントリーは Elastic Advent Calendar の18日目です。
qiita.com


元々、マイクロサービスの可視化や、Kafkaを用いたデータ収集の安定化について書くつもりだったのですが、思いつきで作ったタグクラウドが予想外にイイ感じだったので、このエントリーではその経緯を紹介したいと思います。

f:id:acro-engineer:20161218225727p:plain:w400
タグクラウドとは、文中に頻繁に登場するワードを上の絵のように可視化する機能です。
最近リリースされたKibana 5.1.1に新しく追加されました。

この機能を使って、ツイッターの自分のツイートのタグクラウドを作ってみます。

構成

全体の構成は次の通りです。

  • Twitterのつぶやき(twilogからダウンロード)
  • Logstash 5.1.1(ファイル読み込み、解析、Elasticsearchへの登録)
  • Elasticsaerch 5.1.1(データ保存)
  • Kibana 5.1.1(タグクラウドの作成)

まずTwitterのつぶやき収集ですが、これは私がツイート検索などに使っているtwilogからダウンロードしたものを使います。twilogに登録しているユーザは、過去の分まで含めてCSV形式でダウンロードすることができます。

そのファイルをLogstashで読み込んでパースし、Elasticsearchに登録し、Kibanaで可視化するという流れです。
今回は大したボリュームではないので、すべて自分のMacBookで行うことにしました。

twilogからツイートをダウンロードする

twilogのユーザは、twilogにログインすることで過去ログをダウンロードできます。今回はCSV(UTF8)をダウンロードしました。

twilogのCSVデータは、このような形式になっています。

"807060211827490816","161209 121203","パイとキッシュとタルトの違いを教えてください"

最初の項目がID、次がツイートした日時、最後がツイート本文です。


なお複数行のツイートは、改行区切りになるようです。

"806848561765715969","161208 221102","「冬のボーナス」 cero_t 殿 【支給額】 ¥0
https://shindanmaker.com/294574
ぐぬぬ。"

この辺りは、Logstashで複数行の読み込みをできるようにするため、少し工夫する必要がありますね。


またツイート中の "(ダブルクォート)は、""(ダブルクォート2つ)にエスケープされるようです。

"789133408131649536","161021 005720","""第1作では不可能だった「8台持ち寄りの8人対戦」が可能であるばかりか"" の意味が分からない :
これは「Splatoon」新作なのか?! 「ニンテンドースイッチ」初公開映像に映ったタイトルをチェック! - GAME Watch http://game.watch.impress.co.jp/docs/news/1026016.html"

この辺りも、少し注意が必要でしょうね。

Logstashでtwilogデータを読み込む

まずはLogstashを使ってtwilogのデータを読み込みます。流れとしては、input-fileでツイートデータを読み込み、filter-csvCSVをパースして、output-elasticsearchでElasticsearchへの登録を行います。

先に設定ファイルの全体像を貼っておきます。

input {
  file {
    path => "/tmp/cero_t161216.csv"
    sincedb_path => "/tmp/cero_t.sincedb"
    start_position => "beginning"
    codec => multiline {
      pattern => "^\"(?!\")"
      negate => "true"
      what => "previous"
    }
  }
}

filter {
  csv {
    columns => ["id", "date", "tweet"]
  }

  date {
    match => ["date", "yyMMdd HHmmss"]
    timezone => ["Japan"]
    remove_field => ["date"]
  }
}

output {
  elasticsearch {
    index => "tweet-%{+YYYY.MM}"
  }
}

このファイルを twilog_csv.conf という名前にして、Logstashのディレクトリに置いておきます。


input-fileで設定しているsincedb_pathは「ここまで読んだ」を示すファイルです。いつもどこに出力されるか分からなくなるので、必ず指定しています。
start_positionにbeginningを指定することで、ファイルを先頭から読めるようにしておきます。

また、codecのmultilineを指定しているところが今回のポイントです。
twilogのファイル形式のところに書いたように、ツイート中の改行はそのまま改行されています。そのためmultiline-codeを使って、複数行を1つのデータとして扱えるようにする必要があります。
ここではCSVの先頭文字である " から始まる所が新しい行である、という指定をしたいのですが、それだけ指定すると、たまにツイート本文に「改行後のダブルクォート」があると、それを行頭だと誤認識してしまいます。
そのため「" から始まるけど "" から始まってはいない」という正規表現を用いて、CSVの先頭行であることを指定しています。


そこまでできれば、あとはcsv-filterで区切って、dateフィルタで日付をパースして、elasticsearch-outputに出力するだけです。
Elasticsearchに作るindexは、月ごとに作ることにしました。別に日ごとでも年ごとでも構わないのですが、数が増えすぎず、管理もしやすいのは月ごと程度と考えました。

準備ができたので、可視化するよ

ここからは、可視化まで一気にやりましょう。

まず、ElasticsearchとKibanaを起動します。

cd elasticsearch-5.1.1/
bin/elasticsearch
cd kibana-5.1.1-darwin-x86_64/
bin/kibana

これでブラウザから localhost:5601 にアクセスして、Kibanaの画面が出れば成功です。


次に、Logastashを実行してtweetデータをElasticsearchに流し込みます。

cd logstash-5.1.1/
bin/logstash -f twilog_csv.conf 

twilog_csv.confは、先ほど上に書いたLogstashの設定ファイルのことです。
これで数十秒ほど待って、Elasticsearch側のコンソールログ出力が止まったなと終わったら完了です(雑)


ここまで終わったら、Kibanaの画面でManagementから、いま投入したデータのindex patternを作成します。先ほど投入したデータのindex名である「tweet-*」を指定します。

f:id:acro-engineer:20161218231332p:plain:w640

これは、平たく言えば、投入されたデータのそれぞれの型をKibanaに認識させる作業です。
Elasticsearchはスキーマレスなデータストアであるため、RDBMSのテーブルのような厳密な定義をする必要はないのですが、Kibanaで可視化するためにはデータの型が明確になっている必要があるため、このようなindex patternの作成が必要になるのです。


index patternの作成が終われば、次は可視化です。
左メニューのVisualizeから「Tag Cloud」を選択して、いま作った「tweet-*」を指定します。

Tagsを選択して、Fieldに「tweet.keyword」を指定してSizeを「100」にし、右上の時間を選択する所で「Last 1 year」を選んで、再生ボタンみたいな三角をクリックします。


それでタグクラウドができるわけですが・・・

f:id:acro-engineer:20161218231642p:plain:w640

・・・なんじゃこりゃ。ほぼほぼ「わかる」しか言ってないしw

そうなんです、先ほど選んだ「tweet.keyword」というフィールドは、tweetフィールドを単語分割せず、全体を一つのワードとして扱うフィールドなのです。


ここまででひとまずタグクラウドはできましたが、やはり単語分割をしたものをタグクラウドで見たいですよね。
ここからが長い戦いになります。

単語分割したフィールドをKibanaで可視化する

さきほど作った「tweet-*」のindex patternを見てみましょう。
ここで「aggregatable」という列を見ると、「tweet.keyword」にはチェックがついていますが、「tweet」にはチェックがついていないことが分かります。また「analyzed」という列を見ると、それが逆になっています。

f:id:acro-engineer:20161218231950p:plain:w640

「aggregatable」はKibanaの可視化に使えるフィールド、「analyzed」は単語分割されたフィールドを示しています。いま欲しいのは、この両方にチェックがついたフィールドです。
なので「tweet」というフィールドをaggregatableにできるよう設定します。


aggregatableにするには、Elasticsearchのdynamic templateという機能を利用して設定を行います。
Elasticsearchはスキーマレスということは先にも書いた通りですが、特定のフィールドの設定を変えたい時には、dynamic templateを利用して、「このフィールド名なら、こういう設定にする」というような設定を行うのです。

Kibanaの左メニューから「Dev Tools」を開き、Consoleに次のjsonを入力します。

PUT _template/tweet
{
  "template": "tweet-*",
  "mappings": {
    "_default_": {
      "dynamic_templates": [
        {
          "named_analyzers": {
            "match_mapping_type": "string",
            "match": "tweet",
            "mapping": {
              "fielddata": true,
              "fields": {
                "keyword": {
                  "type": "keyword"
                }
              }
            }
          }
        }
      ]
    }
  }
}

この設定の詳細は割愛しますが、肝は「"fielddata": true」の部分です。
string形式のtweetというフィールドのfielddata属性をtrueとすることで、フィールドの情報がメモリに展開され、このフィールドがaggregatableになるのです。


残念なことに、dynamic templateはデータを投入する際に適用される機能なので、既に投入したデータには適用されません。そのため、一度データをすべて削除し、改めてツイートデータを読み込む必要があります。

Logstashを一旦停止させ、Elasticsearchからデータを削除し、sincedbを削除してファイルを最初から読み込み直せるようにして、改めてLogstashを実行してみましょう。

(Logstashをctrl-cで停止させた後)
curl -XDELETE localhost:9200/tweet-*
rm /tmp/cero_t.sincedb 
bin/logstash -f twilog_csv.conf 

これでデータの投入が始まったら、Kibanaで「tweet-*」のindex patternを開き、上にある矢印がグルグルしている更新ボタンを押します。

f:id:acro-engineer:20161218232436p:plain:w640

これで「tweet」フィールドが「aggregatable」になり、Kibanaで可視化できるようになりました。


続いてKibanaのVisualizeから、改めてタグクラウドを作成します。先ほどは「tweet.keyword」しか選択できませんでしたが、今回は「tweet」というフィールドも選択できるようになっているはずです。
「tweet」を選択して、タグクラウドを作成してみましょう。

f:id:acro-engineer:20161218232626p:plain:w640

なんじゃこりゃ(再)


そう、Elasticsearchのデフォルトの単語分割は、英単語の分割はできますが、日本語は1文字ずつバラバラに分割してしまいます。これでは使い物になりませんね。
もう少し賢いアナライザを使って単語分割をしてみましょう。

Kuromojiのインストールと設定

Elasticsearchで単語分割と言えば、そう、kuromojiですね。
kuromojiは次のコマンドで簡単にインストールできます。

(Elasticsearchをctrl-cで停止させた後)
bin/elasticsearch-plugin install analysis-kuromoji

インストールが終わった後、Elasticsearchを起動し直します。

bin/elasticsearch


続いて、さきほどのtweetというフィールドがkuromojiで単語分割されるよう、dynamic templateを修正します。

PUT _template/tweet
{
  "template": "tweet-*", 
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "kuromoji_user_dict": {
            "type": "kuromoji_tokenizer",
            "mode": "normal"
          }
        }
      }
    }
  }, 
  "mappings": {
    "_default_": {
      "dynamic_templates": [
        {
          "named_analyzers": {
            "match_mapping_type": "string",
            "match": "tweet",
            "mapping": {
              "fielddata": true,
              "analyzer": "kuromoji",
              "fields": {
                "keyword": {
                  "type": "keyword"
                }
              }
            }
          }
        }
      ]
    }
  }
}

kuromojiの設定を行い、またtweetのanalyzerとして「kuromoji」を利用できるように設定しました。


またLogstashを一旦停止させ、データを消して、Logstashを実行してみましょう。

(Logstashを停止させた後)
curl -XDELETE localhost:9200/tweet-*
rm /tmp/cero_t.sincedb 
bin/logstash -f twilog_csv.conf 


これでデータを投入させた後、タグクラウドを作ってみると、こうなりました。

f:id:acro-engineer:20161218233340p:plain:w640

だいぶまともになりましたが、httpが入っていたり、よく会話するスクリーンネームの方が入っているなど、余計な言葉が多いですね。
次はこれを取り除きましょう。

Logstashで不要なワードを除外する

単語分割した後にURLやスクリーンネームを除外するのは難しいため、Logstashの時点でこれらを削除します。

Logstashのfilterに、gsubで文字列置換をする処理を追加します。

input {
  file {
    path => "/tmp/cero_t161216.csv"
    sincedb_path => "/tmp/cero_t.sincedb"
    start_position => "beginning"
    codec => multiline {
      pattern => "^\"(?!\")"
      negate => "true"
      what => "previous"
    }
  }
}

filter {
  csv {
    columns => ["id", "date", "tweet"]
  }

  date {
    match => ["date", "yyMMdd HHmmss"]
    timezone => ["Japan"]
    remove_field => ["date"]
  }

  mutate {
    gsub => [
      "tweet", "@\S+", "",
      "tweet", "http:\S+", "",
      "tweet", "https:\S+", ""
    ]
  }
}

output {
  elasticsearch {
    index => "tweet-%{+YYYY.MM}"
  }
}

これで@から始まる文字と、http / httpsから始まる文字を削除します。
この設定を追加した後、またLogstashを一旦停止させ、データを消して、Logstashを実行してみましょう。


データの投入が終わった後にタグクラウドを作り直すと、こんな風になりました。

f:id:acro-engineer:20161218233705p:plain:w640

かなりそれっぽくなってきましたね。

Kibanaの設定で不要な単語を除外する

しかしまだ「てる」とか「僕」とか「p」とか「d」とか、よく分からない単語が多いです。
これらを除外するためには、KibanaのVisualizeの一番下にある「Advanced」を開き、「Exclude Pattern」を設定します。

ここでは4文字以下の単語を除外するため

.|..|...|....

と入力しました。


すると・・・

f:id:acro-engineer:20161218233754p:plain:w640

できました!!
elasticsearchやSpring、babymetalが目立ってる辺り、僕のタグクラウドっぽいですね!

ところで128162って?

これで上手くタグクラウドができたのは良いんですが、その中にある「128162」って何なんでしょうかね?
不思議に思ってtwilogで検索してみたら・・・

💢 のコードかよ!!

いや確かに、しょっちゅう使ってますけどね。
そんなイイ感じのオチもついたところで、このエントリーを締めくくりたいと思います。


Stay elastic, see you!


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


  • ビッグデータHadoop/Spark、NoSQL)、データ分析(Elasticsearch、Python関連)、Web開発(SpringCloud/SpringBoot、AngularJS)といった最新のOSSを利用する開発プロジェクトに関わりたい。
  • マイクロサービスDevOpsなどの技術を使ったり、データ分析機械学習などのスキルを活かしたい。
  • 社会貢献性の高いプロジェクトや、顧客の価値を創造するようなプロジェクトで、提案からリリースまで携わりたい。
  • 書籍・雑誌等の執筆や、対外的な勉強会の開催・参加を通した技術の発信、社内勉強会での技術情報共有により、エンジニアとして成長したい。

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

【Elastic search 関連】
Elasticsearchを仕事で使いこみたいデータ分析エンジニア募集中! - Acroquest Technology株式会社のエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

【データ分析】
データ分析案件の急増に伴い実践的なデータ分析エンジニアWanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com