CLOVER🍀

That was when it all began.

Apache Geodeでインデックスを定義する

先日、Apache Geodeの検索機能(OQL)を試してみましたが、その時はとりあえずQueryを投げてみただけでした。

Apache Geodeの検索機能を使う(OQLを使う) - CLOVER

Apache Geodeでは、Queryのパフォーマンスアップのためにインデックスを定義することができるようです。

When should I create indexes to use in my queries?

How do I create an index?

今回は、インデックスを作ってからQueryを投げるようにしてみたいと思います。

こちらのドキュメントを見つつ…。

http://geode.docs.pivotal.io/docs/developing/query_index/query_index.html

Apache Geodeで定義できるインデックス

Apache Geodeでは、以下の種類のインデックスが定義できるようです。

  • 通常?のインデックス(Functional Index? Range Index?)
  • Key Index
  • Hash Index

それぞれ使い方、効果が違うのですが、明示的に名前があるのはKey IndexとHash Indexみたいですね。

http://geode.docs.pivotal.io/docs/developing/query_index/creating_key_indexes.html

http://geode.docs.pivotal.io/docs/developing/query_index/creating_hash_indexes.html

それ以外の特に名前のないインデックスは、なんと呼ばれているか微妙にわかりませんでした…。ソースコード上はFunctionalと言った方が正しい気もしますが。

https://github.com/apache/incubator-geode/blob/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/cache/query/IndexType.java#L46-L47

とはいえ、作られるのはRangeIndexだったりします。Cache XMLで定義する時には、"range"指定になりますしね。
https://github.com/apache/incubator-geode/blob/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/cache/query/internal/index/RangeIndex.java#L68

まあ、バラバラと呼んでいても微妙なので、今回は通常のIndexを「Functional Index」と呼ぶことにします。

また、インデックスはローカルメモリに保持するみたい?
https://github.com/apache/incubator-geode/blob/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/cache/query/internal/index/IndexManager.java#L108

インデックス作成時のガイドライン(メンテナンスコストがあるとか、メモリを消費するとか、オプティマイザの話とか)については、こちらを参照のこと。
http://geode.docs.pivotal.io/docs/developing/query_index/indexing_guidelines.html

とりあえず、種類や持ち方はこのくらいにして、順次使っていってみましょう。

準備

まずは、ビルド定義を。Maven依存関係は、こちら。

        <dependency>
            <groupId>org.apache.geode</groupId>
            <artifactId>gemfire-core</artifactId>
            <version>1.0.0-incubating.M1</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.3.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.byteman</groupId>
            <artifactId>byteman-bmunit</artifactId>
            <version>3.0.2</version>
            <scope>test</scope>
        </dependency>

利用するApache Geodeバージョンは、1.0.0-incubating.M1とします。

また、テストコードとしてJUnitとAssertJを利用するのですが、今回はトレースのためにByteman(BMUnit)も使おうと思います。

テストコードの雛形とBytemanのRule

今回記述するテストコードは、以下のクラス内に実装するものとします。
src/test/java/org/littlewings/geode/query/QueryWithIndexingTest.java

package org.littlewings.geode.query;

import java.util.Arrays;
import java.util.List;

import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.CacheFactory;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.RegionShortcut;
import com.gemstone.gemfire.cache.query.FunctionDomainException;
import com.gemstone.gemfire.cache.query.IndexExistsException;
import com.gemstone.gemfire.cache.query.IndexNameConflictException;
import com.gemstone.gemfire.cache.query.NameResolutionException;
import com.gemstone.gemfire.cache.query.Query;
import com.gemstone.gemfire.cache.query.QueryInvocationTargetException;
import com.gemstone.gemfire.cache.query.QueryService;
import com.gemstone.gemfire.cache.query.SelectResults;
import com.gemstone.gemfire.cache.query.Struct;
import com.gemstone.gemfire.cache.query.TypeMismatchException;
import org.jboss.byteman.contrib.bmunit.BMScript;
import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(BMUnitRunner.class)
@BMScript("src/test/resources/trace.btm")
public class QueryWithIndexingTest {
    // ここに、テストを書く!

    protected List<Book> getBooks() {
        return Arrays.asList(
                new Book("978-4777518654", "はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発", 2700),
                new Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104),
                new Book("978-4048662024", "高速スケーラブル検索エンジン ElasticSearch Server", 3024)
        );
    }
}

BMUnitによるRule付与と、テスト用のデータ登録メソッドを含んでいます。

なお、Cache/Regionに登録するクラスは、以下とします(前回と同じもの)。
src/test/java/org/littlewings/geode/query/Book.java

package org.littlewings.geode.query;

import java.io.Serializable;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    private String isbn;
    private String title;
    private int price;

    public Book() {
    }

    public Book(String isbn, String title, int price) {
        this.isbn = isbn;
        this.title = title;
        this.price = price;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

BytemanのRuleは、以下のように作成しました。
src/test/resources/trace.btm

RULE trace Compiled PlanInfo
INTERFACE com.gemstone.gemfire.cache.query.internal.CompiledValue
METHOD getPlanInfo
AT EXIT
IF TRUE
  DO org.apache.logging.log4j.LogManager.getLogger($0.getClass())
       .info("### [byteman] [{}]\n  => PlanInfo: evalAsFilter = {}, isPreferred = {}, indexes = {}",
             new Object[] { $0.getClass(), $!.evalAsFilter, $!.isPreferred, $!.indexes })
ENDRULE

Queryのコンパイル結果を構築する際の、実行計画を見るためのRuleです。Queryのコンパイル時に、実行計画が以下のメソッドで返されるようになっています。
https://github.com/apache/incubator-geode/blob/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/cache/query/internal/CompiledValue.java#L103

こちらのパッケージに、Queryのコンパイル結果を表すクラスがたくさんあるので、なにかあったら見てみるとよいかもしれません。
https://github.com/apache/incubator-geode/tree/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/cache/query/internal

あと、トレース時にはApache GeodeApache Log4j2を使っているようなので、こちらに合わせてログ出力するようにしました。

と準備はここまでにして、試していってみましょう。

インデックスなしの場合

まずは、インデックスを定義せずにQueryを投げてみます。

    @Test
    public void nonIndexQuery() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, Book> region =
                    cache.<String, Book>createRegionFactory(RegionShortcut.LOCAL).create("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();
            Query query1 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE isbn = '978-4777518654'");

            SelectResults results1 = (SelectResults) query1.execute();
            assertThat(results1)
                    .hasSize(1);

            List<Book> resultBooks1 = results1.asList();
            assertThat(resultBooks1.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");

            Query query2 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price = 2700");

            SelectResults results2 = (SelectResults) query2.execute();
            assertThat(results2)
                    .hasSize(1);

            List<Book> resultBooks2 = results2.asList();
            assertThat(resultBooks2.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
        }
    }

2つのクエリを投げています。また、この時にtraceを付与してQueryの実行の様子を追えるようにしています。

            Query query1 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE isbn = '978-4777518654'");

How do I debug queries?

…と、それだけでは足りないのでBytemanを持ち出したわけですが。

得られたログは、以下のとおり。「[byteman]」と書かれているのが、Bytemanでねじ込んだログです。

## ひとつ目のQuery
[info 2016/03/06 15:01:22.419 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 15:01:22.420 JST <main> tid=0x1] Query Executed in 2.748897 ms; rowCount = 1; indexesUsed(0) "<trace> SELECT * FROM /bookRegion WHERE isbn = '978-4777518654'"

## 2つ目のQuery
[info 2016/03/06 15:01:22.422 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 15:01:22.423 JST <main> tid=0x1] Query Executed in 1.765848 ms; rowCount = 1; indexesUsed(0) "<trace> SELECT * FROM /bookRegion WHERE price = 2700"

当然ですが、インデックスは作っていないのでどちらのQueryもindexesUsedが0になっています。

また、Bytemanでねじ込んでいる方は実行計画が出力されていて、evalAsFilterがtrueになるとインデックスが使われていることを示し、indexesの中に使われるインデックスが入ることになります。

Key Index & Functional Index

まずは、Key Indexから。

こちらを読むと、特性はこんな感じみたいです。
http://geode.docs.pivotal.io/docs/developing/query_index/creating_key_indexes.html

  • ソート不可(ソートしたい場合は、Functional Indexを作ること)
  • 等値比較のみで、Notは不可
  • QueryServiceは、キーと値の関係を認識していないので、Primary Keyで引く場合にも明示的にKey Indexを作成する必要がある

要は、キー項目で等値検索する場合に使えるインデックスだということです。

範囲指定などがしたかったら、Functional Indexを作れ、と。

Key IndexとFunctional Indexを使ってみる

それでは、Key IndexとFunctional Indexを定義してみます。

    @Test
    public void programmaticKeyAndNormalIndex() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, Book> region =
                    cache.<String, Book>createRegionFactory(RegionShortcut.LOCAL).create("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();
            queryService.createKeyIndex("isbnKeyIndex", "isbn", "/bookRegion");
            queryService.createIndex("priceIndex", "price", "/bookRegion");

            Query query1 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE isbn = '978-4777518654'");

            SelectResults results1 = (SelectResults) query1.execute();
            assertThat(results1)
                    .hasSize(1);

            List<Book> resultBooks1 = results1.asList();
            assertThat(resultBooks1.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");

            Query query2 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price = 2700");

            SelectResults results2 = (SelectResults) query2.execute();
            assertThat(results2)
                    .hasSize(1);

            List<Book> resultBooks2 = results2.asList();
            assertThat(resultBooks2.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
        }
    }

コードは、先ほどとほとんど一緒です。違うのは、以下の2行が入っていること。

            queryService.createKeyIndex("isbnKeyIndex", "isbn", "/bookRegion");
            queryService.createIndex("priceIndex", "price", "/bookRegion");

こちらを実行した時のログは、以下のようになります。

## Query1
[info 2016/03/06 15:01:22.580 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=isbnKeyIndex Type =PRIMARY_KEY IdxExp=isbn From=/bookRegion Proj=*]]

[info 2016/03/06 15:01:22.580 JST <main> tid=0x1] Query Executed in 0.822788 ms; rowCount = 1; indexesUsed(1):isbnKeyIndex(Results: 1) "<trace> SELECT * FROM /bookRegion WHERE isbn = '978-4777518654'"

## Query2
[info 2016/03/06 15:01:22.581 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=priceIndex Type =FUNCTIONAL IdxExp=price From=/bookRegion Proj=*]]

[info 2016/03/06 15:01:22.582 JST <main> tid=0x1] Query Executed in 0.705429 ms; rowCount = 1; indexesUsed(1):priceIndex(Results: 1) "<trace> SELECT * FROM /bookRegion WHERE price = 2700"

それぞれ、Key IndexとFunctional Indexが使われていることがわかります。

実行計画上も、そんな感じの内容が出力されていますね。evalAsFilterもtrueになりました。

  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=isbnKeyIndex Type =PRIMARY_KEY IdxExp=isbn From=/bookRegion Proj=*]]
キー項目以外のフィールドに、Key Indexを定義した場合の挙動

なんとなく動作しない感はありましたが、ものは試しにとKey Indexをキー項目以外のフィールドに作成してみました。

    @Test
    public void programmaticInvalidIndex() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, Book> region =
                    cache.<String, Book>createRegionFactory(RegionShortcut.LOCAL).create("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();
            queryService.createKeyIndex("priceKeyIndex", "price", "/bookRegion");

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price = 2700");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .isEmpty();
        }
    }

結果、見事に0件になりました。Key Indexは、ちゃんとキーに対して定義しましょうということですね。

トレース情報としては、「インデックスは使ったけど0件だったよ」みたいな挙動になります。

[info 2016/03/06 15:01:22.281 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=priceKeyIndex Type =PRIMARY_KEY IdxExp=price From=/bookRegion Proj=*]]

[info 2016/03/06 15:01:22.281 JST <main> tid=0x1] Query Executed in 0.721004 ms; rowCount = 0; indexesUsed(1):priceKeyIndex(Results: 0) "<trace> SELECT * FROM /bookRegion WHERE price = 2700"

検索条件と使ったインデックスがちぐはぐな状態なので、動きませんってことですね。

Cache XMLで定義する場合

最初のKey IndexとFunctional Indexを、Cache XMLで定義する場合の例。

        try (Cache cache = new CacheFactory().set("cache-xml-file", "cache-key-index.xml").create()) {
            Region<String, Book> region =
                    cache.<String, Book>getRegion("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();

            Query query1 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE isbn = '978-4777518654'");

            SelectResults results1 = (SelectResults) query1.execute();
            assertThat(results1)
                    .hasSize(1);

            List<Book> resultBooks1 = results1.asList();
            assertThat(resultBooks1.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");

            Query query2 = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price = 2700");

            SelectResults results2 = (SelectResults) query2.execute();
            assertThat(results2)
                    .hasSize(1);

            List<Book> resultBooks2 = results2.asList();
            assertThat(resultBooks2.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");

作成済みのRegionを取得するようにします。

Cache XMLは、こちら。
src/test/resources/cache-key-index.xml

<?xml version="1.0" encoding="UTF-8"?>
<cache
        xmlns="http://schema.pivotal.io/gemfire/cache"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://schema.pivotal.io/gemfire/cache http://schema.pivotal.io/gemfire/cache/cache-8.1.xsd"
        version="8.1">
    <region name="bookRegion" refid="LOCAL">
        <index name="isbnKeyIndex" from-clause="/bookRegion" expression="isbn" key-index="true"/>
        <index name="priceIndex" from-clause="/bookRegion" expression="price"/>
    </region>
</cache>

だいたい、Javaで定義する場合と同じような感じになりますね。

Functional Indexで範囲検索

範囲指定を行って検索する場合は、Functional Indexを使います。

Javaで定義する場合のサンプル。

    @Test
    public void programmaticIndexForRange() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, Book> region =
                    cache.<String, Book>createRegionFactory(RegionShortcut.LOCAL).create("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();
            queryService.createIndex("priceIndex", "price", "/bookRegion");

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price > 3000 AND price < 4000");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .hasSize(1);

            List<Book> resultBooks = results.asList();
            assertThat(resultBooks.get(0).getTitle())
                    .isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");
        }
    }

トレース情報は、こちら。

[info 2016/03/06 15:54:35.791 JST <main> tid=0x1] Query Executed in 5.294277 ms; rowCount = 1; indexesUsed(1):priceIndex(Results: 1) "<trace> SELECT * FROM /bookRegion WHERE price > 3000 AND price < 4000"

このQueryは、Key Indexでは動作しません。

Cache XMLでも定義してみましょう。

        try (Cache cache = new CacheFactory().set("cache-xml-file", "cache-range-index.xml").create()) {
            Region<String, Book> region =
                    cache.<String, Book>getRegion("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price > 3000 AND price < 4000");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .hasSize(1);

            List<Book> resultBooks = results.asList();
            assertThat(resultBooks.get(0).getTitle())
                    .isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");

Cache XMLは、こちら。
src/test/resources/cache-range-index.xml

<?xml version="1.0" encoding="UTF-8"?>
<cache
        xmlns="http://schema.pivotal.io/gemfire/cache"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://schema.pivotal.io/gemfire/cache http://schema.pivotal.io/gemfire/cache/cache-8.1.xsd"
        version="8.1">
    <region name="bookRegion" refid="LOCAL">
        <index name="priceIndex" from-clause="/bookRegion" expression="price" type="range"/>
    </region>
</cache>

今回はtype="range"を明示的に指定していますが、省略時のデフォルト値も"range"なので、実は指定しなくても一緒です…。

Hash Index

次は、Hash Index。

http://geode.docs.pivotal.io/docs/developing/query_index/creating_hash_indexes.html

Hash Indexを使用すると、メモリの利用効率の改善につながるそうです。通常のインデックスだと、フィールドの値をコピーしたものがインデックスに含まれるようになるみたいですね。

ただ、以下のような制限らしきものも。

  • 他のインデックスと比べると、速度は劣る可能性がある
  • 等値比較、もしくはその否定にのみ使用可能
  • インデックスのメンテナンスが、他のインデックスより遅い
  • インデックスのメンテナンスモードを、非同期実行にできない
  • 複数のIteratorやネストしたCollectionを対象には使えない
Hash Indexを定義する

とにかく使ってみましょう。まずは、Java APIで定義してみます。

    @Test
    public void programmaticHashIndex() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, Book> region =
                    cache.<String, Book>createRegionFactory(RegionShortcut.LOCAL).create("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();
            queryService.createHashIndex("priceIndex", "price", "/bookRegion");

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price = 2700");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .hasSize(1);

            List<Book> resultBooks = results.asList();
            assertThat(resultBooks.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
        }
    }

単純に、QueryService#createHashIndexするだけです。

トレース情報は、こちら。

[info 2016/03/06 16:01:00.122 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=priceIndex Type =HASH IdxExp=price From=/bookRegion Proj=*]]

[info 2016/03/06 16:01:00.122 JST <main> tid=0x1] Query Executed in 0.80871 ms; rowCount = 1; indexesUsed(1):priceIndex(Results: 1) "<trace> SELECT * FROM /bookRegion WHERE price = 2700"

ちゃんとHash Indexが使われていますね。

範囲検索で試してみる

等値比較系しかダメだよと言っているのですが、一応試してみました。

    @Test
    public void programmaticHashIndexForRange() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, Book> region =
                    cache.<String, Book>createRegionFactory(RegionShortcut.LOCAL).create("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();
            queryService.createHashIndex("priceIndex", "price", "/bookRegion");

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price > 2000 AND price < 3000");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .hasSize(1);

            List<Book> resultBooks = results.asList();
            assertThat(resultBooks.get(0).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
        }
    }

検索することはできますが、インデックスは使われていませんね。

[info 2016/03/06 16:01:00.326 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledJunction]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:01:00.328 JST <main> tid=0x1] Query Executed in 3.157187 ms; rowCount = 1; indexesUsed(0) "<trace> SELECT * FROM /bookRegion WHERE price > 2000 AND price < 3000"
Cache XMLでHash Indexを定義する

Cache XMLでもHash Indexを定義してみましょう。Java側のコードは、こちら。

         try (Cache cache = new CacheFactory().set("cache-xml-file", "cache-hash-index.xml").create()) {
             Region<String, Book> region =
                     cache.<String, Book>getRegion("bookRegion");

             getBooks().forEach(b -> region.put(b.getIsbn(), b));

             QueryService queryService = cache.getQueryService();

             Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion WHERE price = 2700");

             SelectResults results = (SelectResults) query.execute();
             assertThat(results)
                     .hasSize(1);

             List<Book> resultBooks = results.asList();
             assertThat(resultBooks.get(0).getTitle())
                     .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
         }

Cache XMLは、このように定義しています。
src/test/resources/cache-hash-index.xml

<?xml version="1.0" encoding="UTF-8"?>
<cache
        xmlns="http://schema.pivotal.io/gemfire/cache"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://schema.pivotal.io/gemfire/cache http://schema.pivotal.io/gemfire/cache/cache-8.1.xsd"
        version="8.1">
    <region name="bookRegion" refid="LOCAL">
        <index name="priceIndex" from-clause="/bookRegion" expression="price" type="hash"/>
    </region>
</cache>

typeが"hash"になります。

Joinしてみる

今度は、インデックスの種類に着目するのではなく、Joinで使われるか試してみるとしましょう。インデックスは、Cache XMLで定義します。

Key Indexで試す

まずは、キー項目でJoinすることを考え、Key Indexで試してみます。Java側のコードはこちら。

    @Test
    public void preDefinedIndexEqualJoinUnusedIndex() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().set("cache-xml-file", "cache-eq-join-unused.xml").create()) {
            Region<String, Book> region =
                    cache.<String, Book>getRegion("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion b1, /bookRegion b2 " +
                                                "WHERE b1.isbn = b2.isbn AND b2.price = 2700");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .hasSize(1);

            List<Struct> resultBooks = results.asList();
            assertThat(((Book)resultBooks.get(0).get("b1")).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
        }
    }

Cache XMLは、Key Indexで定義。
src/test/resources/cache-eq-join-unused.xml

<?xml version="1.0" encoding="UTF-8"?>
<cache
        xmlns="http://schema.pivotal.io/gemfire/cache"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://schema.pivotal.io/gemfire/cache http://schema.pivotal.io/gemfire/cache/cache-8.1.xsd"
        version="8.1">
    <region name="bookRegion" refid="LOCAL">
        <index name="isbnKeyIndex" from-clause="/bookRegion" expression="isbn" key-index="true"/>
    </region>
</cache>

ところが、このQueryではKey Indexは使われません。

[info 2016/03/06 16:11:18.217 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:11:18.218 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:11:18.218 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledJunction]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:11:18.219 JST <main> tid=0x1] Query Executed in 4.127707 ms; rowCount = 1; indexesUsed(0) "<trace> SELECT * FROM /bookRegion b1, /bookRegion b2 WHERE b1.isbn = b2.isbn AND b2.price = 2700"

ということは、Key Indexはあくまで単一の値指定で釣り上げる用途でしかダメだということですね。

Functional Indexで試す

となれば、Functional Indexで試してみましょう。Java側のコードはこちら。まあ、ほぼ変わりません。

    @Test
    public void preDefinedIndexEqualJoin() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException, IndexNameConflictException, IndexExistsException {
        try (Cache cache = new CacheFactory().set("cache-xml-file", "cache-eq-join.xml").create()) {
            Region<String, Book> region =
                    cache.<String, Book>getRegion("bookRegion");

            getBooks().forEach(b -> region.put(b.getIsbn(), b));

            QueryService queryService = cache.getQueryService();

            Query query = queryService.newQuery("<trace> SELECT * FROM /bookRegion b1, /bookRegion b2 " +
                                                "WHERE b1.isbn = b2.isbn AND b2.price = 2700");

            SelectResults results = (SelectResults) query.execute();
            assertThat(results)
                    .hasSize(1);

            List<Struct> resultBooks = results.asList();
            assertThat(((Book)resultBooks.get(0).get("b1")).getTitle())
                    .isEqualTo("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発");
        }
    }

Cache XMLは、こちら。
src/test/resources/cache-eq-join.xml

<?xml version="1.0" encoding="UTF-8"?>
<cache
        xmlns="http://schema.pivotal.io/gemfire/cache"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://schema.pivotal.io/gemfire/cache http://schema.pivotal.io/gemfire/cache/cache-8.1.xsd"
        version="8.1">
    <region name="bookRegion" refid="LOCAL">
        <index name="isbnIndex" from-clause="/bookRegion" expression="isbn"/>
    </region>
</cache>

今度は、インデックスが使われるようになります。

[info 2016/03/06 16:16:58.096 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:16:58.097 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*], Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*]]

[info 2016/03/06 16:16:58.097 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledJunction]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*], Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*]]

[info 2016/03/06 16:16:58.099 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:16:58.099 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*], Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*]]

[info 2016/03/06 16:16:58.099 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = true, isPreferred = false, indexes = [Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*], Index [ Name=isbnIndex Type =FUNCTIONAL IdxExp=isbn From=/bookRegion Proj=*]]

[info 2016/03/06 16:16:58.102 JST <main> tid=0x1] ### [byteman] [class com.gemstone.gemfire.cache.query.internal.CompiledComparison]
  => PlanInfo: evalAsFilter = false, isPreferred = false, indexes = []

[info 2016/03/06 16:16:58.107 JST <main> tid=0x1] Query Executed in 11.6957 ms; rowCount = 1; indexesUsed(1):isbnIndex(Results: 3) "<trace> SELECT * FROM /bookRegion b1, /bookRegion b2 WHERE b1.isbn = b2.isbn AND b2.price = 2700"

とここまで書きましたが、Joinとインデックスについてはこちらを。

http://geode.docs.pivotal.io/docs/developing/query_index/using_indexes_with_equijoin_queries.html

今回使わなかった機能

今回、インデックスを使っていろいろ試してみましたが、触れなかったものもあります。いくつか紹介しておきます。

ヒント句

こんな感じで、ヒント句を付与できるみたいです。

<HINT 'IDIndex'> SELECT * FROM /Portfolios p WHERE p.ID > 10 AND p.owner = 'XYZ'

ページ内リンクが貼られていないので、タイトルの部分を読んでください…

Can I instruct the query engine to use specific indexes with my queries?

もしくは、こちらを。

http://geode.docs.pivotal.io/docs/developing/query_index/query_index_hints.html

Mapフィールドに対して、インデックスを定義する

インデックス自体は通常のインデックスなのですが、expressionの指定でMapの個々の要素を指定することができます。
http://geode.docs.pivotal.io/docs/developing/query_index/creating_map_indexes.html

インデックスのメンテナンス

インデックスは通常同期的に更新されるようなのですが、これを非同期にするかどうか設定可能なようです。

http://geode.docs.pivotal.io/docs/developing/query_index/maintaining_indexes.html

Hash Indexは、これを非同期にできないってことなのでしょうね。

また、インデックスのリビルド、みたいなものはなさそうに見えます。

使ってみてハマったこと

最初、RegionをPartitionにしていたのですが、今回のコードではうまく動かず、LocalまたはReplicateにすると動作するという状態になりました。

インデックスを使うための判断の一部として、こちらの結果に依存するようなのですがまだちゃんと読めていません。
https://github.com/apache/incubator-geode/blob/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/cache/query/internal/ExecutionContext.java#L145-L161

Partitionの場合は、データのNodeごとの配置状況とか関わってくるはずなので、そのあたりの用語とコードを見ないとわかんないでしょうなぁ…。

まとめ

Apache Geodeでインデックスを定義して、実際にQueryを投げる時に使われているかどうかも含めて確認してみました。

ちょっと特性はあるみたいですが、挙動を把握できればそう迷わず使えそうな感じがしますね。

あとは、サンプルなどを見つつ試していくとよい感じかな?
http://geode.docs.pivotal.io/docs/developing/query_index/index_samples.html