レビューを受ける前に僕が気をつけていること2017

昨日、スターウォーズの前夜祭でエピソード8見てきました! えーすけです!!!

ラストジェダイ最高だったんでみんなエピソード1から全部見て行きましょう!!!! ネタバレすると、スクリーンで動くポーグがひたすらにかわいい。

f:id:pokotyamu:20171215102558j:plain:w300

この記事は feedforce Advent Calendar 2017 - Adventar の 15日目の記事です!

昨日は、スケちゃんの

GraphQL API をスキーマファーストで開発したいけどスキーマと実装で乖離を起こしたくない - Feedforce Developer Blog

でした! GraphQL 楽しそう!!!


今回の記事は、コードレビューについてです(サムネ画がポーグなのは全く関係ありませんw)。 最近行った勉強会でレビューについて考える機会があったので、普段の業務の中で日々受けて自分なりにこうしてるよというのをいい感じにまとめれたらなと思っています😀

ちなみにその勉強会はこちら

techplay.jp

前提

今の開発チームはバックエンド2人・インフラ1人・フロントエンド2人・デザイナー1人で開発しています。 自分はバックエンドチームとして、先輩と2人で頑張ってます。

今回の話は、基本的にバックエンドの先輩とのやり取りで得たことがほとんどです。

事前準備

そもそも自分がタスクに取り掛かる上で、

  • 自分が何を作るべきかを理解しておく
  • 0 コメを充実させる

この2点は必ず意識しています。

何を作るべきがを理解する

ここで言う理解には、仕様の理解と機能がどんな使われ方をされるか?の理解とがあります。

仕様については基本的には、ミーティングの中でやりとりをしているんですが

ここらへんは再度自分がやる前に漏れがないかを確認しておくと、スムーズに開発が出来ると思います。

また、機能の使われ方については、実際にユーザーにどう使われるものかのイメージを持っておくことで、気付けるテストケースもあるので非常に重要です。 どう使われるか知らずに作るより、自分が作るものは自信をもって説明できるようにしておきたいですよね。

0 コメの充実

Github の PR を作る時にまず書くのが description いわゆる 0コメ部分だと思います。 自分は、まずは空コミットで push して、やることリスト的な感じで書いていきます。

{対応する issue 番号(あれば)}

## レビュー開始条件
- [ ] 関連 PR がマージされて rebase 済み
- [ ] CI 通ってる
   ...

## やったこと
- [ ] Schema の更新
- [ ] User モデルの追加
    ...


## やらないこと

## 実装方針(新規機能とかの時)

まずはレビュー開始条件。 これは主に自分用の TODO みたいな形で使っています。

次にやったこと/やってないこと。 ボリューム感が多くなりそうだったら、やってないことに書いて、 PR として分ける必要がありそうだなぁとかはここで妄想しています。また、やったことがふわっとしか書けなかったら、自分のタスクへの理解が足りないので、この時点で一旦相談しています。

最後に実装方針。0コメ書き始める前に確認しておいたポイント、ミーティングで決まった仕様や、今回のざっくりの実装方針をまとめていきます。細かい実装のことというより、ざっくりと処理の流れを書いて頭の整理をしています。

f:id:pokotyamu:20171215113455p:plain:w600
0コメサンプル

ルフレビュー

0コメ書いて実際に実装が一段落付いて、レビュー条件が全部クリアできたら、レビューに出す前にセルフレビューをしています。 今年度1番頑張ってやっているのがこのセルフレビューです。

ルフレビューで何をするかですが、一通り眺めるのではなく、他の人が書いたコードの気持ちで実際にレビューしていましょう。 セルフレビューでは3つのことを意識しながらやっていきます。

  • コメントで自分の気持ちを先出し
  • テストケースは網羅できてる?
  • 自分の認識を明文化していく

一つずつ説明していきます。

コメントで自分の気持ちを先出し

ここでこんな気持ちで実装したんや!というのも合わせて、コメントに書いていきます。

f:id:pokotyamu:20171215012111p:plain:w600
コメントサンプル

特にクラスや変数の命名はかなりレビューコメントを受けるポイントだと思うので、自分の場合

  • こんな意味を込めて、xxxx にしました
  • 似たような機能の gem が xxxx なので、xxxx にしました
  • 他の候補として oooo も考えましたが、xxxx にしました

今感じのコメントを付けることが多いです。 なんでこれをしておくかというと、「こっちの名前の方がいいんじゃない?」ってコメントに対して、コメントするより、自分がこうしたいからこうみたいな気持ちを先に伝えておいたほうが言いやすさがあると思っているからです😌

もちろん、パイセンの提案の方がよいなぁと思ったら、そっちにしますし、それ以上の場合は口頭で話すようにしています。

ちなみに、こんな感じのコメントは先輩にも書いてもらうように依頼しています。 これは完璧に勉強目的です🙏ありがたや〜😭

テストケースは大丈夫?

次に見るポイントは、テストケースが網羅出来ているかです。

  • Context の切り方大丈夫?
  • 境界値でちゃんとテストできてる?
    • 以上/以下/より大きい/未満/nil/型違い ...etc
  • 正常系、異常系の書き漏れがない?
  • 重複しているテストはない?
    • Model と Controller で同じことをテストしてない?
  • スタブが変な感じになってない?
    • ちゃんとバグがあった時にも気づけそう?

などなど。せっかく自動テストを書いているならば、テストケースも抜けがないように確認していきたいですね。

あとは、関数として共通化できる部分がないか? N+1 とかなってない? とか普段レビューで見るポイントはしっかり見ていきましょう 👀

自分の認識を明文化していく

※ここは筆者がやりたいけど、苦手なので結局どこまで理解できたの?って1番指摘を受けている部分です。

ルフレビューの時にやっておくといいのが、認識の明文化だと思います。 ここで一番やりたいのは、自分がやったことを整理して伝えることです。

新しい gem を導入したり、ベンチマークを取って検証してみた際に、

  • どんな使い方をするもの?
  • テストデータはどんなものを使った?
  • 結果はどうだった?

これらのことをまとめていきます。ポイントはそのコメントを見た人がコメントを追っていけば自分の環境でも再現できることが大事です。

更に発展させるとしたら、自分の認識が複数の状態を混在させてないか?という点も大事です。 特にバグ修正関連の PR を作る時はここ大事だと思います。

  • どんな事象が発生したからバグが起こっているのか?
  • そのバグの再現性は?(テストデータ込み)
  • ライブラリの問題なら、どの部分が悪そう?

こんなコメントをバシッとまとまって書いてあると修正 PR をレビューする側から見ても分かりやすいですね。 また、複数の問題が絡まっていそうな場合、一つ一つを独立した問題として検証が出来ると、相手にも分かりやすく伝わる気がします。

この問題の切り分けが難しいので、毎回先輩にはどんな感じで調査進めたかは聞くようにしていて、真似していきたいポイントです😇

[番外編]レビューする時に気をつけていること

レビュー受けるばかりではなく自分がすることも当然ながらあります。 むしろ、自分が1つ PR 作っている間に 2〜3個 PR が作られていることも稀によくあります。

自分がレビューする時に気をつけているのは

  • 自分だったらこう書くのになんでこう書くのだろう?
  • コードを読んで、自分の理解をまとめから聞く

この2点です。

自分だったらこう書くだろうな

自分が普段業務で使っている言語は Ruby/Python が多いので、人によって上手く書けるかが大きく変わってくる言語だと思っています(個人的に) 設計の仕方だったり、メソッドの使い方だったりで、自分の思った実装と違った場合、なんでこう書いたかを最近意識しています。

この時、実際にコードを書く必要は全くないと思います。 自分が知らなかったメソッドを使ってたら、 irb とか pry で実際に挙動を試してみることもやっています。

コードを読んで、自分の理解をまとめから聞く

これは、認識の明文化と同じ話にはなりますが、ただ単に「わからないんで教えて欲しい」だけだと、先輩も困るので、一旦自分の中で整理して「ここまでは分かったんですけど、ここが分からんので教えて欲しい」的な感じ話し始めると、先輩に質問するのも楽になりました。

自分は話しを整理するのが苦手なので、ひたすら付箋に自分の頭を書き出して整理するように心がけています。 やっぱ頭の中だけで考えるのと、視覚的にも整理が出来るのは大きく違いそうです。

ちなみに、先輩に話し始めて自分で話しているうちに自己解決する場面が結構あります。 人に話していく中で自分の理解が整理されて解決することは多々あるので、言葉の力って凄いなと日々思っています。

人に話して解決するのをテディベア効果とも言うらしく、テディベア持ってないんでスヌーピーに話しかけるしか😏

ベアプログラミング(テディベア効果) - 発声練習

まとめ

今回は、自分がレビューを受ける(する)時に気をつけていることをまとめてみました。

実際問題、自分がこれ全部毎回出来ているかは微妙なのですが、毎回意識してやっていくことで、レビューに対する苦手意識もなくなってきました。

レビューは相手がいてこその部分もあるので、お互いにどんな感じで指摘して欲しいとか言い合える関係や場所があるかも大事かと思います。 (実際、私もこんな感じで書いていただけるとレビューしやすいのでよろしくお願いします的なのを話しました)

レビューはたくさん受けると辛いこともありますが、よりよいプロダクトにしていくためには絶対に必要なことなので、来年は自分からもガンガン提案出来るように勉強頑張るぞ💪💪💪


明日は、今の私が所属しているプロダクトである dfplus.io の PM ガッキーさんが io リリースからの1年を振り返ってという記事の予定です!!!楽しみ!!!

世紀末で、ヒャッハー!アウトリブ!

この記事は、 ボドゲ紹介 Advent Calendar 2017 - Adventar の12日目です! 昨日は madeinxxx さんの リスボア の紹介でした! 1755年のポルトガルが舞台のゲーム!かつらを最も多く集めたら勝ちというのがいいっすねwww

さてさて、そんな僕が紹介するのは 2079年の核戦争後の世界を舞台にしたアウトリブです!!

アウトリブ 完全日本語版

アウトリブ 完全日本語版

プレイヤーは、世界中の優れた生存者を集めた「コンボイ」と呼ばれる集団に認められるために、クランを率いて人類最後の都市に移住するとめのリーダーとなります。


アウトリブはこんなゲーム

核戦争後の世界ともあって、資源は限られています。 その資源は、早い者勝ちで奪い合わなければなりません!

f:id:pokotyamu:20171212010618j:plain

フィールド上には、森、ダム、軍事基地、鉱山、遊園地、都市、タンカーにクランのメンバーを移動させることで、資源を獲得することが出来ます。

集めた資源は、自分のクランの設備を増やすか装備を作成することに使います!

自分のクランには7つの部屋があって、その部屋の空いてるスペースに生存者が埋まると強力なルームの効果を使うことが出来ます! 例えば、装備を作る資源が減ったり、毎ターン資源を発生させたり。

f:id:pokotyamu:20171211194709j:plain

ただ、問題が一つ。 人が増えるということは、やっぱり食料も必要になるんですね。

f:id:pokotyamu:20171211235656j:plain

食料は大きく分けて3つ

  • 動物を狩ることで得られるお肉
  • ダムから取れる水
  • 保存が効く缶詰

缶詰以外の食料に関しては、お肉だったら取りやすい代わりに毎ターン腐るので破棄しなきゃいけなかったり、お水も毎ターン2つしか持ち越すことが出来ません。

ア○リ○ラやス○ーン○イジのように木を食べることはできず、足りない食料の分生存者が死んでしまいます。

ルームを作るのを先にして効果を得るか? それとも装備を先に作って資源の効率を早くするか? そんなジレンマを楽しみながら進めていきます!

ちなみに装備一覧はこんな感じです。

f:id:pokotyamu:20171211194818j:plain

獲得できる資源が増えたり、生存者が増えたりなど効果は様々です。

ここが辛いよアウトリブ

ここまでの説明では、わりと普通のよくある資源や食料の先勝での取り合いでしたが、このゲームはプレイヤー間のやり取りも発生する場面があります。 このセクションでは、アウトリブならではの辛い部分を紹介していきます。

移動について

各クランには英雄コマと呼ばれる資源を調達する部隊が4体与えられています。 それぞれの英雄コマはパワー(3が2体、4、5が1体)を持っていてそのパワー分だけ移動先の資源を獲得することができるのですが、それらの移動は2マスまでの隣接する場所しか行くことが出来ません。 もちろん同じ場所に留まることも出来ません。

さらに、今自分がいるコマがある位置には行くことが出来ません。

この自分のコマがいるところには移動出来ないというのが悩みのタネで、この資源がほしいけどお前邪魔!!!!みたいなイライラを味わうことが出来ます😈

資源が足りない?奪うしかないじゃろ!

まぁ世紀末なんで、自分が足りない資源がなければ他のクランから奪うしかないですよね!!

f:id:pokotyamu:20171212000417j:plain

各クランの英雄コマには 移動が終わった英雄コマはアクティブな状態となります。 他のクランのアクティブな英雄コマがある場所に移動した時奪い合いが発生するんです。

もし、自分より低いパワーのクランの英雄がいる場合、そのパワー差の分だけ相手に資源を渡さなければなりません。

f:id:pokotyamu:20171212010636j:plain

この写真の場合は、黄色のパワー5に対して、オレンジはパワー4なので1つ、紫はパワー3なので2つ分の資源を渡さなければなりません。

これを防ぐためには、パワーの分だけの弾薬を使って追い返すことができます! ただ装備によっては、与えるプレッシャーを増やすものもあります。そうですバットですね!

f:id:pokotyamu:20171211194716j:plain

グレイソンさんほんま強そう。。。

まとめ

他にも毎ターン核の汚染が起こったり、動物が凶暴化したり、ルールを細かく説明すると、この記事では僕の文章力が持たないので、今回はざっくりアウトリブを紹介させていただきました!

ちなみに最終盤面はこれぐらいのカオスさでございます。

f:id:pokotyamu:20171212003542j:plain

まぁこのくらいの終盤になると、

「こいつ死んでも大丈夫なやつ」とか「こいつの分の食料はあいつから奪えばいいから」とか言い始めるので、みんな最高に世紀末してます。


もし、この記事で少しでも興味を持っていただけたら嬉しいです☆ 毎週水曜日に弊社フィードフォースでボドゲ会やっているので、もしお時間あれば一緒に遊びましょ〜 今週はゲムマの戦利品会な予定!

フィードフォース ボドゲ部 (@ff_boardgame) | Twitter


明日のアドベントカレンダーは、see_know さんです!大聖堂やったことないから楽しみ^^

Serverless Conf Tokyo 2017 に行ってきたぞ

先日の11月3日文化の日に Serverless Conf Tokyo 2017 に参加してきました!

tokyo.serverlessconf.io

モチベーション的には、最近業務的なところで、サーバーレスなシステムの実装をしているので、事例だったり AWS Batch の話が気になって参加しました。

LT の中で、『アウトプットしないのは知的な便秘。』 と言われたので、積極的にアウトプットしてこうな。

今回は、その中で特に気になったものとまとめての感想を書きたいと思います。

資料

まとめていた方がいらっしゃるので、引用させていただきます。 素晴らしいまとめ(-人-)

www.n-novice.com

会場の雰囲気とか

外人の方も多かった印象。

ブースにも外人の方が多くて、英語でガッツリ話してたのが印象的でした。

ブースで話をきくとバッチがもらえて、全部のブースを回ると Tシャツがもらえる仕組みは素敵だった。

以下、戦利品

f:id:pokotyamu:20171106012147j:plain

Step functionsとaws batchでオーケストレートするイベントドリブンな機械学習基盤

ピタゴラスイッチ部分の構成図をしっかり見せていただけたのはすごくありがたかった。 S3 にデータが上がったのをトリガーに Lambda を使って、 Step Functions を使って AWS Batch のステートを管理。

一つ一つの状態を変化させるのにも Lambda を使って、Dynamo DB の情報を update する、と言った流れが非常に分かりやすかったです。

ほんとに最小単位で関数を用意するんだなという発見でした。

The mind of Serverless as a Software

slideship.com

サーバーレス全般の話について分かりやすかった。 新しくサーバーレスでなんかするって人にとりあえず読んどけレベルな資料だと思いました。初心者僕が思ったんだから間違いないはず。

データの流れを一方通行にして、次の関数次の関数とどんどん渡していくことが大事。 そうなれば、一つの関数ごとに単体テストがかけるはず。

またサーバーレスに向いているのは、横にスケール(並列性)する分野。 向いていないのは、縦にスケール(処理速度)する分野。

ここらへんは今システム開発してて、納得感がありました。 単体テストを信頼して、ちゃんと通しでも E2E で確認というのが個人的にはしっくりきています。

まとめ

そもそも Serverless が出てきたのは、アプリ開発者がアプリにコミットできるようにってことだろうし、その仕組を知っておく意味で行く価値のあるイベントでした。

インフラ/バックエンド/デザイン/フロントエンド ここがすべてそれぞれの分野にコミットできるようなチームが組めるとホントに強いんだろうなと感じました。もちろん無関心は良くないと思うんですけどね。インフラもやってくぞ!!

また、今回のイベントで、Serveless な構成はビジネス面でスケールさせるためには、絶対に必要になってくる分野な気がしています。 特に開発スピードが求められる環境ならば、なおさらインフラのことを考えずに開発できることはメリットでしかないので、ここらへんも使いこなしたいな。

最近、Rails エンジニアーだけじゃなく、AWS 周りも色々触り始めるテラフォーマーになったので、有識者から学べるところをどんどん吸収してきたいと思いますので、今後共よろしくお願いしますん。

個人的には、 AWS が用意した、 Python の Serverless フレームワークchalice が気になってます!!!

github.com

Serverless フレームワークとどう違うんだろ?触ってみる!!!

Elasticsearch 先輩で価格周りを触りたい

やりたいこと

1000 JPY みたいな文字列をいい感じに数値検索の対象にできないかなという実験。

今回の実験は以下のデータに対して、実験方法の curl を実行し、正しく指定した範囲のデータが返ってくることができるか?ということを調べる。

{"index":{"_index":"sample_index","_type":"sample_type","_id":"1"}}
{"PRICE1":"3000 JPY"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"2"}}
{"PRICE1":"5000 JPY"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"3"}}
{"PRICE1":"100 JPY"}
$ curl -XGET 'localhost:9200/sample_index/_search?pretty' -H 'Content-Type: application/json' -d'
{
   "query" : {
        "range" : {
            "PRICE1": {
                "gte": 10,
                "lte": 3000
             }
        }
    }
}'

# id 1, 2 のみ検索結果にヒットすることを期待している。

自動マッピングに任せる

$ curl -X POST -H 'Content-Type: application/x-ndjson' 'http://localhost:9200/_bulk?pretty&refresh' --data-binary "@price.json"
$ curl -XGET 'localhost:9200/sample_index?pretty'
{
  "sample_index" : {
    "aliases" : { },
    "mappings" : {
      "sample_type" : {
        "properties" : {
          "PRICE1" : {
            "type" : "text",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          }
        }
      }
    },
    "settings" : {
      "index" : {
        "creation_date" : "1500889167417",
        "number_of_shards" : "5",
        "number_of_replicas" : "1",
        "uuid" : "eZdsR51MRkeqfem48th0Rw",
        "version" : {
          "created" : "6000002"
        },
        "provided_name" : "sample_index"
      }
    }
  }
}

text 型で入ってるため、上手く数値の範囲検索を行うことが出来ない。

Numeric Detection

Dynamic field mapping | Elasticsearch Reference [5.5] | Elastic

text 型で入ってきた数値に関して、検索できるようにするぜという設定。 これによって、 以下のような text の中身が数値というデータに関しては、数値検索可能となる。

{"index":{"_index":"sample_index","_type":"sample_type","_id":"1"}}
{"PRICE1":"1000"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"2"}}
{"PRICE1":"10"}
$ curl -XPUT 'localhost:9200/sample_index?pretty' -H 'Content-Type: application/json' -d'
{
  "mappings": {
    "sample_type": {
      "numeric_detection": true
    }
  }
}'
{
  "acknowledged" : true,
  "shards_acknowledged" : true
}
$ curl -X POST -H 'Content-Type: application/x-ndjson' 'http://localhost:9200/_bulk?pretty&refresh' --data-binary "@test-price.json"
$ curl -XGET 'localhost:9200/sample_index/_search?pretty' -H 'Content-Type: application/json' -d'
{
   "query" : {
        "range" : {
            "PRICE1": {
                "gte": 100,
                "lte": 1000
             }
        }
    }
}'
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "sample_index",
        "_type" : "sample_type",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "PRICE1" : "1000"
        }
      }
    ]
  }
}

Tokenize

あとは、 100 JPY となっている部分を 100JPY に分けたら 100 に対する検索にヒットするようになりそうなので、やってみる。

$ curl -XPOST 'localhost:9200/_analyze?pretty' -H 'Content-Type: application/json' -d'
{
  "tokenizer": "whitespace",
  "text": "100 JPY"
}
'

{
  "tokens" : [
    {
      "token" : "100",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "JPY",
      "start_offset" : 4,
      "end_offset" : 7,
      "type" : "word",
      "position" : 1
    }
  ]
}

空白で、トークンを区切る whitespace が使えそうなので、これでトークンを 100JPY に分けることはできそう。

{"index":{"_index":"sample_index","_type":"sample_type","_id":"1"}}
{"analyzer": "whitespace","PRICE1":"3000 JPY"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"2"}}
{"analyzer": "whitespace","PRICE1":"5000 JPY"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"3"}}
{"analyzer": "whitespace","PRICE1":"100 JPY"}

というわけで、実験データに analyzer を足した物を実験データとして再度やり直した。

しかし、挙動が安定しない 意図通りの挙動になってくれない感じバグっぽい気もしている。 またフォーラム行きかしら。。。

ちなみにこんな感じ↓ 100000 以上について指定している。

$ curl -XGET 'localhost:9200/sample_index/_search?pretty' -H 'Content-Type: application/json' -d'
{
   "query" : {
        "range" : {
            "PRICE1": {
                "gte": 100000
             }
        }
    }
}'

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "sample_index",
        "_type" : "sample_type",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "analyzer" : "whitespace",
          "PRICE1" : "5000 JPY"
        }
      },
      {
        "_index" : "sample_index",
        "_type" : "sample_type",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "analyzer" : "whitespace",
          "PRICE1" : "3000 JPY"
        }
      },
      {
        "_index" : "sample_index",
        "_type" : "sample_type",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "analyzer" : "whitespace",
          "PRICE1" : "100 JPY"
        }
      }
    ]
  }
}

そもそもトークンが上手くいっていない感じだな。。。うーん。。。わからん。。。。

$ curl -XGET 'localhost:9200/sample_index/_search?pretty' -H 'Content-Type: application/json' -d'
{
   "query" : {
         "term": { "PRICE1": "JPY"}
        }
    }
}'

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 0,
    "max_score" : null,
    "hits" : [ ]
  }
}

【追記】

johtani さんからアドバイスを頂いた。

アプローチの仕方が間違っていた。 やっぱり、数値にアプリ側で変換してインデックス貼ってあげるのが楽な気がする。

Elasticsearch 先輩で日付を触りたい

やりたかったこと

日付の取り回しについて調べてみた。 具体的には、以下のようなデータを想定する。

  • 20170713
  • 2017/07/13
  • 2017-07-13
  • 2017年07月13日
  • 2017713
  • 2017/7/13
  • 2017-7-13
  • 2017年7月13日

これらが不特定なカラムに入っている時に、 Elasticsearch で、いい感じに受け取るにはどうすればいいか?を調べてみた。

自動的にやってくれるもの

Elasticsearch は自動的に型を判断していい感じにマッピングしてくれる機能がある。 そこで使われる日付のフォーマットはこちらに一覧されている。

format | Elasticsearch Reference [5.5] | Elastic

というわけで、以下の実験データを何もマッピングされていない index に突っ込んでみる

{"index":{"_index":"sample_index","_type":"sample_type","_id":"1"}}
{"DATE1":"2017年07月13日"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"2"}}
{"DATE2":"2017/07/13"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"3"}}
{"DATE3":"2017-07-13"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"4"}}
{"DATE4":"2017.07.13"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"5"}}
{"DATE5":"20170713"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"6"}}
{"DATE6":"2017年7月13日"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"7"}}
{"DATE7":"2017/7/13"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"8"}}
{"DATE8":"2017-7-13"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"9"}}
{"DATE9":"2017.7.13"}
{"index":{"_index":"sample_index","_type":"sample_type","_id":"10"}}
{"DATE10":"2017713"}
$ curl -s -X GET 'http://localhost:9200/sample_index/' | jq .
{
  "sample_index": {
    "aliases": {},
    "mappings": {
      "sample_type": {
        "properties": {
          "DATE10": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "DATE2": {
            "type": "date",
            "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
          },
          "DATE3": {
            "type": "date"
          },
          "DATE4": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "DATE5": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "DATE6": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "DATE7": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "DATE8": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "DATE9": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    },
    "settings": {
      "index": {
        "creation_date": "1499935832320",
        "number_of_shards": "5",
        "number_of_replicas": "1",
        "uuid": "Tf1HqJSfQzmoIPVKrMgnQQ",
        "version": {
          "created": "5040299"
        },
        "provided_name": "sample_index"
      }
    }
  }
}

結果から分かる通り、YYYY/MM/DD の形のみ受け付けている

そして、 format | Elasticsearch Reference [5.5] | Elastic にある通り、07月のように0埋めしないと自動ではマッピングしてもらえない。

なので、 DATE5~10 のフォーマットは、事前に何かをしてやるとかしないと、難しそう。

フォーマットを自分で定義してみる

次に自分で mapping を指定してデータの登録を行ってみる。

{
  "mappings": {
    "sample_type": {
      "properties": {
        "DATE1": {
          "type":   "date",
          "format": "yyyy年MM月dd日"
        },
        "DATE3": {
          "type":   "date",
          "format": "yyyy-MM-dd"
        },
        "DATE4": {
          "type":   "date",
          "format": "yyyy.MM.dd"
        },
        "DATE5": {
          "type":   "date",
          "format": "yyyy年M月dd日"
        }
      }
    }
  }
}

登録は次の用に行う。

curl -X PUT 'http://localhost:9200/sample_index' --data-binary @mapping.json | jq .

これを行った後、 DATE1~5について再度データの挿入をしてみた。

$curl -X GET 'http://localhost:9200/sample_index/' | jq .
{
  "sample_index": {
    "aliases": {},
    "mappings": {
      "sample": {
        "properties": {
          "DATE1": {
            "type": "date",
            "format": "yyyy年MM月dd日"
          },
          "DATE3": {
            "type": "date",
            "format": "yyyy-MM-dd"
          },
          "DATE4": {
            "type": "date",
            "format": "yyyy.MM.dd"
          },
          "DATE5": {
            "type": "date",
            "format": "yyyy年M月dd日"
          }
        }
      },
      "sample_type": {
        "properties": {
          "DATE1": {
            "type": "date",
            "format": "yyyy年MM月dd日"
          },
          "DATE2": {
            "type": "date",
            "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
          },
          "DATE3": {
            "type": "date",
            "format": "yyyy-MM-dd"
          },
          "DATE4": {
            "type": "date",
            "format": "yyyy.MM.dd"
          }
        }
      }
    },
    "settings": {
      "index": {
        "creation_date": "1500369904641",
        "number_of_shards": "5",
        "number_of_replicas": "1",
        "uuid": "EN99Zmc7TuK-7ePO1S9T6A",
        "version": {
          "created": "5040299"
        },
        "provided_name": "sample_index"
      }
    }
  }
}

これで、データ型での登録ができるようになった。 しかし、これでは、予め決められているカラムにしかデータの取込が出来ないため、デフォルトとして登録することを考えるもやり方が分からない。。。

Dynamic template は index の正規表現に則って、決まった index に対してマッピングを当てはめるみたいなことはできるが、不特定のカラム名に対して割り当てるのは難しい。

Dynamic templates | Elasticsearch Reference [5.5] | Elastic

うーん。。。

フォーラムで質問して見るかしら。。。

(追記 7月20日) フォーラムで質問したら、大谷さんから回答が返ってきた。

不特定カラムに対して日付のフォーマットを指定する - Elastic In Your Native Tongue / 日本語による質問・議論はこちら - Discuss the Elastic Stack

6.0.0-alpha2 だと試せるそうなので、試し見た。

$ curl -X PUT -H 'Content-Type: application/json' http://localhost:9200/my_index -d '
{
  "mappings": {
    "my_type": {
      "dynamic_date_formats": ["yyyy年MM月dd日", "yyyy/MM/dd", "yyyy.MM.dd", "yyyy-MM-dd"]
    }
  }
}' | jq .
{
  "acknowledged": true,
  "shards_acknowledged": true
}
$ curl -X POST -H 'Content-Type: application/json' http://localhost:9200/_bulk --data-binary @test.json 
$ curl -X GET http://localhost:9200/my_index | jq .
{
  "my_index": {
    "aliases": {},
    "mappings": {
      "my_type": {
        "dynamic_date_formats": [
          "yyyy年MM月dd日",
          "yyyy/MM/dd",
          "yyyy.MM.dd",
          "yyyy-MM-dd"
        ],
        "properties": {
          "DATE1": {
            "type": "date",
            "format": "yyyy年MM月dd日"
          },
          "DATE2": {
            "type": "date",
            "format": "yyyy/MM/dd"
          },
          "DATE3": {
            "type": "date",
            "format": "yyyy-MM-dd"
          },
          "DATE4": {
            "type": "date",
            "format": "yyyy.MM.dd"
          }
        }
      }
    },
  以下略
  }
}

意図通り、 date 型でのデータの保存ができるようになった。 ポイントは、事前に dynamic_date_formats を指定することで、予め想定される date を確定することだった。 これを template 化しておくことで、任意の index に対して、任意のカラムの日付を割り当てることができそう。

Dynamic field mapping | Elasticsearch Reference [master] | Elastic

日付が扱えるとどんな検索ができるか?

Range Query | Elasticsearch Reference [5.5] | Elastic

  • ○日から○日の範囲内
  • 今から何日以内
  • この日から何日以内

といった検索が行えそう

まとめ

文字列をトークン化するとか、予め決められたデータを処理することに対しては、 Elasticsearch は有効打にできそうだけど、日付周りは、フォーマットの関係に一工夫必要だった。

Elasticsearch 先輩との戯れ日記(登場人物の整理)

この記事何?

お仕事で Elasticsearch 先輩を使うことになったので、そのお戯れの記録

登場人物の整理

事例を中心に調査していたが、色々混乱してきたので、 Elasticsearch の世界の登場人物を整理することにした。

雑な説明

hoge

オブジェクト 概要
Cluster Node を束ねる存在
Node Elasticsearch のインスタンスのこと
Shard Index を分割したもの
Index インデックス
Type テーブル
Document レコード

雑すぎるので1個ずつ見ていく。

Cluster

Node を束ねるもの。 Cluster Health を使って、 Cluster の状態を確認することができる。

  • green: すべての Shard が Cluster 上に配置されてる
  • yellow: PrimaryShard は Cluster 上に配置されているが、配置されていない ReplicaShard が存在している
  • red: Cluster 上に配置されていない Shard が存在している

Shard については、後述。

Node

Node | Elasticsearch Reference [5.4] | Elastic

Node には Master (eligible) Node と Data Node と Ingest node が主要な感じ(Tribe Node はよくわからなかった)

Master Eligible Node

  • クラスタ内で行う処理の分配を行う
  • もし、 Master Node が死んだら、別の Master Eligible Node が Master になってくれる(デフォルトでは全 Node)
  • Master Node は、基本的に分配だけをメインにやらせると安定運用につながる

Data Node

  • document の管理を行う
  • 主に CRUD 操作を担当
  • Master Node とは役割分離させようね
  • I/O 系の処理はメモリいっぱい使うからちゃんと監視しようね

Data Node が死んじゃったらデータは欠損する? discovery.zen.minimum_master_nodes を設定すれば、設定した値の数で最小構成として、データの書き込みとかを行うみたい(おそらく) なので、一応データの欠損は防げるらしい。

参考:Avoiding split brain with minimum_master_nodes

Ingest Node

  • Elasticsearch 5.0 から登場
  • index を貼る前にドキュメントを変換するために使う Node
  • パイプラインを作って複数の処理を組み合わせて実行もできる

Shard

Shards & Replicas

10億個の document を複数に分割して読み込めるように、 index を分割する仕組み。 これによって、オペレーションの分散と並列化ができるため、パフォーマンスの向上につながる。

Primary shard を増やせば増やすほど並列数が上がって1ノードのパフォーマンスが上がるかも? →ただ、その分スペックは要求される デフォルトは5つの Primary Shard と 各 Primary Shard 毎に 1つの Replica Shard

Replica Shard は Primary Shard コピーを複製してあるもの。 このコピーは同一 Node には作成出来ない。

elas_0401.png

出典 : How Primary and Replica Shards Interact | Elasticsearch: The Definitive Guide [2.x] | Elastic

Index

Document を纏めるもの。 インデックス 付け、検索、更新、削除全てを行う。

Reindex | Curator Reference [5.1] | Elastic

インデックスを貼り直すとかって事もできる。 ここらへんが検索周りのチューニングポイントらしい。

Type

Document を種類別に分別することができる。 データベースで言うところの table

Document

1レコード分の情報が入ってるもの。 JSON で表現される。

Index Type Document の相関図はこんな感じ el-640x374.png

出典: データ構造について – AWSで始めるElasticSearch(4) | Developers.IO

まとめ

登場人物整理したら、なんとなく事例でチューニングした話とかも分かりそうな気がした。 あくまで、気がしているだけ。 図とかは出典元を見ていただけるとうれしいです。

番外編

こいつで、docker-compose up ってするだけで、 kibana と elasticsearch がどっちも立ち上がるんだって :sugoi:

version: '2'
services:
  kibana:
    image: kibana
    links:
      - elasticsearch:elasticsearch
    ports:
      - 5601:5601

  elasticsearch:
    image: elasticsearch
    ports:
      - 9200:9200
      - 9300:9300

公式もサポートしているっぽい。 library/elasticsearch - Docker Hub

Elasticsearch 先輩との戯れ日記(データの投入)

この記事何?

お仕事で Elasticsearch 先輩を使うことになったので、そのお戯れの記録

複数ドキュメントを扱う API って何がある?

Document APIs | Elasticsearch Reference [5.4] | Elastic

Multi-document API いっぺんにデータ突っ込んだり取ってきたりするやつっぽいな。

  • Multi Get API(データの取得)
  • Bulk API(データの挿入)
  • Delete By Query API(データの削除)
  • Update By Query API(データの更新)
  • Reindex API(インデックスの貼り直し)

データの投入について

Bulk API

Bulk API | Elasticsearch Reference [5.4] | Elastic

The bulk API makes it possible to perform many index/delete operations in a single API call. This can greatly increase the indexing speed.

endpoint は /_bulk, /{index}/_bulk, {index}/{type}/_bulk の3つ。 パラメーターに JSON 形式で挿入したいデータを指定する。 もし、データを送りたい場合は、

  • データの最終行に改行(\n)を入れる
  • もし改行したい場合は、 carriage return (\r) を使ってね
  • Content-Type: application/x-ndjson をヘッダーに含めて送ってね

を守ること。

$curl -X POST -H 'Content-Type: application/x-ndjson' 'localhost:9200/test/account/_bulk?pretty&refresh' --data-binary "@accounts.json"  
   ...
    {
      "index" : {
        "_index" : "test",
        "_type" : "account",
        "_id" : "990",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "created" : true,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "account",
        "_id" : "995",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "created" : true,
        "status" : 201
      }
    }
  ]
}

CSV ファイルから直接いけるか?

:no_good: :no_good: デフォルトでは CSV ファイルから、直接ぶち込むことはできず、一旦 JSON 形式に変換してあげる必要がありそう。

$ curl -s -X POST -H 'Content-Type: text/csv' 'localhost:9200/test/feed/_bulk?pretty&refresh' --data-binary "@test_feed1000.csv" | jq
{
  "error": "Content-Type header [text/csv] is not supported",
  "status": 406
}

打開案

1手目はできそうだけど、そこまでして、 Elasticsearch の内部でデータを持つ必要があるのか説はあるな。。。

注意点

設計によっては、 OOM とかが頻発しちゃうのは結構怖さあるな。 OOM 発動しなくてもデータ破損とかはかなり痛いしな もうちょっとここは追加調査が必要。 場合によっては、検索/保存で仕組みを切り離しちゃうのも1つなんだろうなというお気持ちになりました。

余談

データの取得

ついでに確認のために、調べたので、データの取得も _search に query を投げてあげれば取れる。 from で指定する値が行数。 size が何行取り出すか。

以下の例は100行目から2行取り出すとなるので、100,101行目が取り出せている。

$ curl -s -X GET http://localhost:9200/test/account/_search -d '{"query":{"match_all": {}}, "from": "100", "size": "2"}' | jq .
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1000,
    "max_score": 1,
    "hits": [
      {
        "_index": "test",
        "_type": "account",
        "_id": "383",
        "_score": 1,
        "_source": {
          "account_number": 383,
          "balance": 48889,
          "firstname": "Knox",
          "lastname": "Larson",
          "age": 28,
          "gender": "F",
          "address": "962 Bartlett Place",
          "employer": "Bostonic",
          "email": "knoxlarson@bostonic.com",
          "city": "Smeltertown",
          "state": "TX"
        }
      },
      {
        "_index": "test",
        "_type": "account",
        "_id": "408",
        "_score": 1,
        "_source": {
          "account_number": 408,
          "balance": 34666,
          "firstname": "Lidia",
          "lastname": "Guerrero",
          "age": 30,
          "gender": "M",
          "address": "254 Stratford Road",
          "employer": "Snowpoke",
          "email": "lidiaguerrero@snowpoke.com",
          "city": "Fairlee",
          "state": "LA"
        }
      }
    ]
  }
}

というわけで、100行単位でページングみたいな使い方はできそうということが分かった。