Cassandraのはじめ方─手を動かしてNoSQLを体感しよう

第7回Cassandraで検索するには[前編]

Cassandraも0.6系がついに0.6.4まで出てきて、stableなリリースとして十分に使えるところまで来ましたね。この連載のコードもすべて0.6系では動作するはずですので、ぜひ最新のものに入れ替えて試してみてください。

前回まででデータの投入、更新、削除までをご紹介しました。今回から複数回に分けて検索を重点的に見ていきましょう。

前準備としてデータを投入しておく

検索メソッドの確認の前準備として、まずデータの投入を行います。今回はシンプルな郵便番号のデータを利用します。以下のURLから東京都のデータを取得して、解凍後、データを投入してください。

郵便番号データのダウンロード:日本郵便
URL:http://www.post.japanpost.jp/zipcode/dl/kogaki/lzh/13tokyo.lzh

データは説明のため簡易的なデータ構造とします。

  • キーは郵便番号
  • データは、郵便番号(postalCode⁠⁠、あて先(address⁠⁠、あて先読み方(addressYomi)の3つ
  • スーパーカラムは使わない

データを投入するコードは以下のとおりです。

リスト1 SimpleAddressInsert.java(一部抜粋)
Cassandra.Client client = new Cassandra.Client(protocol);
long timestamp = System.currentTimeMillis();

Map<String, Map<String, List<Mutation>>> map = new HashMap<String, Map<String, List<Mutation>>>();

is = Thread.currentThread().getContextClassLoader()
		.getResourceAsStream("13TOKYO.CSV");
BufferedReader reader = new BufferedReader(new InputStreamReader(
		is, "MS932"));
String line = null;
while ((line = reader.readLine()) != null) {
	List mutations = new ArrayList();
	String[] lines = line.split(",");
	String postalCode = removeQuote(lines[2]);
	String addressYomi = buildAddressYomi(lines);
	String address = buildAddress(lines);
	System.out.println(postalCode + " " + addressYomi + " "
			+ address);
	{
		Mutation mutation = new Mutation();
		ColumnOrSuperColumn csc = new ColumnOrSuperColumn();
		csc.setColumn(new Column("postalCode".getBytes(),
				postalCode.getBytes(), timestamp));
		mutation.setColumn_or_supercolumn(csc);
		mutations.add(mutation);
	}
	{
		Mutation mutation = new Mutation();
		ColumnOrSuperColumn csc = new ColumnOrSuperColumn();
		csc.setColumn(new Column("address".getBytes(), address
				.getBytes(), timestamp));
		mutation.setColumn_or_supercolumn(csc);
		mutations.add(mutation);
	}
	{
		Mutation mutation = new Mutation();
		ColumnOrSuperColumn csc = new ColumnOrSuperColumn();
		csc.setColumn(new Column("addressYomi".getBytes(),
				addressYomi.getBytes(), timestamp));
		mutation.setColumn_or_supercolumn(csc);
		mutations.add(mutation);
	}
	Map<String, List<Mutation>> mutmap = new HashMap<String, List<Mutation>>();
	mutmap.put(COLUMN_FAMILY, mutations);
	map.put(new String(postalCode), mutmap);
	count++;
}
client.batch_mutate(KEYSPACE, map, ConsistencyLevel.ONE);
reader.close();

設定ファイル(storage-conf.xml)には以下のように指定します。場所はCassandraのデフォルト設定であるキースペースのKeyspace1以下になります。

リスト2 storage-conf.xml
<ColumnFamily Name="SimpleAddress" CompareWith="UTF8Type" />

実行してデータを投入します。以下のようになるはずです。

...
1002100 トウキョウト オガサワラムラ  東京都 小笠原村 
1002101 トウキョウト オガサワラムラ チチジマ 東京都 小笠原村 父島
1002211 トウキョウト オガサワラムラ ハハジマ 東京都 小笠原村 母島
3654件インサート完了.

このデータをサンプルとして、検索メソッドを使ってみましょう。

単一カラムを取得するには

まずは1件検索してみましょう。Cassandraではgetメソッドを使います。getメソッドは以下のようなAPIになっています。

ColumnOrSuperColumn get(キースペース, キー, ColumnPath, ConsistencyLevel)

コードは以下のようになります。この例では郵便番号⁠1006528⁠の住所を検索しています。

リスト3 SimpleAddressSearchOne.java
Cassandra.Client client = new Cassandra.Client(protocol);
ColumnPath path = new ColumnPath(COLUMN_FAMILY);
path.setColumn("address".getBytes());//検索するカラム名を指定

ColumnOrSuperColumn csc = client.get(KEYSPACE, "1006528", path,
		ConsistencyLevel.ONE);
Column c = csc.getColumn();
String name = new String(c.getName());
String value = new String(c.getValue());

System.out.println(name + " -> " + value + "(" + c.getTimestamp()
		+ ")");

結果は以下のようになります。

address -> 東京都 千代田区 丸の内新丸の内ビルディング(28階)(1281276409219)

第5回でも説明したように、カラムファミリとカラムの位置はColumnPathで特定します。今回のサンプルでは、カラムファミリとカラムのケースなので、検索するカラム名を指定します。

今回のサンプルではありませんが、スーパーカラムの場合は上記に加えて、以下のようにしてスーパーカラム名を指定する必要があります。

ColumnPath path = new ColumnPath(COLUMN_FAMILY);
path.setSuper_column(LongUtil.toByteArray(1006528L));//スーパーカラムを指定
path.setColumn("address".getBytes());//カラム名を指定

また戻り値のColumnOrSuperColumnからデータをどのように取得するかは、カラムまたはスーパーカラムで取得方法が異なります。

カラムの場合
→ColumnOrSuperColumn#getColumn()でカラムを取得
スーパーカラムの場合
→ColumnOrSuperColumn#getSuperColumn()#getColumns()でスーパーカラム内のカラムを取得

複数カラムを取得するには

次に1つのキーから複数カラムを同時に取得してみましょう。この場合、get_sliceを使います。get_sliceのAPIは以下のようになっています。

List<ColumnOrSuperColumn> get_slice(キースペース, キー, ColumnParent, SlicePredicate, ConsistencyLevel);

実際のコードは以下のようになります。

リスト5 SimpleAddressSearchSlice1.java
Cassandra.Client client = new Cassandra.Client(protocol);
ColumnParent parent = new ColumnParent(COLUMN_FAMILY);
String key = "1006528";

// カラムの指定
SlicePredicate predicate = new SlicePredicate();
predicate.setColumn_names(Arrays.asList("postalCode".getBytes(),
		"address".getBytes()));

List<ColumnOrSuperColumn> ret = client.get_slice(KEYSPACE, key,
		parent, predicate, ConsistencyLevel.ONE);

// 結果を表示
for (ColumnOrSuperColumn csc : ret) {
	Column column = csc.getColumn();
	String name = new String(column.getName());
	String value = new String(column.getValue());

	System.out.println("\t" + name + " -> " + value + "("
			+ column.getTimestamp() + ")");
}

結果は以下のとおりです。

address -> 東京都 千代田区 丸の内新丸の内ビルディング(28階)(1281276409219)
postalCode -> 1006528(1281276409219)

ここでColumnParentとSlicePredicateという見慣れないクラスが2つ登場していますね。これらを説明します。

ColumnParent

ColumnParentはColumnPathと非常に似た概念のクラスですが、以下のような違いがあります。

  • ColumnPath → 単一のカラムを見つけるためのクラス
  • ColumnParent→ 複数のカラムを見つけるためのクラス

ColumnParentはディレクトリ構造で言うところの /../ にあたります。ある単一カラムを基点にしてディレクトリ構造をさかのぼり、複数カラムを探索するようなイメージです。

ColumnParentは以下の2とおりのどちらかをとります。

  • カラムの場合 → カラムファミリのみを指定
  • スーパーカラムの場合 → カラムファミリとスーパーカラムを指定

SlicePredicate

SlicePredicateは検索をフィルタするための指定を行うクラスです。フィルタリングにおもに以下の2つの設定ができます。

  • ①カラム名による指定
  • ②カラム名の範囲による指定

上記のサンプルコードでは、①のカラム名による指定で、3つあるカラム(postalCode, address, addressYomi)の中から、postalCodeとaddressの2つだけを指定して検索しました。しかし、結果をよく見てみると、サンプルコードで指定した順序になっていないですね。

第4回でかんたんに説明しましたが、Cassandraのカラムはstorage-conf.xmlに書いた順序でソートされています。クライアントコードで指定した順序ではありません。この点に注意してください。

今回は設定ファイルにCompareWithでUTF8を指定しているので、UTF8の順序でソートされています。そのため、address、postalCodeの順序で結果が取れます。詳細については第4回を参照してください。

検索対象のカラムを絞るには

②のカラム名の範囲によって検索対象のカラムを絞ることもできます。この場合はSliceRangeというクラスを使い、以下のようにして範囲を指定します。

  • カラムの開始位置 →SliceRange#setStartで指定
  • カラムの終了位置 →SliceRange#setEndで指定

SliceRangeが便利な局面としては、たとえばカラムファミリの中に大量の日付をキーとしたカラムが入っている場面などで便利です。

Cassandraでは日付などをキーとしてデータを入れたり、ある項目に対して修正や変更をつぶさに記録したりと、あるキー1つに対しても大量に書き込みが発生する可能性があります。そのため「大量にあるカラムの中から、ある範囲内だけ取り出したい」という状況下では、SliceRangeは有効活用できます。

注意点としては、先ほどもお伝えしたようにカラム順序はstorage-conf.xmlのCompareWith設定に沿って決まるということです。これに従って、startとendを決める必要があります。今回の場合だとUTF8のソート順序になりますので、address, addressYomi, postalCodeの順序にカラム名がソートされていることを意識してください。

たとえば、startをaddressYomiとして、endをpostalCodeとすれば問題ありません。しかし、カラムのソート順序に従わないケース、たとえば以下のようにすると、InvalidRequestExceptionが発生します。

  • start → postalCode
  • end → addressYomi

SliceRangeを使ったサンプルコードは以下のようになります。結果を比較しやすいように、SliceRangeでカラムを絞った場合と絞らなかった場合、両方を実装してみました。

リスト6 SimpleAddressSearchSlice2.java
Cassandra.Client client = new Cassandra.Client(protocol);
ColumnParent parent = new ColumnParent(COLUMN_FAMILY);

String key = "1006528";

SlicePredicate predicate = new SlicePredicate();

// 項目の中から、addressYomiからpostalCodeまでのレンジに入る項目を抽出.
SliceRange range = new SliceRange();
range.setStart("addressYomi".getBytes());
range.setFinish("postalCode".getBytes());
predicate.setSlice_range(range);

List<ColumnOrSuperColumn> ret = client.get_slice(KEYSPACE, key,
		parent, predicate, ConsistencyLevel.ONE);

System.out.println("SliceRangeでカラムを絞った場合");
for (ColumnOrSuperColumn csc : ret) {
	Column column = csc.getColumn();
	String name = new String(column.getName());
	String value = new String(column.getValue());

	System.out.println("\t" + name + " -> " + value + "("
			+ column.getTimestamp() + ")");
}

System.out.println("SliceRangeでカラムを絞らなかった場合");
SliceRange rangeAll = new SliceRange();
rangeAll.setStart("".getBytes());
rangeAll.setFinish("".getBytes());
predicate.setSlice_range(rangeAll);

ret = client.get_slice(KEYSPACE, key, parent, predicate,
		ConsistencyLevel.ONE);

for (ColumnOrSuperColumn csc : ret) {
	Column column = csc.getColumn();
	String name = new String(column.getName());
	String value = new String(column.getValue());

	System.out.println("\t" + name + " -> " + value + "("
			+ column.getTimestamp() + ")");
}

結果は以下のようになります。

SliceRangeでカラムを絞った場合
addressYomi -> トウキョウト チヨダク マルノウチシンマルノウチビルディング(28カイ)(1281276409219)
postalCode -> 1006528(1281276409219)
SliceRangeでカラムを絞らなかった場合
address -> 東京都 千代田区 丸の内新丸の内ビルディング(28階)(1281276409219)
addressYomi -> トウキョウト チヨダク マルノウチシンマルノウチビルディング(28カイ)(1281276409219)
postalCode -> 1006528(1281276409219)

昇順・降順を制御するには

勘の良い方はお気づきかもしれませんが、このままだと昇順・降順のコントロールができないようにみえます。数値データや日付データをカラム名に付与した場合は、昇順・降順を制御したくなりますよね。

そこでSliceRangeにはSliceRange#setReversedというメソッドがついており、これで昇順・降順を制御することができます。

先ほどのコードで降順にしてみましょう。コードは以下のようになります。

リスト7 SimpleAddressSearchSlice3_Reversed.java
SliceRange range = new SliceRange();
range.setStart("postalCode".getBytes());
range.setFinish("addressYomi".getBytes());
range.setReversed(true);

結果は以下のようになります。

SliceRangeでカラムを絞った場合
postalCode -> 1006528(1281276409219)
addressYomi -> トウキョウト チヨダク マルノウチシンマルノウチビルディング(28カイ)(1281276409219)
SliceRangeでカラムを絞らなかった場合
postalCode -> 1006528(1281276409219)
addressYomi -> トウキョウト チヨダク マルノウチシンマルノウチビルディング(28カイ)(1281276409219)
address -> 東京都 千代田区 丸の内新丸の内ビルディング(28階)(1281276409219)

今回はgetとget_sliceについて説明しました。

Cassandraの検索系メソッドは非常に特徴的で、やや癖があります。ただきちんと見れば、そんなに難しいものでもありません。今回と次回で1つずつ覚えていきましょう。

次回は、get_range_slices、multiget_slice、get_countの3つを見てみます。特にget_range_slicesと順序の維持を中心に見ていく予定です。お楽しみに。

今回の記事で紹介したサンプルコード全体は以下からダウンロードできます。

おすすめ記事

記事・ニュース一覧