2012-01-14

Play framework でトランザクションを複数並列に扱うには?

Play framework を扱う案件に最近携わっていて、いろいろとドキュメントにないノウハウが溜まりつつあるのでここらでメモ書きしておきます。


Play framework とトランザクション

Play framework では、HTTP リクエストはもちろんのこと、非同期ジョブにおいてもトランザクションが自動的に提供されます。通常の業務であれば、1つの HTTP リクエスト処理・非同期ジョブに1つのトランザクションがあればそれで十分ですが、ちょっと込み入ったことをやろうとすると、例えば

  • 監査用の操作ログ・クエリログをテーブルに出力したい
    →コミットされるタイミングは後でもいいんだけど、本流の処理で転けてロールバックかけた場合に、操作ログまでなかったことになるのは避けたい。
  • 非同期ジョブなどで、ジョブ管理テーブルを随時監視・更新したい
    →本流の処理のトランザクションはコミットしたくないけど、ジョブ管理テーブルへの変更は即座にコミットして反映させたい。
など、1つのトランザクションではちょっと実現が難しいこともあったりします。前者の例は、トランザクションを二つに分けたとして互いが重ならないように工夫できるでしょうが、後者はそう簡単にはいきません。互いのトランザクションが一部重なり、同時並列で存在しなければならない状況が発生し得ます。

ところで Play framework 的には、「一連の処理で複数のトランザクションを扱う」ことは「レールから外れた」行為である可能性が高く、このような行為を標準でサポートしているわけでもなく、当然ながら (私の知る限り) ドキュメント・リファレンスの類いに書かれているわけでもありません。


Play のエンティティと EntityManager

Play 標準では複数トランザクションを明示的にはサポートしていませんが、複数トランザクションを扱えないと困ることがあるのは事実なので、何とかしたいところです。何とかするには、まず Play がどのようにトランザクションを提供してくれているかを把握する必要があります。そこで、Play の実装、特にトランザクション周りをソースコードリーディングしていくことにします。

初めは、Play における永続化、特に トランザクションと関わりの深い EntityManager 周りの扱いを確認してみましょう。

EntityManager は、Play では play.db.jpa.JPA クラスフィールド entityManager で管理されています。JPA クラスのオブジェクト自体は、ThreadLocalクラスフィールド JPA.local で管理されています。ThreadLocal オブジェクトで管理されていることから、JPA オブジェクトそのもの、またそのフィールドにある EntityManager オブジェクトは、スレッド毎に用意されていることがわかります。

Play のエンティティは、この JPA.local クラスフィールドで管理されている、スレッド毎に用意された EntityManager を利用して永続化やクエリを実現しています。例えば、Model#save() メソッド から呼び出される play.db.jpa.JPABase クラス_save() メソッド を見てみましょう。JPABase.em() メソッドから始まり、呼び出し先を追っていくと JPA.em() → JPA.get().entityManager → JPA.local.get().entityManager と、ThreadLocal オブジェクトで管理されている JPA オブジェクトのフィールドにたどり着くことができます。Model#find() メソッド も同様で、その内部ではやはり JPA.local クラスフィールドで管理されている EntityManager を利用します。


トランザクションの開始・終了

トランザクションと関わりの深い EntityManager が、JPA.local にスレッド別になるよう管理されていることがわかりました。次に、トランザクションの開始と終了について確認してみましょう。

play.db.jpa.JPAPlugin クラス を見てみます。このクラスの startTx() クラスメソッド と、closeTx() クラスメソッド がそれぞれトランザクションの開始・終了処理を担当します。JPAPlugin.startTx() クラスメソッドの実装を見ると、その最後で JPA.createContext() クラスメソッド を呼び出しています。この JPA.createContext() クラスメソッドの実装を見て明らかなように、

  1. 既存の JPA オブジェクト、EntityManager オブジェクト (≒トランザクション) が存在する場合は、その EntityManager オブジェクトをクローズ(≠コミット)し、
  2. 新たな JPA オブジェクトを生成して、JPA.local クラスフィールドの ThreadLocal オブジェクトに設定する
という処理が、トランザクション開始時に行われます。つまり「新しいトランザクションを開始しようとすると、既存のトランザクションは破棄された上で新しいトランザクションが始まる」ということになります。

なお、JPAPlugin.startTx()/closeTx() 両クラスメソッドは、それぞれ HTTP リクエストに対応するコントローラ・アクションメソッドの呼び出し前後、非同期ジョブの実行前後に呼び出され、標準のトランザクションを構成します。


複数トランザクションを並列して扱う方法

上記のように、JPAPlugin.startTx() クラスメソッドを呼び出すことで新しいトランザクションの開始はできますが、古い・既存のトランザクションは破棄されて利用できなくなってしまいます。このままでは、複数のトランザクションを並列して扱うことはできません。それではどのようにして複数トランザクションを実現すればよいのでしょうか?

実は結構簡単で、新しいトランザクションを開始する前に、既存のトランザクション(JPA オブジェクト)を JPA.local より待避し、空っぽの状態にしてしまえばいいのです。

コードで表すと、以下のようになります

もとのトランザクションに復帰させたいときは、待避しておいた JPA オブジェクトを JPA.local に設定する(ThreadLocal#set())だけです。

実装がやや煩雑になってしまうのが難点ですが、待避した JPA オブジェクトを Stack で管理したり、または Map で管理するなどして、複数トランザクション管理機能をクラス化してしまえば多少は扱いやすくなるのではないでしょうか。


注意点

以上が Play で複数並列にトランザクションを扱う方法になりますが、1つ、注意しなければならないことがあります。

先に述べたとおり、JPAPlugin.startTx()/closeTx() クラスメソッドは HTTP リクエスト処理のアクションの前後に (暗黙的に) 呼び出されます。特に JPAPlugin.closeTx() については、アクションのメソッド内で例外が発生したとしても必ず呼ばれる実装になっています(例外が発生したら closeTx(true) でロールバック、例外発生がなければ closeTx(false) でコミットするように呼び出されます)。

しかし、この暗黙的な JPAPlugin.closeTx() 呼び出しでクローズされるのは、JPA.local に設定されている JPA オブジェクトのトランザクションだけであり、たとえば上記コードの 8 行目で待避した JPA オブジェクトについては、25 行目で再び JPA.local に戻されるまでの間に例外が発生すると、クローズされずに放置プレイ状態になってしまいます。

そのため、JPA.local から JPA オブジェクトを待避させる場合は例外発生に注意し、たとえ例外が発生したとしても、待避された JPA オブジェクトのトランザクションを確実に・明示的にクローズする実装が必要になるでしょう。

16:22No comments

2011-09-12

GDD2011 DevQuiz のスライドパズルに挑戦してみました

Google Developer Day 2011 に参加するために、DevQuiz に初めて挑戦してみました。ウォームアップクイズ、分野別クイズはさておき、一番やりがいのあった「スライドパズル」について、自分の戦略などを書き残しておこうと思います。

ソルバーのソースコードは、http://code.google.com/p/k11i-gdd2011jp-slidepuzzle-solver/ にて公開しています。


言語

  • 回答期限までそこそこ時間があった
    →コーディングに多少時間がかかっても OK。
  • 計算・空間効率を追求する必要がありそう
    →言語自体の実行性能が高いこと。またある程度、内部の挙動を把握できる、かつ効率化のポイントを把握していること。
などの理由により、開発言語は Java を選びました。


ソルバーの戦略

ソルバーのプログラムは「ただひたすらにパズルを解く」ステージと、「利用可能な L/R/U/D の上限値をもとに、前ステージで得られた答えを取捨選択する」ステージに分けました。


パズルを解く戦略

「ただひたすらにパズルを解く」ステージは、当初から探索ベースの手法を前提としていろいろと試行錯誤した結果、「A* アルゴリズム」と「双方向探索」を組み合わせた実装となりました。この二つのアルゴリズムの組み合わせは、M.Hiroi さんの Algorithms with Python / ヒューリスティック探索 のページ内容をかなり参考にさせてもらっています。

この「A* + 双方向」の組み合わせでは最適解(最短手数)を求めるのが難しいのですが、それぞれを単体で利用する場合よりも圧倒的に速く問題を解くことができる(たいていの問題が 1 秒以下で計算可能)特徴があります。

A* アルゴリズムに限りませんが、幅優先的な探索アルゴリズムはメモリを富豪的に使うので、何も制御せずに問題を解かせると OutOfMemoryError をスローしてしまう可能性があります。今回のソルバーでは、展開される状態数に上限値を設けて、その上限値に達したところで解の計算を諦める制御ロジックを組み込んでいます。

A* アルゴリズムのヒューリスティック関数には、「マンハッタン距離」を用いています。ただ、壁の存在があるため、実際には単純なマンハッタン距離ではなく、壁を回避してたどり着く最短経路での手数を距離としています。なので、壁が一つもなければ、常にマンハッタン距離と等しくなります。


回答を選ぶ戦略

算出された各問題の答えの集合から実際に提出する回答を選ぶ処理は、貪欲なロジックで実装しています。つまり、既定の L/R/U/D 利用上限値をすべて下回るまで、L/R/U/D それぞれの利用が多い答えを捨てる仕組みとなっています。しかしながら、今回のソルバー/実行環境では、この機能が実行されることはありませんでした…


処理効率の最適化

上記の戦略に基づくソルバーをそこそこの性能の PC で実行して、40 点前後のスコアを 1~2 時間で獲得することができました。もう少し高いスコアを目指すために、これより先はひたすらアルゴリズム以外の最適化に手を尽くしました。


状態保存の空間効率向上

当初のソルバーでは、盤面上のパネルの配置状態を 1 つのパネルにつき 1 バイト消費するようにしていました。しかし実際には、最大の 6x6 パズルにおいて 1 パネルの配置状態を 6 ビットで表すことができます。このため、4 バイト(32 ビット)で 5 つのパネルを表現するように実装を改めました。これにより、6x6 = 36 バイト必要だった盤面の状態表現を、約 4/5 の 32 バイトで表現できるようになりました。

また盤面のパネル配置状態のみならず、付随的な複数の情報(双方向探索の探索向きや、手数、距離コスト)も、(C でいうところのビットフィールド的なアプローチで)1 つの 32 ビット数値で効率よく表現するようにしました。


ハッシュ値の計算効率化

パネルの配置状態をハッシュ表(HashMap)に記録して探索の重複チェックを実現していたので、上記の 4 バイトで 5 パネルを表すようにする改修に合わせて、ハッシュ値の計算を 1 パネルずつではなく 5 パネルまとめて計算するようにしています。また、一度計算したハッシュ値はキャッシュすることで、ハッシュ値の計算コストを低く抑えるようにしています。


ライフサイクルの短いオブジェクトを再利用する

オブジェクトの生成は Java にとってそれなりにコストのかかる処理なので、いったん生成してすぐに不要になってしまったオブジェクトを再利用する(フィールドだけごそっと入れ替える)仕組みを組み入れました。もともと immutable に実装したクラスを無理矢理こじ開けて mutable にしたりと、あまり気持ちのいいものではないですが。


ハッシュ表を自作する

Java クラスライブラリの HashMap は汎用的に作られているため、今回のソルバーとしては余計な機能が付いていたり 1 エントリあたりのヒープ消費に Map.Entry が加算されてあまりうれしくないので、HashMap を簡略化したものを開番地法で実装しました。これにより計算効率と若干の空間効率の向上が達成できます。


古臭い・妖しいコーディング

「for (int i = 0;...) が許されるのは JDK 1.4 までだよねー」とか言われかねなけど、for-each 文を使わなかったり、オブジェクトの等価判定に == 演算子を使ったり、メソッドのインライン展開を狙って final 修飾子をつけてみたりしました。効果のほどは定かではないですが。


JVM を 64 ビット化

32 ビットの JVM では、利用できるメモリ空間が小さいため、Java の環境を 64 ビットの JDK に差し替えました。これにより 8 GB の物理メモリ+ページファイルのサイズ分、ヒープを確保することができるようになりました。また、計算効率も 32 ビット版に比べて心なしかよくなったように思います。


そのほか試してみたこと


ヒューリスティック関数の(デ)チューニング

A* のヒューリスティック関数を前述のマンハッタン距離ベースのものから、パネルの位置によって重みを加えてみたものに変えてみたり、距離を +1 してみたりと、試行錯誤してみました。特に距離 + 1 のヒューリスティック関数は、通常のマンハッタン距離のものよりも早く答えを見つけ出せる(代わりに手数は多くなる…)など面白い結果が確認できたのですが、処理時間が遅くなるなどの理由で結局採用しませんでした。


Simplified Memory-bounded A* (SMA*) への差し替え

ヒープの大量消費がソルバーの悩みの種だったので、コアとなる探索アルゴリズムを SMA* に差し替えようと検討してみました。しかし、答えを出力するための履歴をどのように保持するべきかが把握できなかったのと時間的制約により、あえなく断念しました。


Java VM のパラメータチューニング

JVM を 64 ビット化してヒープをよりたくさん利用できるようになったものの、オブジェクトポインタも 4 バイト→ 8 バイトになっているようで、オブジェクトを大量に生成した場合にかなりの勢いでヒープを大量消費してしまう様子がうかがえました。JDK 6 のいつかのバージョンからか、オブジェクトポインタを圧縮できる仕組みが備わったようで、それを有効化(-XX:+UseCompressedOops)してみましたが、結果は変わらず。仕方がないので、ページファイルを利用してヒープ領域の拡大する方向に突き進みました。


やり残したこと


マルチコア化

ソルバーの調整については終始、計算効率を気にしながら行っていましたが、その割にマルチコア CPU を一切有効活用せず、シングルコアでひたすら頑張る構成にしてしまったのがちょっと残念でした。すべてのコアを活用できれば、2~3 割は速くなったはず…


人間的なアルゴリズムで解く

メモリの大量消費するような問題を解く場合、人間的なアプローチで問題を完全に解く、あるいは上・左半分など途中まで解く(以降は A* + 双方向で計算する)アルゴリズムを組み込めばよいのでは? と考えていたのですが、「壁」の存在によりこのアルゴリズムを一般化することができませんでした。


最終結果・感想

最終的に物理メモリ+ページファイルで 20G のヒープを構成して実行し、4832 問を 3 時間ちょっとで解けるまでになりました。メモリさえあれば、5000 問全問解くのも難しくはなさそうです。メモリさえあれば…。でも、この結果には満足しています。A* アリゴリズムとか、大学の講義で聞いただけで業務で使うことなんて今まで一度もありませんでしたが、実際に触れてみるとなかなか楽しいものです。機会があれば業務でも使ってみようかな? ないと思うけど。

12:29No comments

2011-06-07

Amazon の Item-to-Item CF の論文を読んだメモ

今となってはかなり古い部類に入る論文ですが、レコメンデーションの基礎を改めて勉強し直すために読んだので、そのメモを残しておきます。1

Amazon.com Recommendations: Item-to-Item Collaborative Filtering


この論文が議論の対象としているのは、(Amazon.com のような)数千万ユーザ・数百万点の商品(アイテム)という大規模なデータセットに対し、リアルタイム(遅くとも 500ms 以下)、オンライン 2 で、かつ高品質な推薦を行うという問題です。Amazon はこの問題をアイテムベースの協調フィルタリングで解決しています。

ユーザベースの協調フィルタリング


協調フィルタリングの詳細については こちらの資料 に譲るとして、端的にいうと「協調フィルタリング」というのは、アクティブユーザ(推薦対象のユーザ)に類似する他のユーザを、商品購入履歴や評点情報の近さから探し出し、そのユーザが購入している商品をアクティブユーザに推薦する、というものです。

古典的な GroupLens のユーザベース協調フィルタリングでは、時間計算量の制約から、オンラインでの推薦を提供できるのは中規模(数十万ユーザ、数万点の商品程度?)のデータセットまでとなっているようです。このユーザベースの協調フィルタリングの時間計算量は、ユーザ数を m、商品数を n とすると、最悪のケースで O(mn) となります。ただし、実際のデータセットにおいては、ユーザと商品の関係が疎であることが多いので、その場合には O(m + n) の時間計算量に近くなるそうです。どちらにせよ、Amazon ほどの大規模なデータセットにおいては、O(m + n) の時間計算量でさえもオンライン・リアルタイムでの推薦を難しくする要因となり得ます。

このユーザベースの協調フィルタリングを大規模データセットに適用する場合は、時間計算量を小さくする工夫が必要になります。この論文では以下のように、データの規模を減らすことで時間計算量を小さくする手法が挙げられています。
  • ユーザをランダムサンプリングして、全体のユーザ数を減らす
  • 購入量の少ないユーザを省く(類似度計算結果の精度が低く、推薦にあまり役立たないため)
  • とてもよく購入されている/ほとんど購入されていないアイテムを省く
ほかにも、商品の集合をそのカテゴリごとに分類し、その分類された商品の集合内で推薦処理を行う方法も挙げられています。しかしながら、いずれの手法を採用するにしても、推薦の品質・精度が低下してしまう問題がつきまとってしまいます。

クラスタモデルの手法


ユーザベースの協調フィルタリングでは、リクエストの都度、ユーザ間の類似度を計算して、アクティブユーザに類似するユーザを探していました。一方でクラスタモデルでは、ユーザ集合を類似するユーザ同士であらかじめクラスタリングしておきます。このクラスタリング処理は、推薦処理とは切り離してオフラインで計算することができます。推薦処理では、このユーザのクラスタを利用し、アクティブユーザがどのクラスタに属するかを判定したのちに、そのクラスタに属する他のユーザの情報を利用して推薦が行われます。

このクラスタモデル、計算時間の観点では優れた手法ではありますが、推薦品質については決して高くはないようです。推薦品質を向上させるためにはクラスタの数を増やす必要があり、クラスタ数を増やしてしまうと、オンラインでの計算量が増えてしまう、という問題を抱えています。

Amazon.com のアイテムベース (Item-to-Item) の協調フィルタリング 3


以上のように、大規模なデータセットに対して高品質・高精度で、かつパフォーマンスのよい推薦を実現するのは難しい問題と言えます。Amazon はこの問題に対し、アイテムベースの協調フィルタリングを利用することで解決を図っています。

Amazon のアイテムベース協調フィルタリングの一番の特徴は「アイテム間の類似度行列をオフラインで計算して、推薦に備えておく」ことです。アイテム間の類似度行列の計算は、最悪のケースで O(n^2 * m)、疎なデータセットでも O(nm) の時間計算量となりますが、オフラインで計算するのであればこの計算量は許容できる範囲になるのではないでしょうか。また、よく購入されている商品を購入したユーザをサンプリングしてアイテム間の類似度行列を計算することで、品質・精度の低下を小さくしつつ、時間計算量を削減できるようです。

推薦処理では、このアイテム間の類似度行列を利用します。その推薦処理の時間計算量は、ユーザ数や商品総数に依存せず、アクティブユーザの購入商品数、評点情報数にのみ依存します。そのため、大規模データセットであっても、オンラインでの推薦処理で充分な性能が出せるようになる、とのことです。

また、このアイテムベースの協調フィルタリングは、アクティブユーザの購入済みの商品が 2,3 個程度と僅かな状況であっても、質の良い推薦が実現できます(ユーザーベースの協調フィルタリングでは、類似するユーザーを探すためにそれなりの数の購入商品数を必要とするので、2,3 個程度の商品数では十分な推薦が実現できません)。

まとめ


大規模データセットに対して推薦をする場合、高いパフォーマンスと推薦品質・精度の両立は簡単ではありません。Amazon.com は、アイテムベースの協調フィルタリングを採用することで推薦品質・精度の問題を解決し、またその協調フィルタリングで利用するアイテム間類似度行列をオフライン計算することでパフォーマンスの問題を解決しています。

感想など


普段扱っているデータの規模が大きくても数十万ユーザ、数万アイテム数だというのに、この程度でヒイヒイ言っている自分が情けなくなりますね… 規模の大きいデータにもっと慣れねば!


1:個人的な解釈が多分に含まれているので、この記事を参考にすることがもしあれば、一度原文と照らし合わせてみることをお勧めします。
2:推薦すべき商品を事前に計算して準備しておくのではなく、リクエストが来たところで初めて推薦する商品を算出する方式。論文の記述から察するに、ショッピングカート内の商品から推薦する場合などにも利用されているらしい。
3:アクティブユーザに類似するユーザを探すのではなく、あるアイテムに類似する別のアイテムを探すタイプの協調フィルタリングを、ここでは「アイテムベースの協調フィルタリング」と呼んでいます。
2:222 comments

2011-02-19

Trie が提供すべき操作について検討してみる

nokuno さんの記事「オープンソースのTrieライブラリまとめ」あたりを参考に、まず既存の Trie ライブラリ群がどのような API を提供しているのかを調査し、その上でこれから Trie を実装する場合に、どのような操作・API を提供すべきかを検討した結果のメモです。

記事にあるすべての Trie 実装を確認するのはしんどいので、代表的な感じのものを適当にピックアップしてみました。

  • Tx ... 岡野原さんによる簡潔データ構造を利用した Trie の実装。
    • prefixSearch() ... prefix search。Trie に格納されている文字列の中で、対象文字列の接頭辞としてもっとも長く一致する文字列を探し出す。common prefix search で列挙される文字列のうち、長さが最長のものと同じ。
    • commonPrefixSearch() ... common prefix search。Trie に格納されている文字列より、対象文字列の接頭辞にあたる文字列をすべて列挙する。結果は一括して求められる。
    • predictiveSearch() ... predictive search。Trie に格納されている文字列より、対象文字列が接頭辞となる文字列をすべて列挙する。結果は一括して求められる。
    • expandSearch() ... common prefix search + predictive search。Trie に格納されている文字列より、対象文字列を接頭辞とする文字列、または対象文字列の接頭辞にあたる文字列を列挙する。結果は一括して求められる。
  • UX ... Tx と同じく岡野原さんによるプロダクト。さらに簡潔。だいたい Tx と同じ API 構成。この命名は、"Tx" の1つ先(t -> u)を行く、という意味が込められているのかな?
    • prefixSearch() ... prefix search。
    • commonPrefixSearch() ... common prefix search。
    • predictiveSearch() ... predictive search。
  • sumire-tries ... やたさんによる、簡潔データ構造を含む各種 Trie の実装。Trie 木構造のノードレベルでの走査(traversal)ができるのが特徴。
    • TrieBase#find() ... exact match。Trie に格納されている文字列より、対象文字列に完全一致する文字列を探し出す。
    • TrieBase#follow() ... Trie・ノードの走査。指定されたノードから始まり、対象文字列に合致する Trie 上の経路(ノード)を辿る。
    • TrieBase#find_child() ... Trie・ノードの走査。あるノードより指定された文字を辿った先にある子ノードを求める。
    • TrieBase#child() ... Trie・ノードの走査。指定のノードの先にある子ノードのうち、最初の子ノードを求める。
    • TrieBase#sibling() ... Trie・ノードの走査。指定のノードの、次の兄弟ノードを求める。
    • class CompleterBase ... predictive search。Iterator パターンで、文字列を順次列挙する。
  • marisa-trie ... やたさんによる、簡潔データ構造を利用した Trie の実装。UX の手法をさらに発展させたものっぽい。
    • lookup() ... exact match。sumire-tries では find() でしたが、marisa-trie では同名メソッドは別の機能を提供しているようです。
    • find() ... common prefix search。一括して列挙する。
    • find_first()/find_last() ... prefix search。
    • find_callback() ... common prefix search。1つずつ、コールバック関数を呼び出して列挙する。
    • predict() ... predictive search。一括して列挙する。
    • predict_callback() ... predictive search。1つずつ、コールバック関数を呼び出して列挙する。
  • Darts ... MeCab 開発者の工藤さんによる Double Array Trie の実装。MeCab でも利用されています。
    • exactMatchSearch() ... exact match。
    • commonPrefixSearch() ... common prefix search。一括して列挙する。
  • Doar ... Igo/Gomoku の開発者 sile さんによる Double Array Trie の C++ 実装。
    • Searcher#search() ... exact match かな?
    • Searcher#common_prefix_search() ... common prefix search。1つずつ、コールバック関数を呼び出して列挙する。
    • Searcher#children() ... Trie・ノードの走査。子ノードを1つずつ、コールバック関数を呼び出して列挙する。
  • jada ... sile さんによる Double Array Trie の Java 実装。
    • Trie#search() ... exact match。
    • Trie#commonPrefixSearch() ... common prefix search。同メソッドを繰り返し呼び出して、1つずつ列挙する。

こんなところでしょうか。以上より「Trie を使った操作」は、
  • exact match
  • common prefix search
  • predictive search
  • prefix search
の4つが用意できるとよさそうです。少なくとも、上から3つは必須となりそうですね。

「Trie の走査を実現する走査」としては、ピックアップした実装ではあまり提供されていませんでしたが、
  • あるノードの直下の子ノードを列挙する
操作は必須となりそうです。

この結果を参考に、汎用的に使えそうな Trie の Java 実装について検討を進めてみます。
1:59No comments

2010-12-11

[メモ]MinGW/MSYS 環境で Kyoto Cabinet Core & Java バインディングをビルドする

本家から Windows 版がでた今となっては全く不要な記事となりましたが、せっかく書いたので残しておきます。

対象バージョン


1. 準備

Kyoto Cabinet のビルドを始める前に、依存ライブラリ regex と zlib をビルド&インストールする必要があります。

1.1 regex のビルド・インストール

上記 URL からダウンロードした regex の tarball を展開し、展開先ディレクトリにて
$ ./configure
$ make
 を実行します。make が終わったら、スタティックな libregex.a を作るため、
$ rm libregex.a
$ ar rcs libregex.a regex.o
 とします。その後、
$ make install
を実行し、regex のビルド・インストールは完了です。

1.2 zlib のビルド・インストール

zlib の tarball を展開した先のディレクトリにて、
$ make -f win32/Makefile.gcc
とし、Windows/gcc 環境用の Makefile を用いて make します。インストールの際は以下のように、事前に INCLUDE_PATH、LIBRARY_PATH の環境変数を定義する必要があります。
$ export INCLUDE_PATH=/usr/local/include
$ export LIBRARY_PATH=/usr/local/lib
$ make -f win32/Makefile.gcc install
以上で準備は完了です。

2. Kyoto Cabinet core library のビルド・インストール

g++ に渡すオプションを、以下のとおり定義します。
$ export CPPFLAGS=-D_WIN32_WINNT=0x0501
あとは、kyotocabinet の tarball 展開先ディレクトリにて、
$ ./configure
$ make
$ make check
$ make install
として、ビルド&インストールが完了します。

3. Kyoto Cabinet Java バインディングのビルド・インストール

3.1 準備

Kyoto Cabinet Java バインディングをビルドする前に、JDK をインストールした先のディレクトリの情報を MSYS の fstab ファイルに追記する必要があります。

fstab ファイルは、(MinGW のインストールルート)\msys\1.0\etc ディレクトリに配置されています。以下は、"C:\Program Files\Java" ディレクトリに JDK をインストールした場合の追記内容の例です。
C:\Progra~1\Java\jdk1.6.0_18 /java
上記のように、JDK をインストールした先のパスの途中に空白を含むディレクトリが存在する場合、チルダ ~ を使ってそのディレクトリ名を表記する必要があるようです(ダブルクォーテーションで括っても、MSYS は認識してくれません)。

3.2 ビルド&インストール

./configure を実行する前に、環境変数 JAVA_HOME を定義し (直し) ます。
$ export JAVA_HOME=/java
JAVA_HOME を定義したら kyotocabinet-java の tarball 展開先で
$ ./configure
とします。./configure が完了したら、出力された Makefile を一部修正します。以下のようにターゲット libjkyotocabinet.so.$(LIBVER).$(LIBREV).0 の内容に、jkyotocabinet.dll を生成するためのコマンドを追記します。
libjkyotocabinet.so.$(LIBVER).$(LIBREV).0 : $(LIBOBJFILES)
 $(CXX) $(CXXFLAGS) -shared -Wl,-soname,libjkyotocabinet.so.$(LIBVER) -o $@ \
   $(LIBOBJFILES) $(LDFLAGS) $(LIBS)
 ##### 以下の2行を追記する #####
 $(CXX) $(CXXFLAGS) -shared -Wl,--kill-at -Wl,-soname,jkyotocabinet.dll -o jkyotocabinet.dll \
   $(LIBOBJFILES) $(LDFLAGS) $(LIBS)
本来なら CPPFLAGS などの環境変数で解決したい内容なのですが、何故か kyotocabinet-java の ./configure では環境変数が反映されないようなので、やむを得ずこのような work around をしています。さて、Makefile の編集が終わったところで、
$ make
$ make check
とします。なお、make check は
make DBNAME="*" RNUM="10000" check-each
のテストの実行時にエラーになってしまうようです。この問題はまだ解消できていませんが、普通に利用する分には支障はないのかな、と思います。

後は、生成された jkyotocabinet.dll と kyotocabinet.jar を利用すれば、Windows 上でも Java から Kyoto Cabinet が使えるようになることでしょう(試していません…)。
2:26No comments

2010-11-21

「第8回 データマイニング+WEB 勉強会@東京」で発表してきました

11/14(日)に開催された「第8回 データマイニング+WEB 勉強会@東京」で、「協調フィルタリングにおける希薄問題の解決法 - Random walk」という題目で発表してきました。以下はその発表に使ったスライドです。


現在、本発表内容に関連した、もう一歩踏み込んで説明した補足資料を作成しています。11/23(火)までに作り終えられるといいなあ。

12/1 追記

補足資料を slideshare で公開しました。

23:34No comments