シャード数を1にすべき理由
scoreによるソート
Elasticsearch Clusterでクエリのユニットテストをするときはシャード数を1にした方がいい。
特にscore
でソートしている場合は、シャード数を1にすべきである。
理由は、ユニットテストの結果が想定通りにならなかったり、テストデータを追加することで結果が変わってしまったりすることがあるから。
検証環境: Elasticsearch 7.5
scoreはシャード単位で計算される
Elasticsearchのクエリのソートで一番よく使うのはscore
の値であり、sort
に何も指定しない場合のデフォルトでもある。
しかし、score
の計算はシャードごとに行われるため、シャードごとに計算結果が異なる。score
の計算式の中に「インデックス内における単語の稀少価値」が含まれているため、1シャード内に存在している他のデータによって影響を受ける。※計算式の詳しい内容はElasticsearchのスコア計算を紐解くがわかりやすい。
そのためid
が違うだけで他が全く同じデータをElasticsearchに保存して検索しても、シャード間のデータの偏りによりscore
に差異が出てしまう。
各シャードで計算されたscore
は最終的にcoordinator nodeでまとめられてソートされるが、シャード間の差異によって人間が論理的に考えたソート順とは異なる可能性が生まれる。
ユニットテストへの影響
ユニットテストの結果が想定通りにならない可能性
用意したテストデータから論理的に考えて、「このソート順で返ってくるはず」というassertを書いても、シャード間のデータの偏りのせいで想定通りにならない可能性がある。
テストデータを追加すると結果が変わる可能性
些細な仕様変更やテストケースの追加のためにテストデータを増やすと、今までパスしていたテストが落ちる可能性がある。パスしていたテストに直接関係ないテストデータだったとしても、シャード内の他のデータによってscore
の計算結果に影響がでる可能性がある。
そのほかにもユーザー辞書に新たに単語を登録したことで、そのテストに直接関係なくてもscore
の計算結果に影響がでる可能性がある。
インデックスの設定JSONの値を書き換える
テストの時だけシャード数を1にする方法を考える。ユニットテストなのであまり上品に書く必要はなく、インデックスの設定JSONの値を書き換えて実行してしまえばいいと思う。
JSONがこのようになっているとする。
{
"settings": {
"number_of_shards": 5
以下略
Javaで以下のようなインデックス作成メソッドを本番環境作成に利用しているとする。
public void createIndex(String path) {
try {
String json = Files.readString(Paths.get(ClassLoader.getSystemResource(path).getPath()));
CreateIndexRequest request = new CreateIndexRequest(indexName).source(json, XContentType.JSON);
elasticsearchClient.indices().create(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
このメソッドの下にシャード数を引数に追加してオーバーロードしたメソッドを作り、正規表現で書き換えてしまえばいい。かなり雑なメソッドでユニットテストでしか使用すべきでないので、JetBrains annotationsをプロジェクトで利用している場合は、@org.jetbrains.annotations.TestOnly
を付与して、テスト専用であることを明示した方がいいだろう。
@TestOnly
public void createIndex(String path, int numberOfShards) {
try {
String json = Files.readString(Paths.get(ClassLoader.getSystemResource(path).getPath()));
json = json.replaceFirst("\"number_of_shard\": [0-9]+",
"\"number_of_shard\": " + numberOfShards);
CreateIndexRequest request = new CreateIndexRequest(indexName).source(json, XContentType.JSON);
elasticsearchClient.indices().create(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}