HBaseAdmin
でcompact
/split
指示をした時 (hbase shell でのコマンド発行、WebUIのボタンぽちとか)MemStore
をflushする時MajorCompactionChecker
(約3時間おきに起動) が、前回のメジャーコンパクションから24時間 (+/- 4.8時間: 後述) 以上経過しているリージョンを発見した時=> HBase のメジャーコンパクション実行時間 - 科学と非科学の迷宮
ScalaCache
が、Actor
を起動しようとする。Thread
が利用できない(必然的にActor
も利用できない)ため、エラーとなってしまう。GAECache
を利用するよう設定されない。application.conf
ファイルに反映してください。$ cd ${PLAY_HOME}/modules
$ cp -R scala-0.8 scala-0.8.1
13 private def prefixed(key: String) = "__" + key
14
15 private lazy val cacheActor =
16 actor{
17 link{self.trapExit = true;loop{react{case Exit(from: Actor, exc: Exception) =>
Actor
を遅延評価にすることで、勝手にActor
が起動するのを防いでいます。static
初期化ブロックを作ってCache
の初期化をする。 12 abstract class CacheDelegate {
13
14 static { Cache.init(); }
15
16 public void add(String key, Object value, String expiration) {
17 Cache.add(key, value, expiration);
18 }
Cache
の初期化はPlay!起動時に行われるんですが、GAECache
が設定される前に初期化されてしまうので、ここで再度初期化してあげます。 7 <path id="project.classpath">
8 <pathelement path="${play.path}/framework/play.jar"/>
9 <fileset dir="${play.path}/framework/lib">
ant
コマンドでビルドします。$ ant -Dplay.path=${PLAY_HOME}
application.conf
ファイルのScalaモジュールのバージョンを新しく作成したバージョンに合わせてください。Cache
を使うplay.cache.Cache
クラス(実体はplay.cache.ScalaCache
クラス)を使います。 53 val(followings, users, cachedAt) = Cache.get(followingsCacheName(currentUser.email), "10min") {
54 val followings = Follow.followings(currentUser)
55 val users = followings.flatMap(f=> User.get(f.following).map(user=>(f->user))).toMap
56 (followings, users, new Date)
57 }
Option#getOrElse
のような動作になります。
Serializable
である(@serializable
アノテーションが付いている)必要があります。(=> User.scala / Follow.scala)Siena is a single API with many implementations. You can use siena with relational databases, with the Google App Engine's datastore or with Amazon's SimpleDB. There is also an implementation called siena-remote very useful if you want to use the Google App Engine's datastore remotely. Other implmenetations are planned such as: HBase, DBSLayer,...
install
コマンドを使ってインストールします。$ play install siena-1.3
conf/application.conf
ファイルに、sienaモジュールを使うよう設定します。module.scala=${play.path}/modules/scala-0.8
module.gae=${play.path}/modules/gae-1.4
module.siena=${play.path}/modules/siena-1.3
Model
クラスModel
クラスを継承したエンティティクラスを実装します。Kind
となり、インスタンスがEntity
、フィールドがProperty
になります。id
値を表す@Id
アノテーションのついたLong
型のフィールド(フィールド名は任意)は必須です。name
値も利用できますが、sienaではLong
のid
値のみとなっています。List
は使えないので、JavaのList
を指定するように気をつけましょう。insert
/update
/delete
Model
クラスを継承すると、insert
/update
/delete
メソッドが使えます。id
値はinsert
時に自動採番され、値がセットされます。Model.all
メソッドでクエリオブジェクトを作成できますので、filter
/order
を設定した後にfetch
/get
/count
することでデータを取得できます。models/User.scala
id
、email
、joinedAt
、invitedAt
という4つのフィールドを定義してあります。all
メソッドをpublic
のままにしておけば、利用側でなんでもできるようになりますが、あまりなんでもできるようにすると管理ができなくなる恐れがあるので、いったんprivate
にして、外部から利用するのに必要なクエリだけを公開するようにしています。id
によるget
はOption
でくるんでおいたほうが何かと便利です。fetch
したものはtoList
を付けておけばScalaのList
として扱うことができるようになります。(import _root_.scala.collection.JavaConversions._
を忘れずに。。。)package object mapreduce {
import _root_.scala.actors.Futures._
import _root_.scala.collection.SortedMap
class Mappable[KEYIN, VALUEIN](mappee: Iterable[(KEYIN, VALUEIN)]) {
def mapper[KEYOUT, VALUEOUT](mapper: (KEYIN, VALUEIN) => Iterable[(KEYOUT, VALUEOUT)])(implicit ord: Ordering[KEYOUT]) : Iterable[(KEYOUT, VALUEOUT)] = {
mappee.map { case (key, value) => future { mapper(key, value) } }.flatMap { _() }
}
}
implicit def iterable2Mappable[A, B](m: Iterable[(A, B)]) = new Mappable(m)
class Reducable[KEYIN, VALUEIN](reducee: Iterable[(KEYIN, VALUEIN)])(implicit ord: Ordering[KEYIN]) {
def reducer[KEYOUT, VALUEOUT](reducer: (KEYIN, Iterable[VALUEIN]) => (KEYOUT, VALUEOUT)) : Iterable[(KEYOUT, VALUEOUT)] = {
reducee.foldLeft(SortedMap.empty[KEYIN, List[VALUEIN]](ord)) {
case (map, (key, value)) => {
map + (key -> (value :: map.getOrElse(key, Nil)))
}
}.map { case (key, values) => future { reducer(key, values) } }.map { _() }
}
}
implicit def iterable2Reducable[A, B](r: Iterable[(A, B)])(implicit ord: Ordering[A]) = new Reducable(r)(ord)
}
WordCount
プログラムを作りました。object WordCount {
def main(args: Array[String]) {
import mapreduce._
import _root_.scala.io.Source
def textInputFormat(lines: Iterator[String], offset: Long = 0): Stream[(Long, String)] = {
if(lines.hasNext) {
val line = lines.next
Stream.cons((offset, line), textInputFormat(lines, offset+line.length))
}
else {
Stream.empty
}
}
val source = Source.fromFile(args(0))
try {
textInputFormat(source.getLines).mapper {
(offset, str) => {
str.split("\\W+").collect { case word if word != "" => (word -> 1) }
}
}.reducer {
(word, counts) => {
word -> (counts.sum)
}
}.foreach { case (key, value) => println("%s: %d".format(key, value)) }
} finally {
source.close
}
}
}
textInputFormat
関数)が、本体は後半です。map
フェーズでは、ファイルの各行毎にmapper
関数が呼ばれ、各行を単語に分割して、 (word -> 1)
の組のリストを出力しています。reduce
フェーズでは、reducer
関数が呼ばれる前にキーで並べ替え&同じキーに対応するバリューをリストにまとめる(shuffle
フェーズ、正確にはreduce
フェーズの前)という処理をします。( word -> ( 1, 1, 1, ... ) )
に対してreducer
関数が呼ばれ、最終的な結果となります。shuffle
フェーズのおかげで、MapReduceがシンプルかつ強力なプログラミングモデルとなっています。shuffle
フェーズは、「魔法が生まれる場所」と言われています。mapper
/reducer
関数の呼び出しにfuture
を使っています。apply()
メソッドを呼び出します。(implicit ord: Ordering[A])
def iterable2Reducable[A, B](r: Iterable[(A, B)])(implicit ord: Ordering[A]) = new Reducable(r)(ord)
Ordering[A]
がどこかで定義されていなければ、メソッド呼び出しができないので、A
は並べ替え可能である、と保証できます。A <: Ordered[A]
である場合にも Ordering[A]
が自動的に導かれるようになっています。Stream
Stream
は、無限リストを実現するためのクラスです。lazy val fib: Stream[BigInt] = Stream.cons(0, Stream.cons(1, fib.zip(fib.tail).map(p => p._1 + p._2)))
Stream
を使いこなせないんですが、ハマれば強力な武器になります。Iterable
インスタンスからMappable
/Reducable
クラスのインスタンスに変換しています。mapper
/reducer
というメソッド名ではなく、map
/reduce
としたかったのですが、元からあるメソッドと同名のメソッド(引数違い)ではimplicit conversionの手がかりにはならない(?)ようで、うまく変換されませんでした。GAE
クラスGAEモジュールが提供するplay.modules.gae.GAE
クラスに便利なメソッドがたくさん定義されています。login()
/logout()
メソッドを使います。login()
メソッドController
からこのメソッドを呼べばGoogleアカウントのログイン画面にリダイレクトします。"ControllerClassName.actionMethodName"
の形式の文字列です。logout()
メソッドController
からこのメソッドを呼べばGoogleアカウントからログアウトします。login()
メソッドと同様、"ControllerClassName.actionMethodName"
の形式の文字列です。logout()
メソッドは呼び出す場面が思い浮かびません。。。User
オブジェクトを、していなければnull
を返します。package controllers
import _root_.play._
import _root_.play.mvc._
import _root_.play.modules.gae._
object Application extends Controller with Defaults {
def index = Template
def login = GAE.login("Application.index")
def logout = GAE.logout("Application.index")
}
login
/ logout
アクションを追加して、UserService
へのログイン/ログアウトとしています。Defaults
トレイトDefaults
トレイトをミックスインしています。package controllers
import _root_.play._
import _root_.play.mvc._
import _root_.play.modules.gae._
trait Defaults extends Controller {
@Before
def check = {
Option(GAE.getUser) match {
case Some(user) => {
renderArgs += "user" -> user
}
case None =>
}
}
}
Application.login
とApplication.logout
の設定を追加しました。 (diff)/{controller}/{action}
という設定があるので、例えば /application/index
というパスが有効です。app/views
以下、ControllerClassName/actionMethodName.html
のようなファイル名で置かれます。$ play gae:deploy play-hello --gae=${GAE_SDK_HOME}
java.util.logging
にディスパッチされますtmp
フォルダは使えませんCache
はGAEのmemcache
のラッパになりますMail
はGAEのmail
サービスのラッパになりますUsers
サービスを利用でき、開発時には擬似ページが使えますPROD
モードになりますinstall
コマンドを使ってインストールしてもいいですが、今のところ、GAEのSDKのバージョンが1.3.7と少々古いです。fork
されていますので、こちら持ってくることにします。$ wget http://download.github.com/Ouziel-play-gae-36f7634.tar.gz
$ cd ${PLAY_HOME}/modules
$ tar zxvf /path/to/Ouziel-play-gae-36f7634.tar.gz
$ mv Ouziel-play-gae-36f7634 gae-1.1-1.4.0
$ cd gae-1.1-1.4.0
$ ant -Dplay.path=${PLAY_HOME}
$ play install gae-1.4
GAEにデプロイ
play-hello
というプロジェクトを作成して、実際にAppEngineで動作を見ていくことにします。$ play new play-hello --with scala
conf/application.conf
ファイルに、GAEモジュールを使うよう設定します。module.scala=${play.path}/modules/scala-0.8
#module.gae=${play.path}/modules/gae-1.1-1.4.0
module.gae=${play.path}/modules/gae-1.4
$ play run play-hello
war/WEB-INF/appengine-web.xml
というファイルが作成されます。<application>play-hello</application>
$ export GAE_PATH=${GAE_SDK_HOME}
$ play gae:deploy play-hello
gae:deploy
コマンドの--gae
オプションが正しく動作しないため、GAE_PATH
環境変数にSDKのインストールパスを設定する必要があります。--gae
オプションの指定の仕方が間違えていたようです。$ play gae:deploy play-hello --gae=${GAE_SDK_HOME}
eclipsify
というやつがあるのでこれを利用します。$ ${PLAY_HOME}/play eclipsify helloworld
.project
.classpath
.settings
などが生成されて、「ファイル」→「インポート」でこのプロジェクトをEclipseプロジェクトとしてインポートすることができるようになります。eclipse
ディレクトリが作成され、サーバーやテストの起動用の.launch
ファイルが作られます。run
コマンドで起動していたサーバーをEclipse上で起動させることができます。eclipse/helloworld.launch
ファイルを右クリック → Run As →helloworld
eclipse/classes
に設定されます。Properties → Build Path → "Libraries"タブ → 「Add Library...」 → 「Scala Library」
JRE System Library
の次くらいにしておくといいと思います。eclipse/Connect JPDA to helloworld.launch
ファイルを右クリック → Debug As →Connect JPDA to helloworld
Debug
パースペクティブに新しいプロセスが表示されていると思います。M-x ensime-conf-gen
で、設定ファイルの雛形を作っていきます。/path/to/helloworld
は適宜読み替えてください。)Find project root: /path/to/helloworld
Your project seems to be of type 'custom', continue with this assumption? (yes or no) yes
What is your project's name? /path/to/helloworld
What is the name of your project's main package? e.g. com.myproject:
Where is the project's source located? /path/to/helloworld/app
Where are the project's dependency jars located? /path/to/helloworld/lib
Is the Scala standard library located somewhere else? (yes or no) yes
Where are is the Scala library located? ${PLAY_HOME}/modules/scala-0.8/lib
Where are classes written by the compiler? /path/to/helloworld/eclipse/classes
/path/to/helloworld/.ensime
)ができます。eclipse/classes
に向けてあります。:sources
に、テストコード用のディレクトリ( "./test" )を追加します。:compile-jars
に、Play!の依存ライブラリがあるディレクトリを追加します。;; This config was generated using ensime-config-gen. Feel free to customize its contents manually.
(
:project-name "helloworld"
:project-package ""
:sources ("./app" "./test")
:compile-jars ("./lib" "${PLAY_HOME}/modules/scala-0.8/lib" "${PLAY_HOME}/framework" "${PLAY_HOME}/framework/lib")
:target "./eclipse/classes"
)
M-x ensime
conf/application.conf
ファイルにMavenモジュールを使うための設定を追加します。module.scala=${play.path}/modules/scala-0.8
module.maven=${play.path}/modules/maven-head
$ ${PLAY_HOME}/play mvn:init helloworld
pom.xml
ファイルが追加されます。pom.xml
ファイルの groupId
や artifactId
タグの設定は自プロジェクトに合わせて修正したほうがいいかもしれません。dependencies
タグで依存ライブラリの設定を行った後、$ ${PLAY_HOME}/play mvn:update helloworld
// or play mvn:up
lib
ディレクトリに保存してくれます。$ ${PLAY_HOME}/play mvn:refresh helloworld // or play mvn:re
$ ${PLAY_HOME}/play mvn:source helloworld // or play mvn:src
eclipsify
することでEclipseにも認識させることができます。eclipsify
やensime-conf-gen
などで生成した設定ファイルは、バージョン管理に含めないほうがいいようです。pom.xml
ファイルは環境依存しにくいので大丈夫です。というかこれは同じものを使うべき。$ unzip /path/to/play-1.1.zip
${PLAY_HOME}
を表記します。play
コマンドで行います。$ ${PLAY_HOME}/play install scala-0.8
$ ${PLAY_HOME}/play install maven-head
play
コマンドでプロジェクトを作成します。$ ${PLAY_HOME}/play new helloworld --with scala
--with scala
を付けることで、Scalaモジュールを読み込んだ状態でプロジェクトが作成されます。$ ${PLAY_HOME}/play run helloworld
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.1, http://www.playframework.org
~
~ Ctrl+C to stop
~
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
Listening for transport dt_socket at address: 8000
21:49:45,400 INFO ~ Starting /Users/ueshin/workspace/helloworld
21:49:45,402 INFO ~ Module scala is available (/usr/local/play-1.1/modules/scala-0.8)
21:49:45,959 WARN ~ You're running Play! in DEV mode
21:49:46,033 INFO ~ Listening for HTTP on port 9000 (Waiting a first request to start) ...
Your new application is ready!
package controllers
import play._
import play.mvc._
object Application extends Controller {
def index = "Hello World!"
}
Template
から "Hello World!"
に修正)して画面をリロードすると、先程の画面の代わりにHello World!
Ctrl-C
です。Ctrl-C
# Run the unit tests in this test bundle.
"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests"
# Run the unit tests in this test bundle.
"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests" 1> /tmp/RunUnitTests.out
/Developer/Library/Frameworks
#import <SenTestingKit/SenTestingKit.h>
#import "Hello.h"
@interface HelloTest : SenTestCase {
Hello *hello;
}
@end
@implementation HelloTest
- (void)setUp {
hello = [[Hello alloc] init];
}
- (void)testHello {
STAssertEqualObjects([hello sayHello:@"ueshin"], @"Hello, ueshin", @"sayHello:@\"ueshin\" must be @\"Hello, ueshin\".");
}
- (void)tearDown {
[hello release];
}
@end
#import <Foundation/Foundation.h>
@interface Hello : NSObject {
}
- (NSString*)sayHello:(NSString*)name;
@end
#import "Hello.h"
@implementation Hello
- (NSString*)sayHello:(NSString *)name {
return [NSString stringWithFormat:@"Hello, %@", name];
}
@end
#import "Hello+Sample.h"
@interface FIXCATEGORYBUG_HELLO_SAMPLE @end
@implementation FIXCATEGORYBUG_HELLO_SAMPLE @end
@implementation Hello (Sample)
・・・
@end
FIXCATEGORYBUG_HELLO_SAMPLE
の部分は他と被らない名前をつけておきます。${SRCROOT}/../path/to/header
-ObjC
ダイクストラ法 (Dijkstra's Algorithm) は最短経路問題を効率的に解くグラフ理論におけるアルゴリズムです。スタートノードからゴールノードまでの最短距離とその経路を求めることができます。
object Dijkstra extends Application {
case class Node(id: Int)
val graph = Map(
Node(0) -> Map(Node(1) -> 5, Node(2) -> 4, Node(3) -> 2),
Node(1) -> Map(Node(0) -> 5, Node(2) -> 2, Node(5) -> 6),
Node(2) -> Map(Node(0) -> 4, Node(1) -> 2, Node(3) -> 3, Node(4) -> 2),
Node(3) -> Map(Node(0) -> 2, Node(2) -> 3, Node(4) -> 6),
Node(4) -> Map(Node(2) -> 2, Node(3) -> 6, Node(5) -> 4),
Node(5) -> Map(Node(1) -> 6, Node(4) -> 4)
)
def dijkstra(routes: Map[List[Node], Int]) : (Map[List[Node], Int]) = {
val scanned = routes.flatMap {
case (route, cost) => {
graph(route(0)).flatMap {
case (n, c) if routes.forall(_._1(0) != n) => Some((n :: route) -> (cost + c))
case _ => None
}
}
}
if(scanned.isEmpty) {
routes
}
else {
dijkstra(routes + scanned.reduceLeft { (a, b) => if(a._2 < b._2) a else b })
}
}
println(dijkstra(Map(List(Node(0)) -> 0)))
}
Map(
List(Node(0)) -> 0,
List(Node(5), Node(4), Node(2), Node(0)) -> 10,
List(Node(3), Node(0)) -> 2,
List(Node(4), Node(2), Node(0)) -> 6,
List(Node(1), Node(0)) -> 5,
List(Node(2), Node(0)) -> 4
)
$ git clone https://github.com/ueshin/hbase-twitter.git
$ cd hbase-twitter
$ git checkout hbase-twitter-0.0.2
package object
package st.happy_camper.hbase.twitter
package subpackage
implicit conversion
Bytes.toBytes()
というメソッド呼び出しを繰り返し記述する必要があります。implicit conversion
すると便利です。new HTable()
の第2引数や、 37行目 new Scan().addColumn()
の各引数などは全てバイト配列なのですが、implicit conversion
のおかげですっきりと記述出来ています。new Scan().addColumn(Bytes.toBytes("score_" + lang), Bytes.toBytes(TagScoring.dateFormat.format(target)))
new Scan().addColumn("score_" + lang, TagScoring.dateFormat.format(target))
implicit conversion
とかただのメソッド呼び出しがそれっぽく見えるように記述できるってことなんですが。close()
を呼び出す必要があるものを、自動的にclose()
してくれる構文です。EnsureClose
クラスとimplicit
なensureClose
メソッドにより、close()
メソッドを持つオブジェクトを、処理を記述したクロージャを引数にしてopen()
すると、処理した後にclose()
してくれます。new HTable(conf, "tagtrend").open {
case tagtrend: HTable => ...
}
tagtrend.getScanner(new Scan() ... ).open {
case scanner: ResultScanner => ...
}
try { ... } finally { close() }
で記述するとものすごく冗長に見えます。case
と型を記述しなくてもいいようになる方法を思いつきましたが、それはまたの機会に。unapply
メソッドapply
メソッドもかなり便利ですが、Scalaの強力なパターンマッチで暗黙的に利用されるメソッドであるunapply
メソッドを利用することで便利なことが多いです。Node
オブジェクトからOption[Status]
オブジェクトを返すメソッドです。Status
オブジェクトが代入されます(HTableHandler.scala 19行目)。XML.loadString(xml) match {
case Status(status) => ...
case Delete(delete) => ...
case _ => ...
}
Option[Score]
オブジェクトを返すメソッドですが、このunapply
メソッドは変数の代入時にも使えるので、例えば(TweetHotTag.scala 42行目)val ScoreWritable(score) = result.value
score
変数にScore
オブジェクトをさくっと代入できてしまいます。unapply
メソッドがNone
を返したら例外飛ぶので注意。classOf[HRegionPartitioner[ImmutableBytesWritable, Put]]
HRegionPartitioner.class
だけです。type Context = Mapper[ImmutableBytesWritable, Result, Text, LongWritable]#Context
import
されるクラス名をScala側のクラスが上書きしてしまうとコンパイルエラーが分かりにくくなります。override def reduce(key: Text, values: java.lang.Iterable[LongWritable], context: Context)
Iterable[LongWritable]
だけで指定するとコンパイルエラーになります。Integer.parseInt("1")
はいけるけど Long.parseLong("1")
はいけないとか。java.lang
パッケージはデフォルトではimport
しなくてもよかったんじゃなかろうか(そしたらJava側のクラスを使うときには常に気を使うようになる気がする)。String
みたいにできなかったのかなとか)。$ git clone https://github.com/ueshin/hbase-twitter.git
$ cd hbase-twitter
$ git checkout hbase-twitter-0.0.2
tagtrend
テーブルに格納します。TagScoring
"yyyyMMddHH"
で、指定しなかった場合のデフォルトは起動時刻の0分となります。TagScoringMapper
map
処理では、各時間枠ごとのツイート数をカウントして(35行目)、減衰係数をかけながら加算していき(41行目)、最後にペナルティを掛けます(45行目)、と。languages
テーブルは、基準となる時間に何回ツイートがあったがを保存しつつ、集計が終わったことを表します。Tweet〜〜
になっているのは(中略)OAuthに移行した$ sh target/appassembler/bin/scoring-tag ja
$ sh target/appassembler/bin/tweet-hottag ja
#aclive 6.693075460306 : count = Map(0 -> 8, 1 -> 4, 2 -> 13, 3 -> 9, 4 -> 15, 5 -> 1)
#gmentalk 5.316292117147331 : count = Map(0 -> 6, 1 -> 4, 3 -> 1)
#boostjp 4.886848175090427 : count = Map(0 -> 6, 2 -> 1)
#lotrsee 3.0 : count = Map(0 -> 3)
#olojp 3.0 : count = Map(0 -> 3)
・・・
$ git clone https://github.com/ueshin/hbase-twitter.git
$ cd hbase-twitter
$ git checkout hbase-twitter-0.0.2
create 'tagtrend',
{ NAME => 'timeline_en', VERSIONS => java.lang.Integer::MAX_VALUE }, { NAME => 'score_en' },
{ NAME => 'timeline_ja', VERSIONS => java.lang.Integer::MAX_VALUE }, { NAME => 'score_ja' },
{ NAME => 'timeline_es', VERSIONS => java.lang.Integer::MAX_VALUE }, { NAME => 'score_es' },
{ NAME => 'timeline_de', VERSIONS => java.lang.Integer::MAX_VALUE }, { NAME => 'score_de' },
{ NAME => 'timeline_fr', VERSIONS => java.lang.Integer::MAX_VALUE }, { NAME => 'score_fr' },
{ NAME => 'timeline_it', VERSIONS => java.lang.Integer::MAX_VALUE }, { NAME => 'score_it' }
TagTransposer
configuration
テーブルを作成してあります。HRegionPartitioner
を設定しています(44行目)。TagTransposeMapper
qualifier
は、ユーザーID(16進数16桁)、値はステータスID(16進数16桁)とします。HRegionPartitioner
によってそれぞれの出力先リージョンを処理するReducerへとデータが渡されるようになっています。HRegionPartitioner
を利用したほうがいいと思います。IdentityTableReducer
ですので、Mapperからの出力がそのままジョブの出力となります。$ sh target/appassembler/bin/transpose-tag
hbase(main):001:0> scan 'tagtrend', { COLUMNS => [ 'timeline_ja' ] }
ROW COLUMN+CELL
#000 column=timeline_ja:00000000064c8bf3, timestamp=1283642556685, value=000000055ba92
88c
#000 column=timeline_ja:000000000772b7bf, timestamp=1280923632610, value=00000004b9f07
6d0
#000037 column=timeline_ja:00000000094a6d5a, timestamp=1283559227519, value=0000000556dca
9a0
・・・
"00000000064c8bf3"
、ステータスIDが "000000055ba9288c"
ということになります。hbase(main):002:0> get 'twitter', '00000000064c8bf3'
COLUMN CELL
status:000000055ba9288c timestamp=1283642530795, value=\x8A\x01*\xDF\x0E\xFE`\x8B\x05[\xA9(\x8C2\xE3\x83\
x8F\xE3\x83\x83\xE3\x83\x8F\xE3\x83\x83\xE3\x83\x8F\xEE\x9B\xB6\xE2\x80\xA6\xE3\x
81\xAA\xE3\x82\x93\xE3\x81\xA0\xE3\x81\x93\xE3\x81\xAE\xE5\xA4\x89\xE8\xBA\xAB\xE
9\x9F\xB3 #000:<a href="http://yubitter.com/" rel="nofollow">yubitter</a>\x00\x00
・・・
"000000055ba9288c"
のところに確かに #000 というハッシュタグが見えていますね。qualifier
に続くもう一つの次元のような扱いをしていますが、このようなやり方には注意が必要です。user:lang
)で集計status#source
)で集計status#text
から抽出)で集計$ git clone https://github.com/ueshin/hbase-twitter.git
$ cd hbase-twitter
$ git checkout hbase-twitter-0.0.1
user:lang
)で集計まずはシンプルな方から。user:lang
)で集計します。Scan
で絞り込みTableMapReduceUtil#initTableMapperJob
(41行目) で、MapRecuceの対象となるテーブル(第1引数)と、データ範囲(第2引数のScan
オブジェクト)を指定します。LangCountMapper
user:lang
カラムのデータをキー、1L
を値として出力します (30行目)。org.apache.hadoop.mapreduce.lib.reduce.LongSumReducer
がHadoopに標準で入っていますので、普通はこちらを使うとよいと思います。$ mvn clean package
$ sh target/appassembler/bin/count-lang <output-dir>
$ cat <output-dir>/part-r-00000
de 25129
en 2835610
es 303250
fr 21047
it 11486
ja 664244
status#source
)で集計status#source
)で集計します。Status
オブジェクトのリストを取り出す必要があります。Scan
設定status
カラムファミリーに含まれるカラムを全て取得します (44行目)。status
カラムファミリーに対するqualifier
が、statusId
(の16進16桁表記)を表していましたので、status
カラムファミリー全体がそのユーザーのツイート群になります。Result
オブジェクトからStatus
オブジェクトを復元して (31行目)、status.source
をキーとして出力しています (32行目)。Status
オブジェクトの復元はScalaのパターンマッチを利用しています (StatusWritable#unapply)。$ sh target/appassembler/bin/count-source <output-dir>
status#text
から抽出)で集計Scan
設定は先程と同様です (48行目)。Status
オブジェクトの復元も先程と同様ですが、今度は status.user.lang == "ja"
という条件を付けて、日本語設定したユーザーのみを集計しています (33行目)。$ sh target/appassembler/bin/count-tag <output-dir>
Scan
の設定によって変わります。Scan
では、行キーの範囲を指定したり、バージョン(タイムスタンプ)の範囲を指定したりすることができます。HDFS NameNode
が 9000番ポート、 JobTracker
が 9001番ポートで動作している想定です。src/pseudo/resources
以下の設定ファイルを修正してください。$ mvn -P pseudo clean package
HDFS NameNode
が node0
サーバーの 9000番ポート、 JobTracker
が node0
サーバーの 9001番ポート、 ZooKeeper Quorum
が node0
サーバーで動作している想定です。src/production/resources
以下の設定ファイルを修正してください。$ mvn -P production clean package