ファイルを直接読み込んで集計する

こんにちは。マーケティングプロダクト開発部の中村です。今回は大量のデータを対象に集計できる Hive の使い方について説明しようと思います。

前提

私が所属しているマーケティングプロダクト開発部では広告配信も行っています。その広告配信では大量のアクセスログを蓄積しています。通常ですとそのログは Amazon Redshift で簡単に集計できます。しかし、ログファイルを直接集計しなければならない場合が稀にあります。その際に使用しているのが Amazon EMR です。今回は Hive を用いてその集計を手元の端末で試してみます。

インストール

まずは動作環境を作るために Hive をインストールします。

brew install hive

集計する前の準備

Hive は任意のディレクトリを作業ディレクトリとすることができます。まず、その作業ディレクトリを作成し、そのディレクトリに移動しておきます。

mkdir -p /tmp/cookpad/logs
cd /tmp/cookpad

次に、その作業ディレクトリで使用する Schema の種類を指定する必要があります。今回は資料でデフォルトで使われてる derby を選択します。

schematool -initSchema -dbType derby

ここまでの作業で、ローカルで起動させるための準備ができました。次に、実際に起動させてみます。

hive

Hive のコンソールが立ち上がれば成功です。

集計してみる

次にサンプルをもとに簡単に集計します。今回は Nginx のアクセスログをサンプルとして集計してみようと思います。具体的には以下の内容のファイルを /tmp/cookpad/logs/nginx.log として、先程作成したディレクトリ以下に保存します。

172.17.0.1 - - [14/Jul/2017:07:48:37 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:48:38 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:48:38 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:19 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:40 +0000] "GET /hoge HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:42 +0000] "GET /hoge HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:51 +0000] "GET /piyo HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:52 +0000] "GET /piyo HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"
172.17.0.1 - - [14/Jul/2017:07:49:53 +0000] "GET /piyo HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" "-"

次にそのファイルを集計するクエリを用意します。なお Hive は起動時にクエリをファイルから読み込めるため、ファイルに書いた方が後から参照できて便利です。具体的には以下の内容で /tmp/cookpad/sample.q として保存します。

drop table nginx_logs;

create external table nginx_logs (
    remote_addr string
    , remote_user string
    , time_local string
    , method string
    , path string
    , protocol string
    , status int
    , body_bytes_sent int
    , http_referer string
    , http_user_agent string
)
row format serde
  'org.apache.hadoop.hive.serde2.RegexSerDe'
with serdeproperties (
    "input.regex" = "([0-9\\.]+) - ([^ ]*) \\[([^\\]]*)\\] \"([^ ]*) ([^ ]*) ([^ ]*)\" ([0-9]*) ([0-9]*) \"(.*)\" \"(.*)\""
)
location
    'file:///tmp/cookpad/logs'
;

select
    *
from
    nginx_logs
where
    path = '/hoge'
;

その保存したファイルを指定して Hive を起動します。

cd /tmp/cookpad/
hive -f /tmp/cookpad/sample.q

クエリが実行されてアクセスログが表示されれば成功です。

UDF を書いて Hive に組み込む

Hive には様々な関数が組み込まれています。通常の集計ではその関数で充分なのですが、時折複雑な条件で集計したくなります。そのようなときは関数を自作して組み込んで使用することができます。

実際に関数を組み込んで集計してみます。ただし、少し準備することが多いので事前にコードは GitHub に用意しておきました。以下のリポジトリを任意の場所に clone してください。

https://github.com/devisualy/udf

そのクローンした場所に移動してビルドします。具体的には以下のコマンドを実行します。

mvn package

ビルド成功すると target/devisualy_udf.jar ができるはずなので Hive の作業ディレクトリに配置します。

cp target/devisualy_udf.jar /tmp/cookpad/devisualy_udf.jar

Hive を起動して組み込んでみます。

cd /tmp/cookpad
hive
hive> ADD JAR /tmp/cookpad/devisualy_udf.jar;
hive> CREATE TEMPORARY FUNCTION converter as 'devisualy.Converter';

OK のような表示が出れば成功しています

UDF を使って集計する

上記までで Hive 上で自作の関数を使う準備は整いました。次に実際にクエリを投げてその関数を使ってみます。具体的には以下のようなクエリを Hive のコンソール上で実行します。

select method, converter(method) from nginx_logs limit 1;

クエリ内で同じ method を参照していますが converter という関数の戻り値が method とは異なっているのを確認できます。

これまでの作業でできるようになったこと

上記までの作業で以下のことができるようになりました。

  • 集計対象ファイルを Hive で読み込めるようになった
  • そのファイルに対してクエリを投げられるようになった
  • そのクエリから自作の関数を呼べるるようになった

その中でも自作の関数を呼べるようになったのは強力です。クエリだけではなかなか表現しきれないビジネスロジックを表現できます。また UDF ファイルを共有することにより他人がその表現を簡単に流用することができます。

まとめ

Hive の基本的な使い方について説明しました。前提にも書きましたが、私が所属しているマーケティングプロダクト開発部では広告配信も行っているため、大量のアクセスログを蓄積しています。そのログに対してローカルで Hive を使うということは無く Amazon EMR を使って高速にログを集計しています。

AWS には、似たようなことが簡単に可能な Amazon Athena というサービスもあります。しかし、集計対象のデータが大きいと料金が高くなるため Amazon EMR を使ったほうが良い場合もあるかと思います。

これらの技術により、大量のデータを高速にかつ簡単に集計できるようになりました。次はその集計結果をどのように活用できるかです。いつかログからユーザーを想像できるようになれればいいなと思っています。

Cookpad Ruby Hack Challenge

f:id:koichi-sasada:20170629153022p:plain

技術部の笹田です。Ruby インタープリタの開発をしています。最近は Fiber まわりを10年ぶりにいじってます。

2017/08/30, 31 に、Cookpad Ruby Hack Challenge というイベントを行いますので、その宣伝をさせてください。

Cookpad Ruby Hack Challenge とは

クックパッドで Ruby インタプリタを Hack しよう!

クックパッドをはじめ、多くのウェブアプリケーション開発でプログラミング言語 Ruby が利用されています。Ruby で書かれたプログラムを動かすときは Ruby インタプリタで実行します。

Cookpad Ruby Hack Challenge は、この Ruby インタプリタに対して機能を追加したり、改良したり、性能向上させたりする方法、つまり Ruby インタプリタを Hack する方法を、二日間かけてお伝えするイベントです。

イベント概要

二日間かけて、Ruby インタプリタをハックします。一日目は共通課題として、用意する資料を手順どおりに進めて頂きます。二日目は発展課題として、Ruby インタプリタに残る未解決問題に取り組んで頂きます。両日とも、講師は Ruby コミッタの笹田が務めます。

また、二日目には特別企画として、クックパッド以外の Ruby 開発者とも交流できる場を用意したいと思っています。Ruby という言語や、Ruby インタプリタに対する疑問や意見がある人は、この機会にぜひ議論してもらえればと思います。

学生の方には、ぜひ夏休みのアクティビティの一つとして楽しんで貰えると幸いです。

なお、本イベントは、以前ご紹介した社内イベント Hackarade: MRI Internal Challenge を、よりわかりやすくブラッシュアップしたものになります。

こんな方に来てほしい

  • Ruby インタプリタの Hack がしてみたい方
  • Ruby プログラムは書けるけど、どうやって動いているのか知りたい方
  • プログラミング言語 Ruby および Ruby インタプリタを普段開発しているような人達が、何を考えているのか知りたい方
  • 難易度の高いプログラミングに挑戦してみたい方
  • 夏休みの思い出が欲しい方

共通課題(1日目)のゴール

  • Ruby のソースコードの構造を知る
  • Ruby のビルドができるようになる
  • Ruby の中身を弄ることができるようになる

発展課題(2日目)のゴール(できれば)

  • 未解決問題を解決する
  • 実際に Ruby インタプリタへの貢献を体験する
  • 開発コミュニティへの参加を体験する

スケジュール

注意:時間等は変わる可能性があります。

開催前

  • 7/27 (木) 募集締め切り
  • 7/29 (土) 参加者抽選決定
  • Gitter を用いたオンライン予習サポート(希望者)

8/30 (水) 一日目

  • 10:00 オープニング
  • 10:30 ハックに必要となる事前知識の講義
  • 12:00 ランチ
  • 13:00 共通課題
  • 16:00 発展課題の紹介と割り振り

8/31 (木) 二日目

  • 10:00 発展課題の開始
  • 11:30 まつもとゆきひろ氏 特別講演
  • 12:00 Ruby開発者を交えてのランチ
  • 13:00 Ruby開発者との Q&A セッション
  • 14:00 発展課題の再開
  • 18:00 打ち上げパーティー

なんでクックパッドで開催するの?

前節までは、募集ページそのままの内容でした。これだけではなんなので、本イベントを開催する動機について、少し触れておきます。

いくつかあるのですが、まずは IT エンジニアコミュニティへの還元です。クックパッドは「毎日の料理を楽しみにする」という目標のために、多くの IT 技術を活用しています。我々は OSS としてプロダクトを公開したり、この開発者ブログでの情報発信をすることで、エンジニアコミュニティへの還元を行っていますが、本イベントはその一環です。おそらく、本イベントに興味を持つ方は、向上心があり、これからのエンジニアコミュニティを牽引して下さる、かもしれない、方々だと思うので、そのような方々へ支援することは大事なことだと思っています。

笹田個人の動機としては、このようなイベントをきっかけに言語処理系といったシステムソフトウェア開発に興味を持つ人が少しでも増えてくれたり、優秀な Ruby コミッタが増えてくれると嬉しいです(Ruby にはまだまだ改良の余地がありますが、人手が足りていません)。最近、システムソフトウェアといったコンピュータの「裏方」に興味を持ってくれる人が少なくなっているような気がします。いろいろな理由があるかと思いますが、一つは難しそう、といった「とっつきづらさ」があるのではないかと思います。本イベントを通して、そのようなハードルを越えるお手伝いができればと思っています。

このような思いを実現するためには、長期的に継続して活動を行っていく必要があると考えており、本イベントはその第一歩です。うまくいけば、第二弾や、別テーマでの開催も検討できるのではないかと思っています。そもそも、クックパッド1社でやってもスケールしないので、もっと広げたいと思っており、夢(だけ)は広がります。というわけで、まずは本イベントがうまくいくといいなぁ。

おわりに

というわけで、Cookpad Ruby Hack Challenge のご紹介でした。 ご興味とお時間がある方は、ぜひご応募ください。

UICollectionView の Layout で悩んだら

こんにちは、サービス開発部の氏です。
主にiOSのクックパッドアプリの開発を担当しています。

UICollectionViewLayout みなさん使ってますか?
UICollectionView でレイアウトを組む際、実際触り始めると実装するための選択肢が複数あり、どれが最適なのか悩ましい場面に遭遇する人もいるのではないかと思います。
今回は、自分が業務で触れた際に得た知見について軽くお話したいと思います。

UICollectionVIewLayout とは

UICollectionView は Cellのサイズや余白等のレイアウトを管理するため、プロパティとして、 UICollectionViewLayout を所持しています。
この UICollectionViewLayout に手をいれることによって、レイアウトを好きな形に変更することができます。

レイアウトを組み立てるときの複数の選択肢

実際に UICollectionViewLayout をいじろうとすると、大きく分けて三つの選択肢が出てきます。

  1. UICollectionViewFlowLayout を調整する
  2. UICollectionViewDelegateFlowLayout を実装する
  3. UICollectionViewLayout (Custom) を作成する

つづけて、各Layoutで出来ること、出来ないことを挙げていきたいと思います。
どんなレイアウトの組み上げ方をすればよいか等、判断に困った際の参考にしていただければ幸いです。

1. UICollectionViewFlowLayout を調整する

一つ目は UICollectionViewFlowLayout をそのまま利用する方法です。
InterfaceBuilderで UICollectionView を設置すると、初期値としてこの UICollectionViewFlowLayout が設定されています。
UICollectionViewFlowLayout では、CellやHeader/FooterのSize等がプロパティとして用意されており、それを変更するだけで良い感じに組み上げてくれます。

let flowLayout = UICollectionViewFlowLayout()  
let margin: CGFloat = 3.0  
flowLayout.itemSize = CGSize(width: 100.0, height: 100.0)  
flowLayout.minimumInteritemSpacing = margin  
flowLayout.minimumLineSpacing = margin  
flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)  
let collectionViewController = CollectionViewController(collectionViewLayout: flowLayout)  

ですが、Cellの大きさを決める itemSize では、動的な変更が行なえません。
全てのCellを同じ大きさで表示するのであれば、UICollectionViewFlowLayout を利用すると良いでしょう。

2. UICollectionViewDelegateFlowLayout を実装する

二つ目は UICollectionViewDelegateFlowLayout を実装する方法です。 UICollectionViewDelegateFlowLayoutUICollectionViewDelegate を継承した Protocolになっており、各種便利メソッドが用意されています。
基本的には、 UICollectionViewFlowLayout のプロパティと同等なものが準備されています。

extension CollectionViewController: UICollectionViewDelegateFlowLayout {  

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {  
        if indexPath.row % 3 == 0 {  
            return CGSize(width: 100.0, height: 100.0)  
        }  
         return CGSize(width: 60.0, height: 60.0)  
    }  

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {  
        return UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)  
    }  

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {  
        return margin  
    }  

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {  
        return margin  
    }  
}  

このレイアウトの利点としては、 indexPath の情報を参照出来るため、動的なCellのサイズ変更が可能です。
ですが、Protocolとして用意されているものでしか変更を行うことが出来ないため、アニメーションを伴う変化には余り適していないと思います。

3. UICollectionViewLayout (Custom) を作成する

最後は UICollectionViewLayout を継承した独自レイアウトを作成する手段です。

自由にレイアウトを組める反面、今までに挙げた2通りの様に良しなにレイアウトを組んでもらえません。
CellやSectionなど各要素の配置先を計算する必要があり手間がかかりますが、その分動的なサイズ変更やレイアウト変更を好きなように行うことができます。(自分で書くので当然ですが…)

UICollectionViewLayout を継承して利用するには、下記の処理を実装する必要があります。

collectionViewContentSize: CGSize

UICollectionViewcontentSize を返します。
UICollectionView は、この contentSize をもとにスクロール量を判断します。
その為、ここでは表示させたい要素に応じた正確な contentSize を返さないと思った通りの位置までスクロールをしてくれません。

layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

IndexPath に応じたCellの UICollectionViewLayoutAttributes を返します。
UICollectionViewLayoutAttributesIndexPath に応じたセルのレイアウト属性です。
この layoutAttributes にCellのサイズと座標を指定しておくと、指定通りの座標に表示されます。
この中でCellのサイズ計算等を行う場合、時間がかかる処理などがあるとカクつきの原因となります。
よくある方法として、prepare() でレイアウト情報を先に計算して配列などに用意しておきここではその情報を返すだけとするケースが多いです。

layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

範囲内に含まれる UICollectionReusableView (cellやsupplementary view)の UICollectionViewLayoutAttributes の配列を返します。
基本的には、その範囲に含まれる layoutAttributesForItem(at indexPath: IndexPath) を取得してくる形になるでしょう。

アニメーションについて

レイアウトを作っていると、アニメーションを求められるケースがそれなりにあるかと思います。
レイアウト変更時のアニメーションは下記の様な形でアニメーションを行うことができます。

collectionView.setCollectionViewLayout(newLayout, animated: true)  

他には、Cellの生成時や削除時のレイアウト属性を返すメソッドがあり、それを実装することで insertItems, deleteItems でもアニメーションをさせる事ができます。

  • initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath)
  • finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath)

また、「UICollectionViewController, UINavigationController の組み合わせでのみ」という制限がありますが、
UICollectionViewControlleruseLayoutToLayoutNavigationTransitions の値を true にしてpushさせると UINavigationBar と連携した遷移が可能です。

let viewController = UICollectionViewController(collectionViewLayout: newLayout)  
viewController.useLayoutToLayoutNavigationTransitions = true  
navigationController?.pushViewController(viewController, animated: true)  

UICollectionViewController 以外への遷移アニメーションは、 UIViewControllerAnimatedTransitioning を実装してあげると良いでしょう。

まとめ

UICollectionView のレイアウトを作るに当たって、ほとんどのケースでは UICollectionViewFlowLayoutUICollectionDelegateFlowLayout で事足りるかと思います。

独自レイアウトを採用するケースとしては、行ベース、グリッドベース以外のレイアウトが必要なケースや、各要素のレイアウトが頻繁に変化する場合に必要になってきます。(カバーフローのようなものだったり)

UICollectionView はiOS10から prefetchUICollectionViewFlowLayoutAutomaticSize 等の新しい機能も追加され、表現の幅も増えています。

クックパッド内で利用するのはまだ少し先かもしれませんが、ユーザーがより良い体験を提供できるよう常に心がけていきたいですね。