mattintosh note

Hello Raspberry Pi!

Elasticsearch で kuromoji を使って Kibana でタグクラウドを作る

Slack のメッセージを解析するために Elasticsearch に Kuromoji を入れて Kibana でタグクラウドでも作ろうとしたんだけど、まぁいろんなサイト見てもわかりづらいので自分なりにまとめる。

Elasticsearch のバージョンは下記の通り。

Version: 6.5.2, Build: default/tar/9434bed/2018-11-29T23:58:20.891072Z, JVM: 1.8.0_181

Kuromoji のインストール

elasticsearch-plugin コマンドを使って analysis-kuromoji をインストールする。elasticsearch-plugin コマンドは elasticsearch コマンドと同じディレクトリに入っている。

$ ./elasticsearch-plugin install analysis-kuromoji

新しいプラグインを読み込むため Elasticsearch を再起動する。Elasticsearch が起動したらプラグインリストを表示して analysis-kuromoji が有効になっているか確認する。(起動ログにも出力されるのでそちらで確認することもできる)

$ curl localhost:9200/_nodes/plugins?pretty
      :
      "plugins" : [
        {
          "name" : "analysis-kuromoji",
          "version" : "6.5.2",
          "elasticsearch_version" : "6.5.2",
          "java_version" : "1.8",
          "description" : "The Japanese (kuromoji) Analysis plugin integrates Lucene kuromoji analysis module into elasticsearch.",
          "classname" : "org.elasticsearch.plugin.analysis.kuromoji.AnalysisKuromojiPlugin",
          "extended_plugins" : [ ],
          "has_native_controller" : false
        }
      ],
      :

マッピングの設定

スキーマレスでデータを投入しても Kibana では項目として使うことが出来ないので事前にマッピングの設定をしておく必要がある。特に Kuromoji のカスタマイズなどをしない場合の書式は下記のようになる。

{
  "mappings": {
    "<ドキュメントタイプ>": { ❶
    "properties": {
      "<親フィールド名>": { ❷
        "type": "text",
        "analyzer": "kuromoji", ❸
        "fielddata": true, ❹
        "fields": {
          "<子フィールド名>": { ❺
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}
  • ❶ … タイプの指定。最近では _doc 一択。
  • ❷ … トップレベルのフィールド名。
  • ❸ … アナライザの指定。ここで kuromoji を指定する。
  • ❹ … ここが true になっていないと Kibana の Aggregation が有効にならず、Terms で表示されない。
  • ❺ … 元のテキストをそのまま保存しておくためのフィールド。使わなければ作らなくてもいい。

インデックス名を my_index、ドキュメントタイプを _doc、トップレベルフィールド名を message とした場合の JSON は下記のようになる。

{
  "mappings": {
    "_doc": {
      "properties": {
        "message": {
          "type": "text",
          "analyzer": "kuromoji",
          "fielddata": true,
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
  }
}

データ投入とタグクラウドの作成

せっかくはてなブログを使っているのではてなさんの代表メッセージをお借りして、先程作成したインデックスにデータを投入する。

{
  "message": """
はてなは「知る」「つながる」「表現する」というミッションを掲げ、Webサービスを提供している会社です。

はてなを代表するサービスである「はてなブログ」や「はてなブックマーク」のように、インターネットサービスを通じ、色々な知識を得て、様々な人とコミュニケーションをとり、自分の考えや体験を表現する。そんなサービスをこれからも作っていきます。

自分の持っている技術にこだわりを持ち、研鑽を怠らない人。失敗を恐れずチャレンジ精神をもって新しいことに取り組める人。素敵な表情で働ける人。はてなのサービスやカルチャーに興味を持ち、もっと良くしていける人。インターネットが好きな人。そんな人をはてなは求めています。

世の中をもっと便利におもしろく。一緒に世界を変えていきませんか?
"""
}

Kibana で my_index なインデックスパターンを作成する。messagemessage.keyword ではない)の Aggregatable が有効になっていることを確認する。

f:id:mattintosh4:20190205212911p:plain
Kibana - Management

Discover で投入されたデータを確認してみる。「はてな」で文字列検索が有効になっていることも確認。

f:id:mattintosh4:20190205212457p:plain
Kibana - Discover

Visualize で Tag Cloud を選択して Buckets を message に指定して実行するとタグクラウドが形成される。

f:id:mattintosh4:20190205213324p:plain
Kibana - Visualize (Tag Cloud)

整った文章の場合はあまり変な区切られ方はしないが、Slack のチャットのようにメチャクチャなワードが含まれていると変に分割されることがある。その場合、Buckets の Advanced オプションで Exclude の欄に .(任意の1文字)や ..(任意の2文字)を指定して除外して上げればよい。条件は | で区切ることで複数指定することが出来、ある程度の正規表現を使うことができるようだ。

条件 書式
1文字だけ .
1文字または2文字 .|..
http または https https?
英数字1文字〜4文字 [a-zA-Z0-9]{1,4}

とりあえずうちの Slack では「半角英数字1文字」、「http または https」、「www」、「com」などを [a-zA-Z0-9]|https?|www|com という条件で除外している。

アナライザの設定で事前にフィルタを設定しておけば Slack 内部で変換されたメンション部分や URL を形態素解析の対象から外すこともできるがその話はまた今度。

サンプルスクリプト

今回のテストで使用した下記の JSON を Kibana の Dev Tools に貼り付けていただくとご自分の環境で同じ検証が出来ます。各コマンドは Dev Tools に表示される「▶」ボタンか、実行したい行をクリックして Ctrl + Enter で個別に実行出来るので順番に進めていくと Kuromoji 導入の勉強になるかもしれません。

※インデックス名はご自分の環境に合わせて変更してください。そのまま実行すると DELETE my_indexmy_index が削除されますのでご注意ください。

※データを投入していまうとマッピングの変更ができなくなりますのでインデックスの作成からやり直す必要があります。

f:id:mattintosh4:20190205215317p:plain
Kibana - Dev Tools

DELETE my_index

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "message": {
          "type": "text",
          "analyzer": "kuromoji",
          "fielddata": true,
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
  }
}

PUT my_index/_doc/1
{
  "message": """
はてなは「知る」「つながる」「表現する」というミッションを掲げ、Webサービスを提供している会社です。

はてなを代表するサービスである「はてなブログ」や「はてなブックマーク」のように、インターネットサービスを通じ、色々な知識を得て、様々な人とコミュニケーションをとり、自分の考えや体験を表現する。そんなサービスをこれからも作っていきます。

自分の持っている技術にこだわりを持ち、研鑽を怠らない人。失敗を恐れずチャレンジ精神をもって新しいことに取り組める人。素敵な表情で働ける人。はてなのサービスやカルチャーに興味を持ち、もっと良くしていける人。インターネットが好きな人。そんな人をはてなは求めています。

世の中をもっと便利におもしろく。一緒に世界を変えていきませんか?
"""
}

GET my_index/_doc/1

Vue.js で2つの配列からデータを取得する

Vue.js の勉強してるけどなんかなぁ…って感じ。どうも文法というか書式というかに馴染めない感じ。自分の頭がオブジェクト指向じゃないからなんだろうけど。MVVM の解説読んでみたけど初歩的なことやってないから View とか Model とかよくわからない。

さて、Elasticsearch に貯めたデータをほぼそのまま Vue.js に持ってきてるんだけども、設計のミスもあったせいでちょっと困った。

大抵の書籍の場合、一つの書籍に対して一人の著者が結び付く 1:1 なんだけど、書籍やストアによっては一つの書籍に対して複数の著者が結びつく 1:n になっていることがある。

xpath で取り出しているせいもあって、初期段階では全ての項目が配列になっているところを、配列の長さが 1 のものは文字列に変換する処理をかけていた。

1:1 のデータ

{
  "hits": {
    "_source": {
      "book_name": "五等分の花嫁",
      "author_name": "春場ねぎ",
      "author_url": "https://booklive.jp/focus/author/a_id/123817"
    }
  }
}

1:n のデータ

{
  "hits": {
    "_source": {
      "book_name": "転生したらスライムだった件",
      "author_name": [
        "川上泰樹",
        "伏瀬",
        "みっつばー"
      ],
      "author_url": [
        "https://booklive.jp/focus/author/a_id/121592",
        "https://booklive.jp/focus/author/a_id/110906",
        "https://booklive.jp/focus/author/a_id/110907"
      ]
    }
  }
}

Python であれば zip(author_name, author_url) で2つの配列を紐付けることができるんだけど Vue.js というか JavaScript でその方法がわからない。

じゃあインデックスは一致してるんだから片方のインデックス使って参照すればいいんじゃないの、と思ったけど、1:1 の方は文字列で入っているので v-for に入れてしまうと1文字ずつに分割されてしまう…Oh。

というわけで v-if="Array.isArray()" で条件分岐すればいいんじゃないかと思ったけどネット上にうまいことやってるお手本が無かった。個人的にはこういう情報がネットにあると嬉しいんだけどなぁと思う。言語によって説明が上手い人もいたりいなかったり。言語の好みはこういうところから分かれていくのだろうか。

v-for はインデックスも取れるようなので author_name のインデックスを index 変数に入れて、そのインデックスを使って author_url の値を拾ってくる。

<!-- author_name が配列の場合 -->
<ul v-if="Array.isArray(hits._source.author_name)">
    <li v-for="author_name, index in hits._source.author_name">
        <a v-bind:href="hits._source.author_url[index]">{{ author_name }}</a>
    </li>
</ul>
<!-- author_name が文字列の場合 -->
<ul v-else>
    <li>
        <a v-bind:href="hits._source.author_url">{{ author_name }}</a>
    </li>
</ul>

Vue.js の公式サイトも翻訳されててまぁいいんだけどどうも説明の仕方が微妙。MDN はわかりやすいんだけどなぁ…。最近は文章読んでも理解できないし、いまから JavaScript 思い出すのもしんどい…(;´Д`)

久しぶりに Web サイト作ってみたものの、内容が至極個人的なものなので見た目とか自分がわかればどうでもいいじゃんよって思うようになりこれ以上モチベーションが上がらない。

ebookman.ga

対象ストアも増えてきちゃったし。ていうかお前 Amazon しか使ってないじゃん?しかもランキングなら Kibana とか Grafana で推移見ながらの方が楽しいじゃない?とかなんとか。

昨日、ニュースに出ていた「アル」というサービスはとても良いものだと思う。自分にはこういうアイデア思いついたりとか実際に作ろうという行動力が無いのですごいなと思う。

alu.jp

www.itmedia.co.jp

さぁこの下がり続けるモチベーションをどうしようか…。

Elasticsearch と Vue.js で電子書籍ランキングを作ってみた

Raspberry Pi で作った Elasticsearch サーバにデータをポイポイと突っ込むこと数日。ある程度データも集まり、ストア間の項目も整理できてきたのでサムネイル一覧的なものが欲しいなぁと思い、http://ebook.stellarcat.net/ で作ってみた。(ドメインの契約が2月までなのでそのうち消えます) 2019-02-12 に新しいドメインを取得しました。

ebookman.ga

f:id:mattintosh4:20190119225515p:plain

会社の人から「Vue.js 使ってみて」と言われたので Vue.js を使うことに。Vue.js でサーバ要らずなので EC2 は使わずに S3 と CloudFront だけでやることにした。

  • Elasticsearch サーバはオフィスにあるので公開はしない。
  • クローリングは一定間隔で行っており、リアルタイム性は無いので JSON は S3 に置いてしまおう。(API Gateway や Lambda の使用も考えたがランキングを表示する度に Elasticsearch に毎回アクセスする必要は無いのでやめた)
  • Vue.js って何?

f:id:mattintosh4:20190120001042p:plain
AWS

データソースは当初 AmazonApp StoreGoogle Play にしていたが、他の電子書籍サービスを見ているとランキングが単巻ではなくシリーズになっているのだと知った。で、そっちの方に専念してしまって Elasticsearch の設計なんかも変わってしまい、Amazon などは作り直しが追いついていない。

とりあえず以下のサービスに絞った。

下調べとしてサービスごとのランキングの変動タイミングの調査。eBookJapan と BookLive! は1日1回の更新らしい。(可視化には Kibana を使っていたが Grafana が ARM でも動くようだったので Grafana に変更した)

f:id:mattintosh4:20190119230130p:plain
Elasticsearch × Grafana

Elasticsearch のクエリは filtershould を使っていて色々面倒だったが、query_string の使い方がわかったのでかなり楽になった。

Before

{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "store_id": "XXXXXX"
          }
        },
        {
          "term": {
            "category_id": "XXXXXX"
          }
        },
        {
          "range": {
            "@timestamp": {
              "gte": "now-1h"
            }
          }
        }
      ]
    }
  },
  "sort": {
    "rank": {
      "order": "asc"
    }
  },
  "from": 0,
  "size": 100
}

After

{
  "query": {
    "query_string": {
      "query": "store_id:XXXXX AND category_id: XXXXX AND @timestamp:[now-1h TO now]"
    }
  },
  "sort": {
    "rank": {
      "order": "asc"
    }
  },
  "from": 0,
  "size": 100
}

Vue.js は「基礎から学ぶ Vue.js」さんのところを見ながら学習。

cr-vue.mio3io.com

とりあえず Webpack を使うほどでもなかったので HTML と JavaScript をカキカキ…。最近は table も css で簡単にピボット出来たり便利だなぁ。

で、S3 に放り投げてドメインの設定して公開。

いまのところ Elasticsearch からの JSON をそのまま使っているのでこれも整形したいところ。著者名とかフィールドが配列になっているところの処理も考えなきゃいけない。Amazon とかのクローラーも作り直さなきゃ。

このランキングは継続して公開する予定はいまのところ無いです。一通り終わる頃にはドメインの契約が切れてる頃だと思うのでそこで終了する予定です。

そういえば本当はランキングじゃなくて無料配信中の書籍を集めるものを作りたかったんだけどそれはどこ行ったのか…。

電子書籍の情報を Elasticsearch で収集する

Raspberry Pi Elasticsearch の検証用に色々とデータを収集。

最近 Amazon プライムで色々と漫画を見てるのだけど、他のストアではどうなんだろうなと思って電子書籍のランキングなんかを拾ってきてる。

ストアによってランキングから拾える情報が異なるため、ストア間の値の紐付けもしないといけないんだけどまだそこまでは手が回っていない。例えば Google Play にはランキングページに出版社情報が無いため、書く書籍のページも巡回しなきゃいけない、など。また、単に電子書籍で漫画と言ってもカテゴリが細かく分かれているのでその辺りの調査も必要だったりする。

Amazon はランキングの更新間隔が1時間と公表されているので毎日よく変動していて、週刊誌の発売(公開)日は特に変動が激しい。逆に Google Play は1日1回程度とあまり変動は見られない。

Amazon は期間限定無料配信をよくやっているので「お、これちょっと読んでみたかったんだよな」と、割と役に立っている。

最近はどのストアを見ても「転スラ」が上位にランクインしていて人気あるのだなぁと感じる。(自分も全巻揃えた)

f:id:mattintosh4:20190109223612p:plain
Elasticsearch/Kibana - Amazon Books

f:id:mattintosh4:20190109223519p:plain
Elasticsearch/Kibana - App Store Books

f:id:mattintosh4:20190109223746p:plain
Elasticsearch/Kibana - Google Play Books

リモートの IP を拾っている部分があるのは API サーバの仕様調査のため。一般に公開されている情報ではないので経験談だが、App StoreiTunes Search APICDN の振り分けで「キャッシュ有り」と「キャッシュ無し」のどこに繋ぎに行くかわからず、ただ単にクローリングしただけでは正しい結果にならない可能性がある(同じ日本国内の同じタイミングでも A さんが見ているランキング結果と B さんが見ているランキング結果が異なることがあるということ)。キャッシュを回避する問い合わせ方法もあるけどそちらは多分推奨されていない。

とりあえずいまはストアごとにクローラーを作ってデータを拾ってこれるかどうか実験中。それが終わったらインデックスの設計やらなんやら考えて、ゆくゆくは電子書籍の新刊情報や無料配信中の情報なんかを全ストアまとめて見れるようなものを作る予定。

最近ようやく Elasticsearch のことがなんとなくわかるようになってきたのでセミナーにでも行って情報収集してみようかな。

Raspberry Pi で Elasticsearch と Kibana

AWS Elasticsearch Service をお試してで使ってみたけど用途に対してコスパが悪いので、余ってる Raspberry Pi 3 Model B で運用することにした。Elasticsearch と Kibana を1台の Raspberry Pi 3 Model B で稼働させるのは重いので現在は ASUS Tinker Board や PINE64 Rock64 4GB を使ったり、Kibana だけ手元の Ubuntu で実行していたりする。

各種バージョン

Elasticsearch や Kibana は依存関係のバージョンが厳しいので(例えば Node.js のバージョンが 8.15.0 だと動かない、など)今回は下記の通りに合わせる。Java は Elasticsearch、Node.js は Kibana で使用する。

  • Elasticsearch: 6.5.2
  • OpenJDK: 1.8.0
  • Kibana: 6.5.2
  • Node.js: 8.14.0
  • curator: 5.6.0

Elasticsearch のセットアップ

Debian 系で Elasticsearch を使う場合は DEB パッケージを使うか、MACOS/LINUX 用の TAR を解凍して使う方法がある。今回は保存用のストレージ等も分けるので TAR ファイルを使う方法にする。

Java Runtime Environment 8 をインストールする。特に Java で開発する予定もないのでヘッドレス版。

※そのうち直るかもしれないが、openjdk-8-jre-headless のインストール後処理でエラーが発生する。もう一度 openjdk-8-jre-headless をインストールすれば正常に終了する。

sudo apt-get update
sudo apt-get install openjdk-8-jre-headless

Elasticsearch をダウンロードする。

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.2.tar.gz
tar xf elasticsearch-6.5.2.tar.gz

elasticsearch-6.5.2/config/jvm.optionsJVM のメモリ設定を変更する。Raspberry Pi 3 Model B はメモリが 1 GB なので 512 MB くらいにしておく。

#-Xms1g
#-Xmx1g
-Xms512m
-Xmx512m

Elasticsearch の設定を elasticsearch-6.5.2/config/elasticsearch.yml で行う。xpack.ml.enabled: false の設定が無いと起動しない。localhost 以外からのアクセスも許可するので transport.hosttransport.tcp.port を設定する。

xpack.ml.enabled: false
network.host: 0.0.0.0
http.port: 9200
transport.host: localhost
transport.tcp.port: 9300

Elasticsearch を起動する。起動まで数分かかる。

elasticsearch-6.5.2/bin/elasticsearch

localhost:9200 にアクセスして Elasticsearch が起動しているか確認する。Raspberry Pi では avahi-daemon が有効になっているのでクライアントが対応していれば raspberrypi.local:9200 でもアクセスできる

curl localhost:9200
{
  "name" : "Z0Eokyo",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "NV6kGw3iQBiF7voqb6tIcA",
  "version" : {
    "number" : "6.5.2",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "9434bed",
    "build_date" : "2018-11-29T23:58:20.891072Z",
    "build_snapshot" : false,
    "lucene_version" : "7.5.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

Kibana のセットアップ

Kibana には ARM 版が無いので Linux x86_64 版をダウンロードする。

wget https://artifacts.elastic.co/downloads/kibana/kibana-6.5.2-linux-x86_64.tar.gz
tar xf kibana-6.5.2-linux-x86_64.tar.gz

こちらもアクセス制限は設けないので kibana-6.5.2-linux-x86_64/config/kibana.ymlserver.host: 0.0.0.0 を設定する。

server.host: 0.0.0.0

Raspberry Pi などの ARM デバイスで Kibana を使う場合、同梱されている Node.js が x86_64 用だったりするので Node.js を別途用意する必要がある。ネットで一通り見た感じ「node/bin/node をバックアップして…」とか書かれているが、あまり好きな方法ではないかな。

kibana-6.5.2-linux-x86_64/bin/kibana を見てみると、自動で node ファイルを探すようになっているが、実行できなければ which で他の場所にある node ファイルを探しに行くようになっている。

#!/bin/sh
SCRIPT=$0

# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
while [ -h "$SCRIPT" ] ; do
  ls=$(ls -ld "$SCRIPT")
  # Drop everything prior to ->
  link=$(expr "$ls" : '.*-> \(.*\)$')
  if expr "$link" : '/.*' > /dev/null; then
    SCRIPT="$link"
  else
    SCRIPT=$(dirname "$SCRIPT")/"$link"
  fi
done

DIR="$(dirname "${SCRIPT}")/.."
NODE="${DIR}/node/bin/node"
test -x "$NODE" || NODE=$(which node)
if [ ! -x "$NODE" ]; then
  echo "unable to find usable node.js executable."
  exit 1
fi

NODE_ENV=production exec "${NODE}" $NODE_OPTIONS --no-warnings "${DIR}/src/cli" ${@}

で、同梱されている node 実行ファイルは ARM 上では実行できない。

$ node/bin/node
-bash: node/bin/node: cannot execute binary file: Exec format error

同梱されている node は使わないので実行権限を削除する。

chmod -x kibana-6.5.2-x86_64/node/bin/node

https://nodejs.org/dist/v8.14.0/ からデバイスに合ったバイナリをダウンロードする。

wget https://nodejs.org/dist/v8.14.0/node-v8.14.0-linux-armv7l.tar.xz
tar xf node-v8.14.0-linux-armv7l.tar.xz

Node.js のインストールは下記のようなインストールスクリプトを使う方法もあるが、Kibana が Node.js のバージョンをチェックしているため、APT でアップデートが発生すると動かなくなってしまう可能性があり、Kibana との組み合わせではこの方法は向いていないと思われる。

curl -sL https://deb.nodesource.com/setup_8.x | bash -
apt-get install -y nodejs

node にパスを通して Kibana を起動する。

PATH=$HOME/node-v8.14.0-linux-armv7l/bin:$PATH kibana-6.5.2-x86_64/bin/kibana

Python でプログラムを書く

Python 3 用の PIP をインストールする。

sudo apt-get install python3-pip

今回はユーザ環境でしか使わないので ~/.config/pip/pip.conf を下記のように設定しておく。「GPIO にアクセスするのに root 権限必要なんじゃ!」っていうデバイスの倍は sudo でどうぞ。

[global]
user = true

PIP をアップグレードする。

pip3 install -U pip

PATH を通しておく。

PATH=$HOME/.local/bin:$PATH

Python 3 用の elasticsearch モジュールをインストールする。

pip3 install elasticsearch

まずは簡単に CPU の温度を投げるだけのプログラムを書いてみる。Pythonタイムゾーンを扱う場合、datetime.now(timezone(timedelta(hours=+9))) と書くのが最初は少々面倒に感じるが、慣れれば定型文のように感じる。

とりあえずデータを投げる場合

#!/usr/bin/env python3

from elasticsearch import Elasticsearch
from datetime import datetime, timedelta, timezone

es = Elasticsearch('localhost:9200')
with open('/sys/class/thermal/thermal_zone0/temp') as f:
    timestamp, cpu_temp = datetime.now(timezone(timedelta(hours=+9))), int(f.read())
body = {
    '@timestamp': timestamp.isoformat(),
    'temperature': cpu_temp,
}
response = es.index(index='foo', doc_type='_doc', body=body)
print(response)
{'_version': 1, '_type': '_doc', 'result': 'created', '_seq_no': 0, '_index': 'foo', '_shards': {'failed': 0, 'total': 2, 'successful': 1}, '_id': 'RFfzK2gBykMjt7Ru8Nx9', '_primary_term': 1}

インデックスのセッティングとマッピングを追加する場合はこんな感じ。実際には毎回データを投げるわけではなく elasticsearch.helpers を使って1秒間隔で取得したデータを60秒間隔でまとめて Elasticsearch に投げるようにしている。

セッティングとマッピングを行う場合

#!/usr/bin/env python3

from elasticsearch import Elasticsearch
from datetime import datetime, timedelta, timezone
import socket

hostname = socket.gethostname()
es = Elasticsearch('localhost:9200')

# CPU の温度を取得
with open('/sys/class/thermal/thermal_zone0/temp') as f:
    timestamp, cpu_temp = datetime.now(timezone(timedelta(hours=+9))), int(f.read())

# インデックス名の生成(logstash-%Y.%m.%d と同書式)
es_index = '-'.join([hostname, timestamp.strftime('%Y.%m.%d')])

# インデックス作成
if not es.indices.exists(index=es_index):
    es_setting = {
        'settings': {
            'number_of_shards'  : 1,
            'number_of_replicas': 0,
        }
    }
    es_mapping = {
        '_doc': {
            '_all': {
                'enabled': False,
            }
        }
    }
    es.indices.create(index=es_index, body=es_setting)
    es.indices.put_mapping(index=es_index, doc_type='_doc', body=es_mapping)

# ポストデータ作成
es_body = {
    '@timestamp': timestamp.isoformat(),
    'hostname'  : hostname,
    'name'      : 'temperature',
    'value'     : cpu_temp,
}

# ポスト
response = es.index(index=es_index, doc_type='_doc', body=es_body)

print(response)

elasticsearch.helpers で Bulk API

適当に書いたので真似しない方がいいかもしれない。

#!/usr/bin/env python3

from datetime import datetime, timedelta, timezone
from elasticsearch import Elasticsearch, helpers
from time import sleep
import socket

hostname = socket.gethostname()
es = Elasticsearch()

def getCpuTemp():
    with open('/sys/class/thermal/thermal_zone0/temp') as f:
        timestamp, temp = datetime.now(timezone(timedelta(hours=+9))), int(f.read())
    return timestamp, temp

data = []
while True:
    timestamp, cpu_temp = getCpuTemp()
    es_index = '-'.join([hostname, timestamp.strftime('%Y.%m.%d')])
    if not es.indices.exists(index=es_index):
        es.indices.create(index=es_index, body={'settings':{'number_of_shards': 1, 'number_of_replicas': 0}})
        es.indices.put_mapping(index=es_index, doc_type='_doc', body={'_doc':{'_all':{'enabled': False}}})
    source = {
        '@timestamp' : timestamp.isoformat(),
        'hostname'   : hostname,
        'temperature': cpu_temp,
    }
    data.append({
        '_index' : es_index,
        '_type'  : '_doc',
        '_source': source,
    })
    if len(data) >= 60:
        try:
            helpers.bulk(es, data)
            data = []
        except:
            pass
    sleep(1)

X-Pack Monitoring を無効にする

f:id:mattintosh4:20190108163654p:plain
Elasticsearch - X-Pack Monitoring

Monitoring を使うと細かくヘルスチェックデータを保存してくれるけど Raspberry Pi 3 Model B ではそれ自体が重い(CPU クロックが常に上がりっぱなし)。しかもインデックスのサイズが毎日 900 MB くらいになる。

クラスタを組んでいるわけでもないし、個人で使う分には必要ないので X-Pack Monitoring を無効にする。

elasticsearch-6.5.2/config/elasticsearch.yml

xpack.monitoring.enabled: false

家の外からのアクセス

家の中であればプライベート IP アドレスで問題ないんだけど、外でちょっと人に見せたりとかする場合に。めんどくさくて家には VPN 入れてないので SSH でやっている。SSH のローカルフォワードで Elasticsearch と Kibana を動かしている Raspberry Pi にそれぞれ接続している。

例えば Elasticsearch が入った Raspberry Pi192.168.1.1、Kibana が入った Raspberry Pi192.168.1.2 であれば下記のように ssh_config を設定すればよい。

Host home-bastion
    Hostname XXX.XXX.XXX.XXX
    Port XXXXX
    LocalForward 9200 192.168.1.1:9200
    LocalForward 5601 192.168.1.2:5601

これで手元の Ubuntu からは curllocalhost:9200 にアクセスすれば Elasticsearch に繋がるし、ブラウザで localhost:5601 にアクセスすれば Kibana に繋がる。