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

第8回Cassandraで検索するには[後編]

前回からずいぶん時間がたってしまいました。申し訳ありません。

前回はgetメソッド、そしてget_sliceメソッドでデータを検索する方法を見ていきました。今回は残りのget_range_slices、multiget_slice、get_countの3つのメソッドを見ていきます。

前回同様、検索メソッドの確認の前準備としてデータを投入しておく必要があります。詳細は第7回をご覧ください。

複数のロウを取得する ~get_range_slicesメソッド

前回は基本的に1つのロウに対する操作でしたが、今回は複数のロウを取得してみましょう。そのために使うのがget_range_slicesメソッドです。

まずはコードを見てください。リスト1は郵便番号、住所とその読みを検索するコードです。

リスト1 郵便番号、住所と読みを検索
public class SimpleAddressSearchGetRangeSlices {

    public static final String KEYSPACE = "Keyspace1";

    public static final String COLUMN_FAMILY = "SimpleAddress";

    public static void main(String[] args) throws IOException {
        TSocket transport = new TSocket("localhost", 9160);
        TProtocol protocol = new TBinaryProtocol(transport);
        try {
            transport.open();
        } catch (TTransportException e) {
            throw new RuntimeException(e);
        }
        try {
            Cassandra.Client client = new Cassandra.Client(protocol);
            SlicePredicate predicate = new SlicePredicate();
            predicate.setColumn_names(Arrays.asList("postalCode".getBytes(),
                    "address".getBytes(), "addressYomi".getBytes()));

            KeyRange range = new KeyRange();
            range.setStart_key("");
            range.setEnd_key("");

            List<KeySlice> ret = client.get_range_slices(KEYSPACE,
                    new ColumnParent(COLUMN_FAMILY), predicate, range,
                    ConsistencyLevel.ONE);
            int i = 0;
            for (KeySlice keySlice : ret) {
                System.out.println((i++) + " key = " + keySlice.getKey());
                for (ColumnOrSuperColumn csc : keySlice.getColumns()) {
                    Column column = csc.getColumn();
                    String name = new String(column.getName());
                    String value = new String(column.getValue());
                    System.out.println("\t\t" + name + " -> " + value + "("
                            + column.getTimestamp() + ")");
                }
            }
        } catch (InvalidRequestException e) {
            throw new RuntimeException(e);
        } catch (UnavailableException e) {
            throw new RuntimeException(e);
        } catch (TimedOutException e) {
            throw new RuntimeException(e);
        } catch (TException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                transport.flush();
            } catch (TTransportException e) {
                throw new RuntimeException(e);
            } finally {
                transport.close();
            }
        }
    }
}

SlicePredicateについては前回ご紹介しているので、そちらをご覧ください。

このコードで新たに登場しているのは、KeyRangeとKeySliceという2つのクラスです。

KeyRange

KeyRangeは複数ロウを取得するときのレンジを表すクラスです。これには以下のようなことを設定でき、これに従って検索結果が返されます。

  • キーの開始から終了のレンジ
  • 取得する最大数
  • トークンによるレンジ

リスト1のコードでは、以下のようにキーのレンジを何も指定しないようにしています。

KeyRange range = new KeyRange();
range.setStart_key("");
range.setEnd_key("");

ちなみに、デフォルトでは100件分のデータしか取得しません。そのため、たとえば1000件分取得する場合には、以下のようにする必要があります。

KeyRange range = new KeyRange(1000);
range.setStart_key("");
range.setEnd_key("");

また、キーの開始位置と終了位置を指定する場合は以下のようになります。

KeyRange range = new KeyRange(1000);
range.setStart_key("1760000");
range.setEnd_key("1760004");

このようにキーのレンジを指定できるのですが、いくつか注意点があります(詳細は後述します)。まずは「KeyRangeでキーの取得範囲を指定できる」ことだけわかれば現段階では問題ありません。

KeySlice

次はKeySliceです。KeySliceはget_range_slicesで返される値で、取得したキーと指定されたカラムの集合を持っています。get_range_slicesでは、このKeySliceのListを返します。

取得するコードは、以下のようにKeySliceのListから1つずつ中身を表示するようになっています。

for (KeySlice keySlice : ret) {
    System.out.println((i++) + " key = " + keySlice.getKey());
    for (ColumnOrSuperColumn csc : keySlice.getColumns()) {
        Column column = csc.getColumn();
        String name = new String(column.getName());
        String value = new String(column.getValue());
        System.out.println("\t\t" + name + " -> " + value + "("
                + column.getTimestamp() + ")");
    }
}

get_range_slicesメソッドで注意すべきところ

ここまでの感じだとCassandraでもレンジクエリが簡単にできている感じがありますが、注意すべきところがあります。それはレンジクエリの順序の問題です。

Cassandraはデータをノード間に分散させる方式として、デフォルトではRandomPartitionerを使います。これは「パーティショナクラス」と呼ばれるもので、キーのハッシュ値を使って、値をノードにランダムに分散させます。

これを使う最大のメリットは、データが均等に分散しやすいため、各ノード間にかかる負担が均等になり、全体として効率がよいことです。ただしその一方で、⁠レンジクエリの順序が保障されない」というデメリットがあります。つまり、データの挿入順序が保障されず、結果がばらばらに返ってきてしまうのです。

以下はリスト1を実行した結果です。一目瞭然ですね。キーによる順序が一貫せずに値が返ってきています。

0 key = 1680082
        address -> 東京都 杉並区 久我山(1281276409219)
        addressYomi -> トウキョウト スギナミク クガヤマ(1281276409219)
        postalCode -> 1680082(1281276409219)
1 key = 1780062
        address -> 東京都 練馬区 大泉町(1281276409219)
        addressYomi -> トウキョウト ネリマク オオイズミマチ(1281276409219)
        postalCode -> 1780062(1281276409219)
2 key = 1750081
        address -> 東京都 板橋区 新河岸(1281276409219)
        addressYomi -> トウキョウト イタバシク シンガシ(1281276409219)
        postalCode -> 1750081(1281276409219)
3 key = 1540003
        address -> 東京都 世田谷区 野沢(1281276409219)
        addressYomi -> トウキョウト セタガヤク ノザワ(1281276409219)
        postalCode -> 1540003(1281276409219)

パーティショナクラスを使い分ける

では、他に方法がないのでしょうか?

実はCassandraは別の手段を準備してあります。それはOrderPreservingPartitionerというパーティショナを使う方法です。この方法ならば、レンジクエリも想定どおり正しく動いてくれます。

ただしメリットの裏にはデメリットもあるものです。OrderPreservingPartitionerを使うと、データの投入順序を保障するために、Cassandraはデータを均等に分散させるのではなく、決まった順序でデータをノードに投入していきます。

そのため、⁠あるノードにはデータが大量に存在し、あるノードにはあまりデータがないためにクエリの検索スピードや運用によるノードのリバランスなど考慮しなくてはいけない点が増える可能性があります。

実行すると、皆さんの予想どおり、順序が保障されて表示されます。

0 key = 1000000
        address -> 東京都 千代田区 (1281196753776)
        addressYomi -> トウキョウト チヨダク (1281196753776)
        postalCode -> 1000000(1281196753776)
1 key = 1000001
        address -> 東京都 千代田区 千代田(1281196753776)
        addressYomi -> トウキョウト チヨダク チヨダ(1281196753776)
        postalCode -> 1000001(1281196753776)
2 key = 1000002
        address -> 東京都 千代田区 皇居外苑(1281196753776)
        addressYomi -> トウキョウト チヨダク コウキョガイエン(1281196753776)
        postalCode -> 1000002(1281196753776)
3 key = 1000003
        address -> 東京都 千代田区 一ツ橋(1丁目)(1281196753776)
        addressYomi -> トウキョウト チヨダク ヒトツバシ(1チョウメ)(1281196753776)
        postalCode -> 1000003(1281196753776)

RandomPartitionerとOrderPreservingPartitionerのどちらを使うかは、Cassandraを使う場合に非常に難しい問題で、一概には答えられません。しかし、まずは両者の違いを把握しておくことが大事です。

また、パーティショナはCassandraではグローバルな設定でデータ構造にも影響を与えるので、storage-conf.xml全体で1つの設定になっています。どちらを使うか、注意深く考えて設計する必要があります。

特定のキーの行を複数取得する ~multiget_sliceメソッド

ある特定キーの集合を取得したいときに使うのがmultiget_sliceメソッドです。まずはコードを見てみましょう。

リスト2 特定のキーの集合データを取得する
public class SimpleAddressSearchMultiGetSlice {

    public static final String KEYSPACE = "Keyspace1";

    public static final String COLUMN_FAMILY = "SimpleAddress";

    public static void main(String[] args) throws IOException {
        TSocket transport = new TSocket("localhost", 9160);
        TProtocol protocol = new TBinaryProtocol(transport);
        try {
            transport.open();
        } catch (TTransportException e) {
            throw new RuntimeException(e);
        }
        try {
            Cassandra.Client client = new Cassandra.Client(protocol);
            SlicePredicate predicate = new SlicePredicate();
            predicate.setColumn_names(Arrays.asList("postalCode".getBytes(),
                    "address".getBytes()));
            Map<String, List<ColumnOrSuperColumn>> ret = client.multiget_slice(
                    KEYSPACE, Arrays.asList("1080074", "1080075"),
                    new ColumnParent(COLUMN_FAMILY), predicate,
                    ConsistencyLevel.ONE);

            for (Map.Entry<String, List<ColumnOrSuperColumn>> e : ret
                    .entrySet()) {
                String key = e.getKey();
                System.out.println("key = " + key);
                for (ColumnOrSuperColumn csc : e.getValue()) {
                    Column c = csc.getColumn();
                    String name = new String(c.getName());
                    String value = new String(c.getValue());
                    System.out.println(name + " -> " + value + "("
                            + c.getTimestamp() + ")");
                }
                System.out.println();
            }
        } catch (InvalidRequestException e) {
            throw new RuntimeException(e);
        } catch (UnavailableException e) {
            throw new RuntimeException(e);
        } catch (TimedOutException e) {
            throw new RuntimeException(e);
        } catch (TException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                transport.flush();
            } catch (TTransportException e) {
                throw new RuntimeException(e);
            } finally {
                transport.close();
            }
        }
    }
}

リスト2の例は

  • キーが1080074、1080075のロウを検索
  • その中の郵便番号と住所のカラムを取得
  • という処理を行うものです。⁠get_sliceを複数キーに対応させたAPI」だと思っていただければ間違いありません。

    カラム数を取得するには ~get_countメソッド

    カラム数を取得するには、get_countメソッドを使います。シンプルに「キーに対するロウ内にどれだけのカラムがあるか」を返します。以下はコードの抜粋です。

    Cassandra.Client client = new Cassandra.Client(protocol);
    
    int count = client.get_count(KEYSPACE, "1500002", new ColumnParent(
            COLUMN_FAMILY), ConsistencyLevel.ONE);

    今回は、簡単ですが、get_range_slices、multiget_slice、get_countの3つのメソッドの使い方を見ていきました。

    この中でやはり重要なのが、レンジクエリを実現するget_range_slicesです。レンジクエリを多用するシステムではCassandraのパーティショナを変更する必要があるでしょう。ただしその場合、運用保守で定期的にノードを再バランシングする必要が出てくるかもしれません。

    今回までは、ThriftによるCassandraのネイティブなAPIを見ていきました。次回からThrift APIを隠蔽してくれるフレームワークやライブラリを見ていきます。

    おすすめ記事

    記事・ニュース一覧