Elasticsearch 7.2.1 で Wildcard query を使って前方一致検索を行う際に、検索文字列に対して正規化を行う

Elasticsearch で前方一致検索を行う際に、検索文字列に正規化を行いたい時があります。
その場合、検索を実行するアプリケーション側で正規化を行うこともできますが、Elasticsearch側で正規化を行うことも可能です。

手元の環境は以下になります。

* Ubuntu 18.04 LTS
* Elasticsearch 7.2.1
* ICU Analysis Plugin

最初にインデックスの設定とマッピングを定義します。( jq コマンドで整形してます。)
Wildcard query は Term-level queries になるため、アナライザが適用されません。
そのため、Match query のようにアナライザの設定でフィルターを指定するのではなく、normalizer を直接指定する必要があります。
今回は goods データに name フィールドのみの状態を作ってみます。

```
$ curl -X PUT 'localhost:9200/shop' -H 'Content-Type: application/json' -d'
{
    "settings": {
        "index": {
            "number_of_shards": 2,
            "number_of_replicas": 1,
            "refresh_interval": "-1",
            "analysis": {
                "normalizer": {
                    "shop_normalizer": {
                        "type": "custom",
                        "char_filter": [
                            "icu_normalizer"
                        ]
                    }
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "type": {"type": "keyword"},
            "name": {
                "type": "text",
                "fields": {
                    "keyword": {
                        "type": "keyword",
                        "normalizer": "shop_normalizer",
                        "ignore_above": 256
                    }
                }
            }
        }
    }
}' | jq
```

次に、以下のようなデータで json ファイルを作成します。
カタカナ、および半角スペースが混じったデータにしてみました。

```
{"index": {"_index": "shop", "_type": "_doc","_id": "goods_id.1"}}
{"type": "goods","name": "タンカンジャム"}
{"index": {"_index": "shop", "_type": "_doc","_id": "goods_id.2"}}
{"type": "goods","name": "ジェラード 屋久島"}
```

そして、json ファイルを使ってインデクシングします。

```
$ curl -H "Content-type: application/x-ndjson" -X POST http://localhost:9200/_bulk?refresh=false --data-binary @request_bulk.json | jq
$ curl -X POST 'localhost:9200/shop/_refresh' | jq
```

全角と半角のどちらの文字列でも検索できることを確認します。

```
$ curl -X POST 'localhost:9200/shop/_search' -H 'Content-Type: application/json' -d'
{
    "from": 0,
    "size": 10,
    "query": {
        "wildcard": {
            "name.keyword": {
                "value": "タンカン*"
            }
        }
    }
}
' | jq
```

以下のように結果が返ってきます。

```
{
  "took": 9,
  "timed_out": false,
  "_shards": {
    "total": 2,
    "successful": 2,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "shop",
        "_type": "_doc",
        "_id": "goods_id.1",
        "_score": 1,
        "_source": {
          "type": "goods",
          "name": "タンカンジャム"
        }
      }
    ]
  }
}
```

```
$ curl -X POST 'localhost:9200/shop/_search' -H 'Content-Type: application/json' -d'
{
    "from": 0,
    "size": 10,
    "query": {
        "wildcard": {
            "name.keyword": {
                "value": "ﾀﾝｶﾝ*"
            }
        }
    }
}
' | jq
```

以下のように結果が返ってきます。

```
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 2,
    "successful": 2,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "shop",
        "_type": "_doc",
        "_id": "goods_id.1",
        "_score": 1,
        "_source": {
          "type": "goods",
          "name": "タンカンジャム"
        }
      }
    ]
  }
}
```

全角スペースが混じった文字列でも検索できることを確認します。

```
$ curl -X POST 'localhost:9200/shop/_search' -H 'Content-Type: application/json' -d'
{
    "from": 0,
    "size": 10,
    "query": {
        "wildcard": {
            "name.keyword": {
                "value": "ジェラード　屋久*"
            }
        }
    }
}
' | jq
```

以下のように結果が返ってきます。

```
{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 2,
    "successful": 2,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "shop",
        "_type": "_doc",
        "_id": "goods_id.2",
        "_score": 1,
        "_source": {
          "type": "goods",
          "name": "ジェラード 屋久島"
        }
      }
    ]
  }
}
```

Wildcard query を実行する際に rewrite パラメータで挙動を制御できるようですが、基本的にはデフォルトの値の方がパフォーマンスが良いようです。

### 参考になった記事
[Term-level queries](https://www.elastic.co/guide/en/elasticsearch/reference/7.2/term-level-queries.html)
[Wildcard query](https://www.elastic.co/guide/en/elasticsearch/reference/7.2/query-dsl-wildcard-query.html)