astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

理解しやすいcssアーキテクチャにするためのtips

こんにちは、主にフロントエンド周りを担当させて頂いているSと申します。

いきなりですが「良いcss」とは何でしょうか。

  • validなcss
  • tableを避けてdivやulで擬似的に表現するcss
  • cssで表現できる事はcssで表現しているcss(svg,data URI,animationの利用)
  • postcssなど最新の技術を取り入れているcss
  • styleguideを作成しているcss

確かにこれらは「良いcss」ですが、それだけでは足りません。

「良いcss」は「良いアーキテクチャ」である事が必要です。

幸運な事に私たちには、oocss,smacss,bemなどの素晴らしいアーキテクチャがありますが、単純にこれらを利用するだけでは「良いアーキテクチャ」にはなりません。

「良いアーキテクチャ」にするためには

  • 理解しやすい
  • 探しやすい
  • 修正しやすい

という3つの"-able"が必要なのです。

本記事では私が3つの"-able"を満たすために重要だと考えるtipsを、3回に分けてつらつらと書いていこうと思います。

初回は「理解しやすいcssアーキテクチャにするためのtips」です。

小生、twitterの140文字も超えられないもので拙い文章になりますが、最後までお付き合い頂けると幸いです。


tips1 : 力を入れるべき配分は 8:2

私がcssを書く時に力を入れる配分は、アーキテクチャ8割、コーディング2割 でおこなっています。

良いアーキテクチャは良い名前をうみます。そして、これらは探しやすさ、理解しやすさ・修正しやすさをもたらしてくれるからです。

一度、エクセルやUML、マインドマップなどのツールを用いながら、時間をかけじっくりと良いアーキテクチャを構築して見てください。私が言った事が真実であると分かるはずです :)

tips2 : 要素の特性を明記しよう

例えば、oocssの設計思想として Separate structure and skin(構造と見た目の分離) があります。

その例としてよく下記のコードが出されますがこれは例としては良いコードではありません。

<button class="btn btn-blue">ボタン</button>

確かにこのコードは読みやすいですが、この質問に答えることは誰もできないはずです。

「何のためのボタン?」

  • btn = ボタン
  • btn-blue = 青いボタン

ボタンの特性上、必ず「目的」がありますが、このボタンからは「青いボタン」であるということ以外何もわかりません。

要素の特性(この場合は目的)を明記することで「理解しやすいコード」になります。

<button class="btn btn-error">ボタン</button>

tips3 : 略称ダイエットはリーサルウェポン

webサイトの高速化はユーザー体験を向上させる素晴らしいデザイン手法であり、最も重要視すべき項目の一つで、その方法として「ファイルサイズのダイエットのためにクラス名を略称化する」がありますが、過度な略称は避けるべきです。

下記コードはgoogle検索結果ページから抜粋し、一部修正したhtmlになります。

<div class="rc">
  <h3 class="r">
    <a href="XX">hogeとは</a>
  </h3>
  <div class="s"><div>
  <div class="f kv _SWb">
    <cite class="_Rm bc">aaa › bbb › ccc</cite>
    <div class="action-menu ab_ctl">
      <a class="_Fmb ab_button" href="#">
        <span class="mn-dwn-arw"></span>
      </a>
      <div class="action-menu-panel ab_dropdown">
        <ol>
          <li class="action-menu-item ab_dropdownitem" role="menuitem">
            <a class="fl" href="xxx">キャッシュ</a>
          </li>
          <li class="action-menu-item ab_dropdownitem" role="menuitem">
            <a class="fl" href="XXX">類似ページ</a>
          </li>
        </ol>
      </div>
    </div>
  </div>
  <span class="st">abcdefg...</span>
</div>

このrc, r, s, f, flの意味が分かる方が一体何人いるでしょう。 スタイルガイドなどのドキュメントを読まなければまず理解できない略称です。

メンバーが成熟した時に、高速化の最後の方法として行いましょう。

tips4 : 無意味な連番

名付けに困ってつい連番で逃げてしまうことがあるかもしれませんが、それは今日で終わりにしましょう。「過度な略称」と同じ、いや、「過度な略称」よりも悪い命名です。

何故ならば、ドキュメントに連番である意味は書きようがないからです。

<ul>
  <li class="list01">hogehoge</li>
  <li class="list02">piyopiyo</li>
</ul>

tips5 : 「ちょっとだけ」が命取り

安易な命名や追加、追記を行ってはいけません。 例えタイトなスケジュール、言語化できないユニークなコンポーネントの大群に襲われたとしても、です。

何故ならば、その行為はwebサイト・アプリケーションはじわじわと蝕ばみ、気づけば誰の手にも負えないほどの重症を負わせる事になるからです(経験のある方も多いことだと思います)。それは2年後かもしれませんし、もしかしたら1週間後かもしれません。

「ちょっとだけ」。

その気持ちが後々の自分たちの首を締め上げる事になるのは想像できるでしょう。

tips6 : リファクタリングは定期的に

リファクタリングを定期的に行い、読みやすさを維持・向上していきましょう。

読みやすさはcssの保守性を維持するためにとても有効な手段となります。

tips7 : デザイナーとのコミュニケーションを欠かさない

時としてデザイナーがとんでもないモンスターに見える事があるかもしれません。 ですが決してモンスターボールを投げつけてはいけません。

我々はチームで1番の良き理解者でならなくてはならないのです。 彼・彼女らの力添えなしにコンポーネントを言語化する事は大変困難な事です。

たくさん議論して、素晴らしいコンポーネントを生み出していきましょう。

ここまで読んでいただきありがとうございました。 次回は「探しやすいcssアーキテクチャにするためのtips」です。

HadoopのWordCountを天気予報のデータに適用してみよう!

自己紹介

こんにちは、astamuseでデータエンジニアをやってる朴と申します。

astamuse入社3年目になります。 最初の1年間はweb開発エンジニアをやってましたが、 もともとデータを色々いじるのが好きだったので、上司と相談して1年前から現在の仕事をさせていただくことになりました。 ←('-')(ということで弊社はやりたい仕事が出来る環境ですよーというアピールでした)

仕事では主にHadoop,Sparkを使っておりますが、Hadoop歴が1年未満というのもあり まだまだ勉強しながらやっているという感じです。

さて本日はHadoopの初心者向けの内容を書いてみようと思います。

Hadoopの初心者向けの内容といえば WordCountが結構定番になりますが、

今日はこちらのサンプルを少し改造して天気予報データの集計を取ってみたいと思います。

やりたいこと&データ準備

気象データから特定の地域の降雨量を月ごとに合計したプログラムを作りたいと思います。
気象データは気象庁のホームページからダウンロードできます。

  • 地点を選ぶ

    • 埼玉県に住んでるので、埼玉県のさいたまと熊谷(いつも天気予報のニュースで取り上げられてるので)を選択
  • 項目を選ぶ

    • データの種類
      • 日別値
    • 項目
      • 降水タブで降水量の日合計を選択
  • 期間を選ぶ

    • 連続した期間で表示する(とりあえず、5年間:2011年1月1日~2015年12月31日)
  • 表示オプション

    • 何もしない

右側のcsvをダウンロードを押す→ダウンロードは一瞬で終わるはず

ダウンロードしたcsvはこんな感じ

ダウンロードした時刻:2016/07/19 12:12:46


           熊谷             さいたま
           降水量の合計(mm) 降水量の合計(mm)
2011年1月1日,--,0
2011年1月2日,--,0
2011年1月3日,--,0
・
・
・

hdfs上処理しやすいように、以下の形に整形してutf-8で保存する(--は0に置換)

2011/1/1,0,0
2011/1/2,0,0
2011/1/3,0,0
2011/1/4,0,0
2011/1/5,0,0
2011/1/6,0,0

1カラム目:日付
2カラム目:さいたま市の降雨量
3カラム目:熊谷市の降雨量

一次整形

上記のデータをhdfs上保存する

hadoop fs -put ./weather_saitama.csv hadoop-example/input

一次整形としていったん月と地域の組み合わせで降雨量の合計値を出力したいと思います。

saitama-201101    6.5
kumagai-201101    4.5
saitama-201102    6.5
kumagai-201102    4.5

Mapperはこんな感じ↓

public class TokenizerMapper extends Mapper<Object, Text, Text, DoubleWritable> {

        private Configuration conf;

        @Override
        public void setup(Context context) throws IOException, InterruptedException {
            conf = context.getConfiguration();
        }

        @Override
        public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            String line = value.toString();
            String[] cols = line.split(",");
            if (cols.length == 3) {
                String[] date = cols[0].split("/");
                String saitamaRainfall = cols[1];
                String kumagaiRainfall = cols[2];
                context.write(new Text("saitama" + "-" + date[0] + String.format("%02d", Integer.parseInt(date[1]))),
                        new DoubleWritable(saitamaRainfall));
                context.write(new Text("kumagai" + "-" + date[0] + String.format("%02d", Integer.parseInt(date[1]))),
                        new DoubleWritable(kumagaiRainfall));
            }
        }
}

Reducerはこんな感じ↓

public class IntSumReducer extends Reducer<Text, DoubleWritable, Text, DoubleWritable> {
        private Configuration conf;
        private DoubleWritable result = new DoubleWritable();

        @Override
        public void setup(Context context) throws IOException, InterruptedException {
            conf = context.getConfiguration();
        }

        public void reduce(Text key, Iterable<DoubleWritable> values, Context context) throws IOException, InterruptedException {
            Iterator<DoubleWritable> valueIter = values.iterator();

            Double sum = 0;
            for (DoubleWritable val : values) {
                sum += val.get();
            }

            result.set(sum);

            context.write(key, result);
        }

}

Jobをスタートするメインクラス

public class WeatherAnalyzer {

    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        GenericOptionsParser optionParser = new GenericOptionsParser(conf, args);
        String[] remainingArgs = optionParser.getRemainingArgs();

        FileSystem.get(conf).delete(new Path(remainingArgs[1]), true);

        Job job = Job.getInstance(conf, "WeatherAnalyzer");
        job.setJarByClass(WeatherAnalyzer.class);
        job.setMapperClass(TokenizerMapper.class);
        job.setReducerClass(IntSumReducer.class);

        FileInputFormat.addInputPath(job, new Path(remainingArgs[0]));
        FileOutputFormat.setOutputPath(job, new Path(remainingArgs[1]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

}

hadoop jar hadoop-example-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.astamuse.hadoop.example.WeatherAnalyzer hadoop-example/input/weather_saitama.csv hadoop-example/output

実行して結果を見てみると

hadoop fs -ls hadoop-example/output

output/part-000
output/part-001

中身を見ると以下のように合計値がちゃんと出力されてますね

kumagai-201512    39.0
saitama-201111    35.0

最終形

最終的には市毎にoutputを分けようと思います。

output/saitama/part-000
output/saitama/part-001
・
・

output/kumagai/part-000
output/kumagai/part-001
・
・

ファイルを複数のフォルダに分けて出力する時はMultipleOutputsを使います。

Jobをスタートするメインクラス

public class WeatherAnalyzer {

    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        GenericOptionsParser optionParser = new GenericOptionsParser(conf, args);
        String[] remainingArgs = optionParser.getRemainingArgs();

        FileSystem.get(conf).delete(new Path(remainingArgs[1]), true);

        Job job = Job.getInstance(conf, "WeatherAnalyzer");
        job.setJarByClass(WeatherAnalyzer.class);
        job.setMapperClass(TokenizerMapper.class);
        job.setReducerClass(IntSumReducer.class);
        job.setMapOutputKeyClass(Text.class);// MultipleOutputsを使用する時、MapのKey、Valueをちゃんと指定しないと何故かError: java.io.IOException: Type mismatch in key from mapが発生
        job.setMapOutputValueClass(DoubleWritable.class);

        LazyOutputFormat.setOutputFormatClass(job, TextOutputFormat.class);// outputフォルダに空のpart-00Xファイルが大量にできるのを防ぐ

        MultipleOutputs.addNamedOutput(job, "saitama", TextOutputFormat.class, Text.class, DoubleWritable.class);
        MultipleOutputs.addNamedOutput(job, "kumagai", TextOutputFormat.class, Text.class, DoubleWritable.class);

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

}

Mapperは上と変わらず

Reducerはこんな感じ↓

public class IntSumReducer extends Reducer<Text, DoubleWritable, Text, DoubleWritable> {
        private Configuration conf;
        private DoubleWritable result = new DoubleWritable();
        private MultipleOutputs<Text, DoubleWritable> mos;

        @Override
        public void setup(Context context) throws IOException, InterruptedException {
            conf = context.getConfiguration();
            mos = new MultipleOutputs<Text, DoubleWritable>(context);
        }

        public void reduce(Text key, Iterable<DoubleWritable> values, Context context) throws IOException, InterruptedException {
            Double sum = 0d;
            for (DoubleWritable val : values) {
                sum += val.get();
            }

            result.set(sum);
            if (key.toString().startsWith("saitama")) {
                mos.write("saitama", key, result, "saitama/");
            } else {
                mos.write("kumagai", key, result, "kumagai/");
            }
        }

        @Override
        protected void cleanup(final Context context) throws IOException, InterruptedException {
            super.cleanup(context);
            mos.close();// きちんとcloseしないと書き出しが反映されない
        }

}

はい、これで出来上がりです。

次回のネタは決まっておりませんが、データ処理周りのネタを継続していきたいと思います。

最後に(余談)

弊社では辛い物が好きなエンジニア同士で定期的に中華会を行ってます。
店は筆者が大好きな羊肉串が自慢な延吉香です、 中国の本土の味(東北料理と四川料理)が堪能できますので、おすすめです!

では次回また!

1,100万文書×480万キーワード。コンパクト且つ高速な辞書マッチングのはなし

はじめまして。開発・インフラ部、福田です。

分散処理環境、ミドルウェアの整備と運用、ELT/ETL、R&D、雑用を担当しています。

舞台裏から眺めるAstamuse.com

f:id:astamuse:20160719144610p:plain

Astamuse.comは、イノベーションを起こすあなたの為のサイトです。そこでは国内約1,100万件の特許文書を誰もが見やすい形で見ることもできます。また、約480万のキーワードを収録し、キーワード経由の訪問は全体の約4割を占めています。

技術ページにはキーワードのリンクがちりばめられ、綺羅星のごとく旅人をやさしく見守っています。

アスタミューゼでは、Hadoopクラスタを運用しており、HBaseをはじめ、YARN上でのMapReduceやSparkなどを使い、語彙の抽出、XML文書の解析・変換、ドキュメントのインデクシング、画像の変換などを行っています。

これらのデータ処理において、私たちはスループットを重視しています。ここでは、1千万件を超える文書と数百万件規模のキーワード辞書マッチングの工夫についてお話します。

基本となる技術要素

トライ木、XBW、辞書マッチング

Hadoop、HBase、Java、Python、Scala、Spark、YARN

要件

まず、実現したいことを整理すると以下の2点になります。

  • 1千万件の特許文書のテキストの辞書マッチングが合理的な時間内に終えられること
  • 辞書マッチングにおいて、キーワード毎に付与したIDが解決できること

1つめは、特許文書のテキスト中の辞書キーワードを抽出する問題です。

一般的には、辞書式マッチングなどと呼ばれ、Trieというデータ構造を使用した方法が知られています。Trieとそれを利用したアルゴリズムについては、ウェブや書籍に詳しいためここでは説明を省きます。

2つめは、アプリケーションの要求で、マッチング結果にキーワード毎に付与された一意の識別子が必要になります。

背景と文脈

初期のアプローチ

当初、われわれのボキャブラリーは25万に満たない程の小さなものでした。コンテンツのリリースに向け、単純なトライ木を実装しました。デプロイされたコードは正しく動き、データは滞りなく更新されていきました。そう、あの日までは。

インシデント発生

ある午後、データ解析処理のジョブがメモリ不足に陥ることがわかりました。コンテンツ拡充のための段階的なキーワード追加の影響です。

当初のナイーブな実装ではメモリ消費量が大きすぎ、並列分散処理に支障をきたすようになりました。

本来、より早い段階で気づける問題でした。チームメンバーとのコミュニケーションミスにより、確実なテストがされないままキーワードの増加を迎えました。このことを深く反省しています。そして、反省そのものは問題を解決しません。

解決に向かって

まず、解決すべき問題を整理し、ゴールを設定しました。

ゴール定義

可能な限り早く障害を取り除き、更新を再開させること。(1週間以内に解決することが望ましい。)

課題ブレイクダウン

何をどうすればよいかを整理した結果、以下の3つを戦術目標としました。

  • メモリの消費はMapタスクあたり2G以内で収まること(フルに使えるわけではない)
  • 現実的な時間内にワークロードを終えられる実行速度が確保できること
  • 既存のプログラムのインターフェースを変えないこと
f:id:astamuse:20160719150324p:plain

調査検討の末、コンパクトかつ高速であることが期待できるXBWというアルゴリズムによるTrie木の実装を試みました。

数日間のハッキングの末、目標を達成できるコードが完成したため、回帰テストを行い、本番環境にデプロイし、更新を再開することができました。

成果の検証

ここで、当時起きたことを再現するコードとテストデータ作成し、2つの実装を比較してみます。

実験手順

全量データからサンプル抽出した25万、50万、100万、250万件と全量(約480万)の5通りのテストデータセットを用意し、それぞれデータをロードします。

平均キーワード長は7.34文字

性能の比較のため、トライ木を巡回し全キーワードのリストを出力するコードを用意し実行し

ます。

今回の実験では、ディスク書き込みの影響を無視するため標準出力に出力された結果を/dev/nullにリダイレクトしています。

XBWの方は、データを前処理し、ロード済みのオブジェクトをシリアライズし永続化したものをロード

ロード時間と、ツリー巡回の実行時間を測定 (各5回実行し、平均値を使用)

メモリの使用量についてはJava8の Native Memory Trackingを有効化し、出力のJava Heap Sizeを記録

実行環境

CPUとメモリ

4.6.3-gentoo x86_64 Intel(R) Core(TM) i7-2600K CPU @ 3.40GHz GenuineIntel GNU/Linux

16GB

Java 1.8.0_92

結果

f:id:astamuse:20160719145848p:plain

f:id:astamuse:20160719161441p:plain

計算のオーバーヘッドのためか、巡回の速度面では単純な実装に及ばないものの、メモリ消費量が低く抑えられており、ロード時間の短縮にも貢献しています。

これにより、限られたリソースで、合理的な時間内にワークロードを終えることができるようになりました。

まとめ

大規模テキスト集合と大きなキーワード辞書のマッチングを、XBWアルゴリズムによる省メモリ且つ高速なトライ木の実装を行い、単純な実装との比較を行い、XBW実装の空間効率が良いことを示しました。

学び

  • コミュニケーションは密に。確認は入念に。
  • 自分たちの使うものは自分たちでつくる。ないものは作る。
  • 最小限のリソースを最大限利用するための工夫を怠らない。

参考文献

  • 高速文字列解析の世界―データ圧縮・全文検索・テキストマイニング 岡野原 大輔 (著)
  • Faster compressed dictionary matching Wing-Kai Hon, Tsung-Han Ku, Rahul Shah, Sharma V. Thankachan, Jeffrey Scott Vitter

本記事の内容は、2014年2月頃の出来事を振り返り、記事作成にあたり新たに比較データを取得しまとめたものです。

Copyright © astamuse company, ltd. all rights reserved.