物理設計
次に物理設計に入っていきます。
物理設計は今回も同様に、「クエリ設計」をした後に、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, …} |