シャード数を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);
    }
}