【Elasticsearch×Java】Javaで取得されたクエリの実行結果が想定と異なったので調査→対応した内容まとめ

JavaでElasticsearchを用いた検索を行った際、想定通りの結果が得られなかったので、調査→対応を行いました。
その内容をまとめておきます。
※業務中に発生した内容の覚え書きを兼ねているため、一部の名称等を代替のものに書き換えております。ご了承いただければ幸いです。

## 環境
- OS: Windows10
- Java: 11（Amazon Corretto 11）
- IDE: Eclipse 2020-03
- Elasticsearch: 7.4.2
- Kibana: 7.4.2

## 調査

### ソース
- インデックス定義
- Javaソースコード
- Kibanaでの実行クエリ（Javaソースコードと同等の内容）
はそれぞれ以下の通り。

インデックス定義では、以下の通り、mappingsの定義に加え、analyzerの設定も行っています。
フィールド名のポジショニングは以下の通り。

|フィールド名|データタイプ|ポジショニング|
|---|---|---|
|itemId|integer|商品ID（主キーに当たる）|
|itemName|text|商品名|
|itemNameKana|text|商品名（カタカナ）|
|itemNameHira|text|商品名（ひらがな）|

```json:インデックス定義
{
  "settings": {
    "analysis": {
      "filter": {
        "my_ngram": {
          "type": "ngram",
          "min_gram": 1,
          "max_gram": 2
        }
      },
      "analyzer": {
        "my_kuromoji_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "char_filter": [
            "icu_normalizer",
            "kuromoji_iteration_mark"
          ],
          "filter": [
            "kuromoji_stemmer",
            "my_ngram"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "itemId": {
        "type": "integer"
      },
      "itemName": {
        "type": "text",
        "analyzer": "my_kuromoji_analyzer"
      },
      "itemNameKana": {
        "type": "text",
        "analyzer": "my_kuromoji_analyzer"
      },
      "itemNameHira": {
        "type": "text",
        "analyzer": "my_kuromoji_analyzer"
      }
    }
  }
}
```


Java側の処理としては、入力されたされた検索ワードが商品名、商品名（カタカナ）、商品名（ひらがな）のフィールドのいずれかにマッチするものがないか検索し、スコアの高い順にソートする内容となっています。

```java:Javaソースコード
	/**
	 * 商品検索
	 *
	 * @param keyword 検索ワード
	 * @param index インデックス名
	 * @param limit 件数
	 * @param client Elasticsearch接続クライアント
	 * @return 検索結果
	 * @throws IOException
	 */
	public SearchResponse search(String keyword, String index, int limit, RestHighLevelClient client) throws IOException{

		// 検索条件の初期化
		SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

		BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

		boolQueryBuilder.should(QueryBuilders.matchQuery("itemName", keyword))
						.should(QueryBuilders.matchQuery("itemNameKana", keyword))
						.should(QueryBuilders.matchQuery("itemNameHira", keyword));

		searchSourceBuilder.query(boolQueryBuilder);
		// ソート順の設定（スコアでソート）
		searchSourceBuilder.sort(new FieldSortBuilder("_score").order(SortOrder.DESC));
		// 返却件数を設定
		searchBuilder.size(limit);

		SearchRequest request = new SearchRequest(index).source(searchSourceBuilder);

		return client.search(request, RequestOptions.DEFAULT);


	}
```

最後にKibanaでの実行クエリ。
「検索ワード」を入力となっている箇所に検索したいワードを入力して検索します。
また、size(Javaのソースコードでいうところのlimit）は5を指定。

```json:Kibanaでの実行クエリ
POST item_list/_search
{
  "from": 0,
  "size": 5,
  "sort": {
    "_score": {
      "order": "desc"
    }
  },
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "itemName": "検索ワードを入力"
           }
        },
        {
          "match": {
            "itemNameKana": "検索ワードを入力"
          }
        },
        {
          "match": {
            "itemNameHira": "検索ワードを入力"
          }
        }
      ]
    }
  }
}
```


### 候補要因

候補要因として考えられたのは以下の通り。

- 1. Javaで発行されたクエリの内容が間違っている
- 2. analyzerの設定がおかしい
- 3. 暗黙的に設定された内容を修正する必要がある

これらを順に調査していくことにしました。

### 調査実施

#### 1. Javaで発行されたクエリの内容が間違っている

まず、Javaで発行されたクエリの内容が間違っているのでは?という観点から。
確認方法としては、Javaソースコードの
SearchRequest request = new SearchRequest(index).source(searchBuilder);
の1行うしろの文にブレークポイントをはり、searchSourceBuilderの中身を見るというもの。

内容は以下の通りとなっていました。

```json:searchBuilderの中身
{"size"5,"query":{"bool":{"should":[{"match":{"itemName":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameKana":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameHira":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"_score":{"order":"desc"}}]}
```

パッと見、問題なさそう。
この内容をKibanaで実行すると、「Kibanaでの実行クエリ」をたたいた場合と全く同じ結果になりました。
実行したクエリは以下の通り。

```json:searchSourceBuilderの内容で実行したクエリ
POST item_list/_search
{"size"5,"query":{"bool":{"should":[{"match":{"itemName":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameKana":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameHira":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"_score":{"order":"desc"}}]}
```

この結果より、「Javaで発行されたクエリの内容が間違っている」は原因ではないといえます。
また、Javaでの取得結果（Elasticsearch→Javaに返却された結果：client.search(request, RequestOptions.DEFAULT)の戻り値）に対し、この段階で<b>スコア（_score）を確認したところ、Kibanaでの実行結果と異なることがわかりました。</b>（調査の中で明らかになった今回重要ポイントなので太字にしておきます）

#### 2. analyzerの設定がおかしい

続いて、analyzerの設定がおかしいのでは?という観点ですが、analyzerの設定がおかしければKibanaでクエリを実行した場合におかしな結果が出るはず。
今回それはなかったので、「analyzerの設定がおかしい」も原因ではないことがわかりました。

#### 3. 暗黙的に設定された内容を修正する必要がある

残るはこれ。
searchSourceBuilderの内容に問題がなかったので、可能性として高いのが、

- request（SearchRequest）
- client.search(request, RequestOptions.DEFAULT)（SearchResponse）

のいずれか。

デバッグモードで、request（SearchRequest）の中身を確認したところ、

```json:request（SearchRequest）の中身
SearchRequest{searchType=QUERY_THEN_FETCH, indices=[item_list], indicesOptions=IndicesOptions[ignore_unavailable=false, allow_no_indices=true, expand_wildcards_open=true, expand_wildcards_closed=false, allow_aliases_to_multiple_indices=true, forbid_closed_indices=true, ignore_aliases=false, ignore_throttled=true], types=[], routing='null', preference='null', requestCache=null, scroll=null, maxConcurrentShardRequests=0, batchedReduceSize=512, preFilterShardSize=128, allowPartialSearchResults=null, localClusterAlias=null, getOrCreateAbsoluteStartMillis=-1, ccsMinimizeRoundtrips=true, source={"size":10,"query":{"bool":{"should":[{"match":{"itemName":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameKana":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameHira":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"_score":{"order":"desc"}}]}}
```
となっていました。
source以降はsearchSourceBuilderの中身確認で確認したので、除外。

となると、大項目的に見れば、

- searchType
- indicesOptions

ですね。

searchTypeについては、

- [Search Type(Elasticsearch Reference [6.8] ) | elastic](https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-type.html)
- [Search API（Elasticsearch Reference [7.9]）| elastic](https://www.elastic.co/guide/en/elasticsearch/reference/7.9/search-search.html#search-type)
- [Enum SearchType（elasticsearch 7.4.2） | org.elasticsearch](https://www.javadoc.io/doc/org.elasticsearch/elasticsearch/7.4.2/org/elasticsearch/action/search/SearchType.html)

に記載があります。

indicesOptionsについては、

- [Class IndicesOptions（elasticsearch 7.4.2） | org.elasticsearch](https://www.javadoc.io/doc/org.elasticsearch/elasticsearch/7.4.2/org/elasticsearch/action/support/IndicesOptions.html)

と、あとはちょっと違う気がしないでもないですが、

- [Indices module（Elasticsearch Reference [7.9]） | elastic](https://www.elastic.co/guide/en/elasticsearch/reference/7.9/modules-indices.html)
- [index_options（Elasticsearch Reference [7.9]） | elastic](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-options.html)

でしょうか。

このうち、スコア（_score）に関して明記があったのがsearchTypeのほう。
(indicesOptionsについてはザっと見渡した限りではスコア（_score）に関する内容は見当たりませんでした。)

上記であげたページ、[Search Type(Elasticsearch Reference [6.8] ) | elastic](https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-type.html)の「Dfs, Query Then Fetch」の箇所に、

> ・・・more accurate scoring.

とあるので、より正確なスコアを出してくれるとみられるこちらを設定してみることに。
（「対応内容」に続く）

## 対応内容

調査結果より、SearchTypeの内容を修正する必要があるらしいことがわかったので対応していきます。
SearchRequestのsearchTypeを「Dfs, Query Then Fetch」を設定します（「追記」と記載のある箇所）。


```java:Javaソースコード（変更後）
	/**
	 * 商品検索
	 *
	 * @param keyword キーワード
	 * @param index インデックス名
	 * @param limit 件数
	 * @param client Elasticsearch接続クライアント
	 * @return 検索結果
	 * @throws IOException
	 */
	public SearchResponse search(String keyword, String index, int limit, RestHighLevelClient client) throws IOException{

		// 検索条件の初期化
		SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

		BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

		boolQueryBuilder.should(QueryBuilders.matchQuery("itemName", keyword))
						.should(QueryBuilders.matchQuery("itemNameKana", keyword))
						.should(QueryBuilders.matchQuery("itemNameHira", keyword));

		searchSourceBuilder.query(boolQueryBuilder);
		// ソート順の設定（スコアでソート）
		searchSourceBuilder.sort(new FieldSortBuilder("_score").order(SortOrder.DESC));
		// 返却件数を設定
		searchSourceBuilder.size(limit);

		SearchRequest request = new SearchRequest(index).source(searchSourceBuilder);
		// 検索タイプをDfs, Query Then Fetchに設定　　　　　　 // 追記
		request.searchType(SearchType.DFS_QUERY_THEN_FETCH); // 追記
		return client.search(request, RequestOptions.DEFAULT);


	}
```

そして、動かしてみたところ、見事想定通りの結果（Kibanaでの実行クエリと同じ）に！
無事問題解決です！！

## 結論

SearchRequestのsearchTypeを「Dfs, Query Then Fetch」としてあげることが今回の正解でした。
最初想定通りの結果が得られなくて焦りましたが、解決できてよかったです。

## 参考

本文中に出てきませんが、参考にさせていただいた記事一覧です。

- [Elasticsearchを日本語で使う設定のまとめ | Qiita](https://qiita.com/shin_hayata/items/41c07923dbf58f13eec4)
- [【elasticsearch】Kuromojiとngramで検索精度を上げてみる | Qiita](https://qiita.com/dai-maru/items/8717c9fb31bf43e1cd31)
- [Java High Level REST Client Tips | Qiita](https://qiita.com/take333/items/5792e190f4ec312d60f3)
- [TransportClientからRestHighLevelClientに移行する | Qiita](https://qiita.com/aoking_Ba/items/aa5db11afa45e9718faf)