CodeZine(コードジン)

特集ページ一覧

HBaseを使ってグラフDBを作ってみよう(前編)

初めてのHBase 第5回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2013/10/02 14:00

目次

物理設計

 次に物理設計に入っていきます。

 物理設計は今回も同様に、「クエリ設計」をした後に、HBase上にどうマッピングするかを設計する「スキーマ設計」を行います。

クエリ設計

 まず、クエリから考えていきましょう。

 要件定義からクエリを考えていくと、以下のようなメソッドを実装すれば良いと思います。

// ノードの作成
void createNode(String nodeId, Map<String, String> properties) throws IOException;

// ノードのプロパティの取得
Map<String, String> getNodeProperties(String nodeId) throws IOException;

// ノードのプロパティの追加・更新・削除
void updateNodeProperties(String nodeId, Map<String, String> putProperties, Set<String> deletePropertyNames) throws IOException;

// ノードの削除
void deleteNode(String nodeId) throws IOException;

// リレーションシップの作成
void createRelationship(String startNodeId, String type, String endNodeId, Map<String, String> properties) throws IOException;

// リレーションシップのプロパティの取得
Map<String, String> getRelationshipProperties(String startNodeId, String type, String endNodeId) throws IOException;

// リレーションシップのプロパティの追加・更新・削除
void updateRelationshipProperties(String startNodeId, String type, String endNodeId, Map<String, String> putProperties, Set<String> deletePropertyNames) throws IOException;

// リレーションシップの削除
void deleteRelationship(String startNodeId, String type, String endNodeId) throws IOException;

// 隣接リレーションシップの取得(最新順)
List<Relationship> select(String nodeId, String type, Direction direction, int length) throws IOException;

 updateNodePropertiesやupdateRelationshipPropertiesは、追加・更新したいプロパティ(putProperties)と削除したいプロパティ(deletePropertyNames)を指定できます。

 どちらかのみを指定したい場合は、指定しない方をnullにする仕様となっています。また、selectではdirection(方向)を指定することができます。

 Directionの定義は、以下のようになります。

public enum Direction {
  INCOMING,
  OUTGOING
}

 INCOMINGが指定された場合は、指定されたノードに入ってくるリレーションシップが取得できます。逆に、OUTGOINGが指定された場合は、指定されたノードから出ていくリレーションシップが取得できるという機能です。

 Relationshipクラスは、以下のようになります。

public class Relationship {
  // リレーションシップの始点ノードID
  private String startNodeId;

  // リレーションシップのタイプ
  private String type;

  // リレーションシップの終点ノードID
  private String endNodeId;

  // リレーションシップのプロパティ
  private Map<String, String> properties;

  // ... setterやgetterは省略
}

スキーマ設計

 それでは、スキーマの設計を考えていきます。

ノードのスキーマ

 最初に、ノードについて考えてみましょう。

 まずは単純に、nodeIdをRowKeyにして、Columnにプロパティ名を、Valueにプロパティ値をマッピングしてみましょう。

 ColumnFamilyに関しては、graphの頭文字の固定値"g"で良いでしょう。また、Timestampには、プロパティの更新時間が指定されます。

RowKey ColumnFamily Column Timestamp Value
nodeId "g" propertyName timestamp propertValue

 この場合、プロパティの数が増えるとHBaseのエントリーの数も増えてしまい使用メモリが大きくなり、Scanの効率も低くなります。そのため、今回は、1つのValueにすべてのプロパティを入れる設計にします。その場合、Columnは固定値"p"にします。

 さらに、ノードの作成時間(createTimestmp)と更新時間(updateTimestamp)を同じRowに追加します。

 それぞれ、Columnは固定値"c"と"u"とします。

 まとめると、以下のスキーマになります。

RowKey ColumnFamily Column Timestamp Value
nodeId "g" "c" timestamp createTimestmp
nodeId "g" "p" timestamp {PropertyName:PropertyValue, …}
nodeId "g" "u" timestamp updateTimestamp

 最後に、書き込みや読み込みの分散のために、RowKeyのプレフィックスにhash値をつけます。

 また、それぞれのRowを区別するために、それぞれのRowのhash値の後にRowKeyのタイプを入れます。

RowKey ColumnFamily Column Timestamp Value
hash(nodeId)-0-nodeId "g" "c" timestamp createTimestmp
hash(nodeId)-0-nodeId "g" "p" timestamp {PropertyName:PropertyValue, …}
hash(nodeId)-0-nodeId "g" "u" timestamp updateTimestamp

リレーションシップのスキーマ

 次に、リレーションシップについて考えてみます。

 リレーションシップもノードと同様に、プロパティと作成時間(createTimestmp)と更新時間(updateTimestamp)を同じRowに入れる設計にします。

 リレーションシップのRowは以下のようになります。

RowKey ColumnFamily Column Timestamp Value
startNodeId-type-endNodeId "g" "c" timestamp createTimestmp
startNodeId-type-endNodeId "g" "p" timestamp {PropertyName:PropertyValue, …}
startNodeId-type-endNodeId "g" "u" timestamp updateTimestamp

 クエリ設計から、nodeId、type、direction(方向)を指定して隣接リレーションシップを最新順で取得できる必要があります。

 これを実現するために、以下のような最新順インデックスのRowが必要になります。

RowKey ColumnFamily Column Timestamp Value
endNodeId-INCOMING-type-(Long.MAX_VALUE - createTimestmp)-startNodeId "g" "" timestamp {PropertyName:PropertyValue, …}
startNodeId-OUTGOING-type-(Long.MAX_VALUE - createTimestmp)-endNodeId "g" "" timestamp {PropertyName:PropertyValue, …}

 typeの後ろに(Long.MAX_VALUE - createTimestmp)が入っていることにより、Scanした際に最新順で取得することができます。

 例えば、"node1"から出ていく(OUTGOING)、タイプが"follow"のリレーションシップを取得したい場合は、"node1"-INCOMING-"follow"でプレフィックスScanすればよく、逆に、"node1"から入ってくる(INCOMING)、タイプが"follow"のリレーションシップを取得したい場合は、"node1"-OUTGOING-"follow"でプレフィックスScanすればよいということになります。

 上記のスキーマでは、インデックスのRowのValueにプロパティを格納する設計(非正規化)にしています。このようにすることで、インデックスのRowをScanするだけでプロパティを取得することができるので、読み込み時に高速です。

 その代わり、リレーションシップのRowとインデックスのRowとの間にデータの不整合が起こる可能性があったり、データ容量が増えてしまったり、書き込み時に多少遅くなるというデメリットがあります。

 もちろん、インデックスのRowにプロパティを格納せず、それをScanした後にプロパティを取得するために、リレーションシップのRowをルックアップする設計(正規化)にすることも可能です。

 後者の設計では、前者とメリットとデメリットが入れ替わります。これらの設計は、アプリケーションの要件によって変えていくべきです。なお、今回は読み込み重視の前者の設計(非正規化)にしています。

 最後に、ノードと同様に書き込みや読み込みの分散のために、RowKeyのプレフィックスにhash値をつけます。また、それぞれのRowを区別するために、それぞれのRowのhash値の後にRowKeyのタイプを入れます。

RowKey ColumnFamily Column Timestamp Value
hash(startNodeId)-1-startNodeId-type-endNodeId "g" "c" timestamp createTimestmp
hash(startNodeId)-1-startNodeId-type-endNodeId "g" "p" timestamp {PropertyName:PropertyValue, …}
hash(startNodeId)-1-startNodeId-type-endNodeId "g" "u" timestamp updateTimestamp
hash(endNodeId)-2-endNodeId-INCOMING-type-(Long.MAX_VALUE - createTimestmp)-startNodeId "g" "" timestamp {PropertyName:PropertyValue, …}
hash(startNodeId)-2-startNodeId-OUTGOING-type-(Long.MAX_VALUE - createTimestmp)-endNodeId "g" "" timestamp {PropertyName:PropertyValue, …}

  • LINEで送る
  • このエントリーをはてなブックマークに追加

バックナンバー

連載:初めてのHBase

もっと読む

著者プロフィール

  • 鈴木 俊裕(スズキ トシヒロ)

    株式会社サイバーエージェント アメーバ事業本部 Ameba Technology Laboratory 2008年4月に株式会社サイバーエージェントに新卒で入社。基盤システムの開発・運用に従事する。 2010年4月にHadoop/Hiveを用いたログ解析基盤の開発・運用を担当する...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5