Aerospikeことはじめ　～セカンダリインデックスの生成と検索～

# はじめに

AerospikeはPrimaryKeyを指定してデータを引き出すKVSだが、
条件を指定して複数Record検索もできるようなのでやってみる。

# 環境

|ソフトウェア|バージョン|
|:-:|:-:|
|OS|Windows10|
|Docker Engine|20.10.5|
|Java|11|
|Aerospike|CommunityEdition-5.5.0.9|

# セカンダリインデックス

## 概要

AerospikeでPrimaryKeyではなく、Binの値で検索を行いたい場合は
対象のBinに対して、`セカンダリインデックス`を作成する必要がある。

[公式](https://docs.aerospike.com/docs/architecture/secondary-index.html#)より引用

>セカンダリ インデックスは主キー以外のキーに存在し、一対多リレーションシップをモデル化できます。セカンダリ インデックスはビンバイビン (RDBMSカラムの場合と同様) で指定されます。これにより、インデックスの保存に必要なリソースの量を最小限に抑え、効率的な更新が可能になります。

どうやらDBでのデータの持ち方はRDBMSのいわゆるインデックスとほぼ同じっぽい。
RDBMSと異なり、このセカンダリインデックスが張られているBinに対してのみ条件指定検索が可能。
セカンダリインデックスが張られていないBinを条件に指定して検索するとエラーになる。

## 作成方法

セカンダリインデックスを作成する方法は、AQL(Aerospike Query Language)かJava等のクライアントライブラリのAPIを叩く方法がある。
今回はJavaクライアントライブラリのAPIを利用して作成してみる。
ついでにJavaでインデックスを利用した検索もする。
（AQLでやってみたら、インデックスを作成できても何故かそのインデックスが有効に働かなかった・・）

## 事前準備

Aerospikeコンテナを起動して、AQLで以下のデータを作成。
（順番がバラバラだが気にしない）

```:テストデータ
aql> SELECT * FROM test.MarioGames
+-------------------+----------+---------+
| Name              | GameHard | Release |
+-------------------+----------+---------+
| "SuperMario64"    | "N64"    | 1996    |
| "SuperMarioUSA"   | "FC"     | 1992    |
| "SuperMarioBros2" | "FC"     | 1986    |
| "SuperMarioWorld" | "SFC"    | 1990    |
| "SuperMarioLand"  | "GB"     | 1989    |
| "SuperMarioBros3" | "FC"     | 1988    |
| "SuperMarioLand2" | "GB"     | 1992    |
| "SuperMarioBros"  | "FC"     | 1985    |
+-------------------+----------+---------+
8 rows in set (0.094 secs)

OK
```

## 実装(Java)

### 1. Aerospikeに接続

とりあえず単一ノードに接続。

```Java
AerospikeClient client = new AerospikeClient("127.0.0.1", 3000);
```

### 2. セカンダリインデックス生成

```Java
String Namespace = "test";
String Set = "MarioGames";
String IndexName = "MarioGamesIDX1";
String IndexTargetBin = "GameHard";

// ①インデックス生成
IndexTask task = client.createIndex(
		null,				// Policy。nullでいいらしい
		Namespace,			// Namespace
		Set,				// Set
		IndexName,			// Index名
		IndexTargetBin,		// Index対象Bin
	    IndexType.STRING);	// 型。文字列ならSTRING、数値型ならNUMERIC

task.waitTillComplete();
```

上記コードを実行すると、セカンダリインデックスが生成されるまで待機し、
生成されれば後続コードを実行する。
ちなみに既にDBに存在するインデックス名を指定するとエラーになる。
（ので、このコードは検索の度に毎回実行するようなものではない。）

セカンダリインデックスが生成されたかどうかは、
AQLで以下のコマンドで確認できる。

```:実行結果
aql> SHOW INDEXES test
+--------+------------+-----------+--------------+-------+------------------+------------+----------+
| ns     | bin        | indextype | set          | state | indexname        | path       | type     |
+--------+------------+-----------+--------------+-------+------------------+------------+----------+
| "test" | "GameHard" | "NONE"    | "MarioGames" | "RW"  | "MarioGamesIDX1" | "GameHard" | "STRING" |
+--------+------------+-----------+--------------+-------+------------------+------------+----------+
[127.0.0.1:3000] 1 row in set (0.001 secs)

OK
```

インデックス「MarioGamesIDX1」ができた👍

### 3. クエリ生成

```Java
// ②Statementを作成
Statement stmt = new Statement();
stmt.setNamespace(Namespace);
stmt.setSetName(Set);

// ③抽出条件指定
stmt.setFilter(Filter.equal(IndexTargetBin, "FC"));
```

検索クエリとなる`Statement`を生成する。
②でNamespace、Setの指定と
③で抽出条件を指定する。
この例の場合、GameHardがファミコン(FC)のものを指定している。

検索条件の指定の仕方として、`Statement`の`setFilterメソッド`に`Filterオブジェクト`を渡す。
`Filterクラス`に`Filterオブジェクト`を生成するstaticヘルパーメソッドがいくつか用意されているので
それを利用すればよい。
`equalメソッド`の他にも`containsメソッド`や`rangeメソッド`などがある。

[Filterクラス APIドキュメント](https://docs.aerospike.com/apidocs/java/com/aerospike/client/query/Filter.html)

### 4. クエリ送信、結果取得

```Java
// ④クエリ送信
QueryPolicy qPolicy = new QueryPolicy();
RecordSet rs = client.query(qPolicy, stmt);

// ⑤結果出力
try {
    while (rs.next()) {
        Key key = rs.getKey();
        Record record = rs.getRecord();

		// RecordからBinの値を取得
		System.out.printf("Key=[%s] Name=[%s] GameHard=[%s] Release=[%d]\n",
				key,
				record.getString("Name"),
				record.getString("GameHard"),
				record.getInt("Release")
				);
    }
}
finally {
    rs.close();
}
```

④で検索クエリを送信し、
⑤で結果を受け取り、出力をしている。
`QueryPolicy`は特に指定するものが無ければnullでもいいようだ。

見てのとおり、JDBCでDBアクセスしているような感覚で実装できる。
上記のコードの実行結果は下記。

```:実行結果
Key=[test:MarioGames:null:478187846c7b97110c6e0588481e387cb77cc076] Name=[SuperMarioUSA] GameHard=[FC] Release=[1992]
Key=[test:MarioGames:null:283523796855567793a088f4cc6d0b152516812d] Name=[SuperMarioBros2] GameHard=[FC] Release=[1986]
Key=[test:MarioGames:null:50090e5fc99f5c11390d547d0b3c9504e3f0d49a] Name=[SuperMarioBros3] GameHard=[FC] Release=[1988]
Key=[test:MarioGames:null:be0e183510a456a9ce6d3d920d7a2a74ef93c1e1] Name=[SuperMarioBros] GameHard=[FC] Release=[1985]
```

ファミコンのマリオのゲームが取得できた🤞

# おわりに

今回はとりあえずここまで。
セカンダリインデックスが張られていないとBinの値を指定した絞り込み検索ができないので
システムの検索仕様とDB仕様を固めたあとにセカンダリインデックス生成AQLクエリを作って
DBで実行する流れになると思う（本来は）

セカンダリインデックス生成中はクエリを受け付けないっぽい？
ので本番などで気軽には実行できなさそう。（当然だが）

セカンダリインデックスが張られているBinの値を更新した場合、
インデックスのデータも更新されるらしい。
ここら辺もRDBMSと似ている。

# 参考

セカンダリ インデックス
https://docs.aerospike.com/docs/architecture/secondary-index.html#

Java クライアントクエリ
https://docs.aerospike.com/docs/client/java/usage/query/index.html

APIリファレンス
https://docs.aerospike.com/apidocs/java/

Aerospike で aql を使ってデータ操作を試してみる
https://qiita.com/amotz/items/b8f52e9e09bce1ddea6b