Cassandra: Cassandra のデータモデル

まずは、公式wikiで日本語訳があるので、そちらの紹介。

DataModel_JP – Cassandra Wiki

次にDiggのエンジニアの人のブログと、その翻訳をしてくれた人のブログの紹介。
WTF is a SuperColumn? An Intro to the Cassandra Data Model
Cassandraデータモデル入門 – Arin Sarkissian

最後にウノラボの記事。
今からはじめるCassandra入門

一番データモデルに関して詳しいのは、Arin Sarkissianさんの記事だね。blogを構築する場合のデータモデル設計例を出してくれているのが大きい。
公式のwikiでも理解出来ないことはないけれど、実例がないからね。

リレーショナルデータベースとの一番の違いは、Columnが固定長じゃない事かな。Key-Value-Storeだから、当然、キーとバリューの関係を次々増やしていける。で、Key-Value-Timestampが1つのColumnなので、自然な流れとして、Columnは増えていく。

更に、リレーショナルという関係がないので、一つのキーを元に複数のバリューを参照したい場合には、SuperColumnという概念が登場する。SuperColumn自体は、通常のColumnとは違い、ハッシュマップでしか無い。つまり、キーを元に複数のColumnを参照する事になる。そして、SuperColumnには、Timestampが存在しない。

そして、ColumnFamilyは、Column, SuperColumnをグルーピングする為の存在。単一のデータ群とみなせるなら、それはColumnFamilyとして纏まる。

最後に、Keyspaceは、データモデルとしては最上位の名前空間といったところかな。リレーショナルデータベースの、データベース名に当たる。

1つのキーで所得したいColumn数が後から増える場合にはこれは非常に便利だ。リレーショナルデータベースの場合は、Tableを追加して、リレーション関係を構築して行うか、ADD COLUMN を行う事になる。どちらにしても、再設計が必要になる。

重要なのが、Nameでソートされる事だろうか。これを利用して、サンプルではエントリーをソートしてる。

ちょっと、Pythonで書いてみた。

エントリー編

from thrift import Thrift
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol.TBinaryProtocol import TBinaryProtocolAccelerated
from cassandra import Cassandra
from cassandra.ttypes import *
import time
import pprint
import uuid

def main():
  socket = TSocket.TSocket('localhost', 9160)
  transport = TTransport.TBufferedTransport(socket)
  protocol = TBinaryProtocol.TBinaryProtocolAccelerated(transport)
  client = Cassandra.Client(protocol)
  pp = pprint.PrettyPrinter(indent=2)

  keyspace = 'BloggyApply'

  try:
    transport.open()

    column_path = ColumnPath(column_family='BlogEntries', column='title')
    key = '001'
    value = '001 title'
    timestamp = time.time()

    try:
      # Insert blog entry.
      client.insert(keyspace,
                    key,
                    column_path,
                    value,
                    timestamp,
                    ConsistencyLevel.ZERO)

    except Thrift.TException, tx:
      print "Thrift: %s" % tx.message

    column_path = ColumnPath(column_family='TaggedPosts', column=uuid.uuid1().bytes)
    key = '__notag__'
    value = '001'
    timestamp = time.time()

    try:
      #Insert the data into TaggedPosts
      client.insert(keyspace,
                    key,
                    column_path,
                    value,
                    timestamp,
                    ConsistencyLevel.ZERO)
      #Query for data
      column_parent = ColumnParent(column_family='TaggedPosts')
      slice_range = SliceRange(start='', finish='')
      predicate = SlicePredicate(slice_range=slice_range)
      result = client.get_slice(keyspace,
                                key,
                                column_parent,
                                predicate,
                                ConsistencyLevel.ONE)

      pp.pprint(result)

    except Thrift.TException, tx:
      print "Thrift: %s" % tx.message

  except Thrift.TException, tx:
    print "Thrift: %s" % tx.message
  finally:
    transport.close()

if __name__ == '__main__':
      main()

コメント編。

from thrift import Thrift
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol.TBinaryProtocol import TBinaryProtocolAccelerated
from cassandra import Cassandra
from cassandra.ttypes import *
import time
import pprint
import uuid

def main():
  socket = TSocket.TSocket('localhost', 9160)
  transport = TTransport.TBufferedTransport(socket)
  protocol = TBinaryProtocol.TBinaryProtocolAccelerated(transport)
  client = Cassandra.Client(protocol)
  pp = pprint.PrettyPrinter(indent=2)

  keyspace = 'BloggyApply'

  try:
    transport.open()

    super_column = uuid.uuid1().bytes
    values = dict()
    values['commenter'] = 'Hoge Mash'
    values['email'] = 'hoge@hoge.com'
    values['comment'] = 'my comment...'

    column_path = ColumnPath(column_family='Comments', super_column=uuid.uuid1().bytes)
    super_key = '002'
    timestamp = time.time()

    try:
      # Insert blog entry.
      for column in values:
        column_path.column = column
        client.insert(keyspace,
                      super_key,
                      column_path,
                      values[column],
                      timestamp,
                      ConsistencyLevel.ZERO)

    except Thrift.TException, tx:
      print "Thrift: %s" % tx.message

    try:
      #Query for data
      column_parent = ColumnParent(column_family='Comments')
      slice_range = SliceRange(start='', finish='')
      predicate = SlicePredicate(slice_range=slice_range)
      result = client.get_slice(keyspace,
                                super_key,
                                column_parent,
                                predicate,
                                ConsistencyLevel.ONE)

      pp.pprint(result)

    except Thrift.TException, tx:
      print "Thrift: %s" % tx.message

  except Thrift.TException, tx:
    print "Thrift: %s" % tx.message
  finally:
    transport.close()

if __name__ == '__main__':
      main()

これを見て分かると思うけれど、リレーショナルデータベースで既に組まれているシステムを、Cassandraに置き換えるのは、大変な努力が必要だね。データ構造も違うし、プログラム側の処理も全然違うものになってしまう。リレーショナルデータベースみたいに、日付でソートしようと思うだけで、2箇所にデータを入れないといけないという。
CassandraのIOアクセスの速さの理由に、データが常にソートされた状態で保存されている。というのがあるから、ソート順番が決まっているデータにしか使えないっていうのもあるね。ソートをいじるということは、一度データベースを止めないといけない気がする。

そんな訳で、リレーショナルデータベースが得意な部分と、Cassandraが得意な部分を見つけて、うまく混ぜて使うのがいいんじゃないかなと思う。一貫性や、トランザクションを使いたいデータとかはリレーショナルデータベースがいいし、ある程度矛盾や壊れてもいいようなデータで、負荷が高いものは、Cassandraとなるのだろうか。
う~ん、これは本当に使いどころを考えるのが難しいね。サーバプログラムでローカルのメモリ上には持つには大きすぎるような構造体を、Cassandraに持たせてしまうとかだろうか。それでいて、一貫性が緩くても良いもの。
DynamoはAmazonのショッピングカートで使っているっていう話だけれど・・・
後は、単純な集計結果のキャッシュかな。それが、Memcachedを使うよりもCassandraのほうがいい。ってのがあるならいいんだけれど・・・まあ、データ構造が持てる分、複数の集計結果を1度に取得出来るとかはあるか。集計結果がスケールアウトする程の物量になるのか?!っていうのもあるね。

後は、リレーショナルデータベース+Cassandraでデータベース定義を作る事かな。トランザクションが使えないから、MySQLなら、MyISAMで構築していた部分をCassandraに持って行けるなら、特に問題はなさそう。1テーブルが余りにも肥大化しすぎてメモリに乗らないようなデータもCassandra向きかなぁ
2重管理はしたくないから、その辺りが落しどころなのかな・・・Cassandraも持っていて、リレーショナルデータベースも持っている。なんていうデータは作りたくないし。

Python: Cassandra: Python で Cassandra に接続する

Cassandra へのアクセスでベースになっているプロトコルが Thrift のようだ。
Cassandra用のPythonライブラリが、python-cassandra の模様。これが、Thrift に依存している。

Python 開発環境を作るのも久しぶりだったので、こちらのサイトを参考に構築。
2009年版Python開発環境を整えよう
環境は OpenSUSE 11.3 Cassandra 0.6.4

su -
yast -i python python-devel gcc python-setuptools
easy_install virtualenv
easy_install virtualenvwrapper
exit
vim ~/.bashrc

# 以下を .bashrc に追加
# Import virtualenvwrapper
if [ -f /usr/local/binvirtualenvwrapper.sh ]; then
  source /usr/local/bin/virtualenvwrapper.sh
fi

mkdir ~/.virtualenvs

source ~/.bashrc

mkvirtualenv --python=/usr/bin/python2.6 cassandra

# bashの表示が変わる。
(cassandra)user@domain:>

pip install thrift
pip install python-cassandra

これで完了。特につまるところはないかなぁ C言語で書かれているモジュールっぽいので、gcc は必要みたいだね。で、その為に、Python.h が必要だから、python-devel を入れておく必要性がある。

ちなみに、virtualenv はローカル環境構築用なので、pipでインストールしたライブラリは ~/.virtualenvs/cassandra/lib/python2.6/site-packages 以下に入る。

サンプルはこちら拝借。
ThriftExamples – Cassandra Wiki

#!/usr/bin/env python # encoding: utf-8
"""Sample Cassandra Client

Created by Chris Goffinet on 2009-08-26."""

from thrift import Thrift
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol.TBinaryProtocol import TBinaryProtocolAccelerated
from cassandra import Cassandra
from cassandra.ttypes import *
import time
import pprint

def main():

    socket = TSocket.TSocket("localhost", 9160)
    transport = TTransport.TBufferedTransport(socket)
    protocol = TBinaryProtocol.TBinaryProtocolAccelerated(transport)
    client = Cassandra.Client(protocol)
    pp = pprint.PrettyPrinter(indent=2)
    keyspace = "Keyspace1"
    column_path = ColumnPath(column_family="Standard1", column="email")
    key = "1"
    value = "foobar@example.com "
    timestamp = time.time()
    try:
        transport.open()
        #Insert the data into Keyspace 1
        client.insert(keyspace,
                      key,
                      column_path,
                      value,
                      timestamp,
                      ConsistencyLevel.ZERO)
        #Query for data
        column_parent = ColumnParent(column_family="Standard1")
        slice_range = SliceRange(start="", finish="")
        predicate = SlicePredicate(slice_range=slice_range)
        result = client.get_slice(keyspace,
                                  key,
                                  column_parent,
                                  predicate,
                                  ConsistencyLevel.ONE)
        pp.pprint(result)
    except Thrift.TException, tx:
        print 'Thrift: %s' % tx.message
    finally:
        transport.close()

if __name__ == '__main__':
    main()

pycassa というのもあるみたいだけれど、これ以上ラッピングしても意味あるのかなぁ~ DjangoみたいなWebフレームワークに組み込まれるくらいかな?

MotionBuilder: Python: 親子関係を作る

FBModel 以下のクラスでは、親子関係を作る事が出来るらしい。

というわけで、どうやるかというと、子供にしたい FBModel の Parent 属性に 親にしたい FBModel を代入するだけ。

FBModel.Parent = FBModel

これは簡単だね!

MotionBuilder: Python: 選択されている Component 取得

前に書いた、職場のモーション担当の人が MotionBuilder用に Python を覚えるという事で
自分がサンプルコードを書いてそれを見ながら、モーション担当の人が、あーでもない、こーでもないといじっております。

しかし、これ日本語の資料殆どないね。

元々Pythonが出来る人じゃないと、難易度が凄い高い。
しかも、日本人でPython出来る人って少ないから、更に難易度高そう。

他にも、MotionBuilder用に Python 覚えたい人いるかもしれないので、サンプルなんぞを。

選択されている Component の名前表示 スクリプト

from pyfbsdk import FBSystem

for lComp in FBSystem().Scene.Components:
    if lComp != None and lComp.Selected:
        # MotionBuilder 2009
        print lComp.LongName
        # MotionBuilder 7.5
        #print lComp.ShortName

バージョンによって、名前の参照方法が違うみたい。
LongName は、 FBModel 以下の階層に存在していて、 Unique name and namespace.
Name は、FBComponent 以下の階層に存在していて、 Unique name.

で、選択されているComponentを取得する方法はもう一通りある。

from pyfbsdk import FBModelList, FBGetSelectedModels

sels = FBModelList()
FBGetSelectedModels(sels, None, True)

for s in sels :
    # MotionBuilder 2009
    print s.Name
    # MotionBuilder 7.5
    #print s.ShortName

# 追記
MotionBuilder 7.5 と 2009 で挙動が違ったので報告。

FBSystem().Scene.Components

で、取得するコンポーネント一覧が 2009 だと FBModel オブジェクトを返していたのに
7.5 だと、 FBComponent オブジェクトを返してくる。
FBModel は FBVector3d などの情報を持っているけれど、 FBComponent は持っていないので
処理に必要な情報が足りなかった。

というわけで、バージョンによって微妙に挙動が違う。

MotionBuilder 使っている職場の人の話だと、2009 は、Pythonスクリプト強化とラグドール機能の追加がメインらしいけれど
Pythonスクリプト組める人が少ない上に、ラグドールは殆ど使っている人が居ないらしいので、操作系が使いやすい 7.5 の方が良いのだそうな。
という訳で、7.5 向きのスクリプト情報が多くなるかも。

Pythonでmayaのプラグインを作ってみた・・・んだけれど?w

Pythonでmayaのプラグインを作ってみたんだけれど、MELで同じチームのモデラーの人が、既に作っていたよ!!w
という訳で、お互いにちょこちょこ機能を見せ合ったりして、順調に機能が拡張されました。

そもそも、mayaでどういう操作をしたいのかが、何度か説明してもらわないとさっぱ~りなので、今回MELを扱える人がいてかなり助かった。
maya操作した時のログがMELで流れるから、MELの方が作りやすいっぽいけれど
あのshell scriptっぽい見た目が嫌だなぁ

MELでできる事は、Pythonでもできるとマニュアルに書いてあったはず・・・なので
これからも、Pythonで作っていこうかな。

Pythonの外部ライブラリ読み込みとかもできるだろうし。
できなかったら、スクリプト言語なんだし、コピペで突っ込めばいいか。

画像変換とかは、Pythonのライブラリ使うより、mayaが持ってるライブラリでやるのかなぁ~
レンダリングとかも、mayaがもってるだろうし。

ふ~む、色々調べる事は多そう。
ちなみに、window作ったり、チェックボックスとか、ボタンとか
そういうのは、結構簡単に作れたりする。

import maya.cmds as cmds
cmds.button(label=’Label’, command=’function_name’)

だけで、ボタン作れちゃうよ。
commandの中は、あらかじめ作っておいた関数名を入れるだけ。

ふ~む、今回は、背景担当の人用に作ったんだけれど、キャラクター担当の人も、作って欲しいのがあるらしい。
mayaのプラグイン作る機会は多そうだなぁ~
Python覚えておいて、超役に立ってるよ!!
やっぱり、時代はPythonですな。
とか言いつつ、今の仕事では、C/C++, Java, Python使っているけれど・・・w

Django: model の URLField のちょっとした機能

URLFieldを使っていたら、はまったので、軽くメモ。
URLField class の定義はこうなっているらしい。

class URLField([verify_exists=True, max_length=200, **options])

何ではまったかというと、デフォルト引数が True になっている、verify_esists ってやつですな。
これ、わざわざHTTP Requestだして、存在しているかどうかを調べるためのオプション。
これを知らないまま、開発環境のURLをぶちこんだら、全然反応がなくない、エラーメッセージもでないし、結構焦ったw
これ調べてくれるのは凄いいい機能だと思うんだけれど、is_valid を実行した瞬間に凄い待たされるのはちょっと困るよね。
だからといって、確認しないままにデータベースに入れたら意味がない。
ってことで、使うときは注意が必要かもなぁ
タイムアウト時間も設定できればいいんだけれどねー

偶然、目的のドメインが見つからなくて、遅かっただけなんだけれどね・・・w
ってか、Django 1.0 の日本語ドキュメントできているんだ。すげー 仕事早いなぁ
http://djangoproject.jp/doc/ja/1.0/index.html

Django: form_for_model の代わりに、ModelForm Classを使うらしい version 1.0

久しぶりに、Djangoを触っていたら、form_for_model がなくなっていた。
で、代わりに、ModelForm Classが出来ていたので、そちらを使うみたいだ。
特に難しいこともなく、よりシンプルになったんじゃないかなぁ~
これを実装するために前はclassの継承とかしていたから・・・俺は凄い楽になったと思う。

Creating forms from models

from django import forms
from integra.profile.models import Profile
class ProfileForm(forms.ModelForm):
class meta:
model = Profile
fields = ('language', 'country', 'timezone', )

Python: Pyrhonで、Unicodeをurlencodeをするときの注意点

Pythonで、HTTP Requestを作成するのに、URLEncode(URIEncode)をしようとして、Unicodeで文字列を受け取って、変換かけようと思ったら、
UnicodeDecodeError
が発生して、は!?なぜに!?と思ったので調べてみた。
どうにも、urllib.urlencode は、Unicodeには対応していないらしい。

import urllib
urllib.urlencode(
{'title': "火目の巫女", }
)

と書いた場合、”火目の巫女”の部分は、Unicode扱いなんだけれど、これがUnicodeDecodeErrorを投げてくれる。
‘ascii’ codec can’t encode characters in position 0-1
こんなエラーが出るっぽい。
ということで、以下の様に、str型に変換してあげる。

import urllib
urllib.urlencode(
{'title': "火目の巫女".encode('utf-8'), }
)

いや、参ったねこいつは。三時間くらい、あれ~?なんじゃこりゃ!?とか悩んでた・・・w

Python: The with statement

with文?とも言うべき、Pythonの新しい文法。
Python2.5では、futureという扱いだったけれど、Python2.6から正式な機能としてリリースされているっぽいので、ちょっと使ってみた。
というか、これを使うと、PythonでのClass設計が大きく変わるだろうから、結構重要な追加機能だと個人的に思っていたり。
それなのに、まだ日本語で紹介しているサイトが少ないっぽい?
少なくとも自分が検索した感じでは見つからなかった。なので、英語のドキュメントがお勧め。

Data model — Python v2.6 documentation#The with statement
Data model — Python v2.6 documentation#With Statement Context Managers
PEP 343

Python2.5で使う場合は、以下の様にimportする必要性がある。

from __future__ import with_statement

とりあえず、どんな機能?っていう説明でも。

簡単に言うと、例外処理。
try, except, finally, else に対応する機能を提供してくれています。

何で新しい機能が?今までの例外処理機能でも十分じゃない?
という疑問は、Pythonがオブジェクト指向言語という概念が理解できていれば、簡単に解決するかと。

今までは、Classの機能を使っての、例外処理という事が出来なかったけれど、with を使えばそれが可能になる。という事。
なので、最初に書いた様に、Class設計が大きく変わるだろうな。と自分は思っていたりする訳です。

という訳で、軽く使ってみた。

最初に、try, except, finally, else を使って書いてみる。
単純に、ファイルをオープンする時に、例外処理を書いているだけである。

try:
  file = open("with.txt")
except IOError:
  print "Failed to open a file."
else:
  print "Success to open a file."
  file.close()
finally:
  print "End of Exception handling."

これを、with statement を使って書き直すとこうなる。

from __future__ import with_statement

class MyClass():
  def __init__(self):
    self.file = None

  def __enter__(self):
    self.file = None
    print "MyClass: enter"
    return self

  def __exit__(self, exc_type, exc_value, exc_tb):
    print "MyClass: exit"
    if not exc_type:
      print "Secucess to open a file."
    else:
      print "Failed to open a file."

    if self.file:
      print "close file"
      self.file.close()
      return True

  def open(self, file_name):
    self.file = open(file_name)
    print "open file"

  def process():
    with MyClass() as my_class:
      my_class.open("with.txt")

if __name__ == '__main__':
  process()

with statement の処理の流れを解説すると、以下の様な流れになる。
リポジトリはこの辺り。Repository: with

1. context managerに対応した書式になっているかを調べる。
2. __enter__() メソッドの呼び出し。
3. __enter__() の戻り値を as の次の変数に格納します。
サンプルでは、__enter__()では、return self をしているので、自分自身のインスタンス値を返します。
更に、 my_classにその値を格納しています。
4. : 以下の実行分を実行します。(以下 suite)
5. __exit__() メソッドの呼び出し。
__exit__() は、def __exit__(self, exc_type, exc_value, exc_tb) と四つの引数を取る必要性がある。引数となる変数名は自由。
それぞれに、例外が発生した場合の例外情報が格納され、例外が発生しない場合は、None が格納される。

__exit__() の仕様に関して補足をすると、__exit__() の戻り値が True の場合、suite での例外発生は隠蔽され、処理は続行される。戻り値が False の場合、通常の例外発生時と同じく、処理がそこで(__exit__() 内の処理終了時点)中断されることになる。

例外以外の制御で suite の処理を抜けた場合、__exit__() メソッドの戻り値は無視される。__exit__() 自体は呼び出される。
サンプルでは、with の部分を関数に切り出しているので、途中で return を呼び出すように変更を加えてみると分かりやすいと思う。

といった感じで、ここまで読めば分かると思うけれど、今まではClassの設計に例外処理までは盛り込めなかった。盛り込んだとしても、あるメソッドで例外が発生した場合、別のメソッドで例外想定した処理を加えて、ラッパーとして定義する。という形になっていたと思う。
それが、オブジェクトの生成から、オブジェクトメソッドの呼び出し、例外処理まで一環して、with statement(Context manager)で扱えるようになったので、Classを設計する人が、with statement を知っているかどうかで、結構変わってくると思う。

好みの問題もあるので、使う使わないは自由だとは思うけれど、よりオブジェクト指向言語っぽくなったなぁ~と思うのは自分だけなのだろうか。

Pythonで日本語文字コードを扱う + for で indexを取得

Shift_JISで書かれたファイルを読み込もうとしていたんだけれど、どうにも、forで回した時におかしい。
って気がついた。
そりゃそうだ、文字コードが違うんだから。
という事で、調べたら、codecsというモジュールを使うのがいいらしい。

この辺りが参考になる。
4.9 codecs — codec レジストリと基底クラス
Python でUTF-8, shift_jis, euc_jpなど日本語を使う方法

ようはファイルをオープンする時に、文字コードを指定できるって事だね。
内部的には、UTF-8へのdecodeを行っているので、それに失敗する場合があるからその例外処理が必要。

import codecs

infp = codecs.open(kanji_file['in'], 'r', 'shift_jis')
try:
  lines = infp.readlines()
except UnicodeDecodeError:
  pass
infp.close()

で、こうやって取得した文字列をぐるんぐるん回そうとしていたんだけれど途中で、何回回ったかカウンターが欲しくなりました。
ぐぐってみたら、こんな方法が!!

for i, ch in enumerate(lines[0]):
  nodes.append(ch)
  if 0 == (i % 12):
    nodes.append(u"\n")

こちらのサイトを参考にさせていただきました(。。
Develogger: Pythonのfor文

列挙型のこんな使い方があったのかぁ・・・
と、ちょっと感動w
Pythonのfor文というか、loop処理は色々な書き方があるからなぁ~
zipとかmapだけでは、まだまだ初歩の段階だよね(。。