match_phrase_prefix クエリを使ってみた

# はじめに
社内で行なっている勉強会で、Elasticsearchのmatch_phrase_prefixクエリに触れる機会があり、自分でも調べてみようと思いましたが日本語で説明された情報が少なかったため、自分なり調べてみました。match_phrase_prefixクエリの利用検討時に本記事がお役に立てれば嬉しいです。
間違えや気になる点などについてはご指摘いただけるとありがたいです。

# 目次
- 環境
- 公式ドキュメント
- 検証データの準備
- 検索してみる

# 環境
検証に利用した環境は、`Elasticsearch 8.5.3`です。
[こちら](https://hub.docker.com/layers/library/elasticsearch/8.5.3/images/sha256-506bc3bd5a56b624e384e3e0b8fea001f339d593a456954c3565c2e1dfe649ca)のdocker imageを利用しました。

# 公式ドキュメント
match_phrase_prefixクエリについて書かれた公式ドキュメント（8.5系）は[こちら](https://www.elastic.co/guide/en/elasticsearch/reference/8.5/query-dsl-match-query-phrase-prefix.html
)です。

# match_phrase_prefixクエリとは
簡単に説明すると、検索に指定された単語と同じ並び順で単語を含むドキュメント返します。このとき指定された最後の単語は前方一致として扱われ、その単語で始まる単語に一致するかどうかを判定しています。

本記事では、`slop`と`max_expansions`パラメータについて検証データを用いて検証したいと思います。

|パラメータ|説明|
|---|---|
|slop|一致する単語の間の最大単語数。デフォルト値は0。単語間の順序の逆転を考慮する場合はプラス２する。|
|max_expansions|クエリに指定された最後の単語を前方一致でターム辞書から取得する最大値。|

# 検証データの準備
検証データの準備するために、まずは[Create index API](https://www.elastic.co/guide/en/elasticsearch/reference/8.5/indices-create-index.html)を利用しindexを作成します。利用するindexは`my-index-1221`です。

```
# index確認
GET /_cat/indices
# すでに存在する場合は削除する
DELETE my-index-1221

# index作成
PUT /my-index-1221
{
  "settings": {
    "number_of_shards": 1
  },
  "mappings": {
    "properties": {
      "title": {
          "type": "text"
      }
    }
  }
}
```

次に[Bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/8.5/docs-bulk.html)でインデクシングします。
検索にヒットしたときの検証がやり易いように、少々違和感があるデータですがご了承ください。

```
POST /my-index-1221/_bulk
{ "index": { "_id": 1 }}
{ "title": "Tops Tshirt" }
{ "index": { "_id": 2 }}
{ "title": "Tops Tshirt long sleeve casual" }
{ "index": { "_id": 3 }}
{ "title": "Tops Tshirt short sleeve california" }
{ "index": { "_id": 4 }}
{ "title": "Tops Tshirt no sleeve canada" }
```

# 検索してみる
まずは単純に`tops tshirt`で検索してみます。
インデクシングした４件全てヒットしました。

```
# 4件ヒットする
GET /my-index-1221/_search
{
   "query": {
    "match_phrase_prefix" : {
        "title" : "tops tshirt"
    }
   }
}
```

## slop:1の場合
slop:1の場合、topsとsleeveの最大単語数が1のためヒットしません。

```
GET /my-index-1221/_search
{
"query": {
  "match_phrase_prefix" : {
    "title" : {
      "query":"tops sleeve",
      "slop":1
    }
  }
 }
}
```

## slop:2の場合
slop:2の場合、topsとsleeveの最大単語数が２のため3件ヒットします。
```
GET /my-index-1221/_search
{
"query": {
  "match_phrase_prefix" : {
    "title" : {
      "query":"tops sleeve",
      "slop":2
    }
  }
 }
}
```

## 単語をひっくり返す
クエリに指定した単語をひっくり返すとヒットするでしょうか？
```
GET /my-index-1221/_search
{
"query": {
  "match_phrase_prefix" : {
    "title" : {
      "query":"sleeve　tops",
      "slop":2
    }
  }
 }
}
```
ヒットしません。

これはクエリに指定した単語をひっくり返した順序でもヒットさせたい場合は、公式ドキュメントに記載の`Transposed terms have a slop of 2.`を考慮する必要があります。つまりslopに指定の値をプラス2する必要があるのです。[こちら](https://www.youtube.com/watch?v=V1Eo429iS9c)のYouTubeも参考になります。

そのためslop:4で検索をするとヒットするようになります。

```
GET /my-index-1221/_search
{
"query": {
  "match_phrase_prefix" : {
    "title" : {
      "query":"sleeve　tops",
      "slop":4   <-- 2ではなくて4を指定する
    }
  }
 }
}
```

# 最終単語で前方一致する
ここでは`ca`という単語を前方一致させ、ターム辞書(※)に登録されている`casual`と`california`と`canada`を前方一致で取得し、アルファベット順の先頭２つ（max_expansions:2）で検索を行います。

```
# caのワイルドカードをmax_expansionsで制御する。2ワードに制御する
GET /my-index-1221/_search
{
"query": {
  "match_phrase_prefix" : {
    "title" : {
      "query":"tops sleeve ca",
      "slop":2,
      "max_expansions": 2
    }
  }
 }
}
```

２件ヒットしました。
```
    "hits": [
      {
        "_index": "my-index-1221",
        "_id": "3",
        "_score": 1.2760415,
        "_source": {
          "title": "Tops Tshirt short sleeve california"
        }
      },
      {
        "_index": "my-index-1221",
        "_id": "4",
        "_score": 1.2760415,
        "_source": {
          "title": "Tops Tshirt no sleeve canada"
        }
      }
    ]
```

※転置インデックスに登録されている単語の一覧を指す。ターム辞書のイメージは下記です。
ターム辞書
|---|
|california|
|canada|
|casual|


# まとめ
match_phrase_prefixクエリについて、公式ドキュメントを参考に動作を検証してみました。前方一致で検索できるので何かしらで利用できそうだなというイメージを持ちつつも、日本語で利用する場合は形態素解析した単語になるので、いろいろ検証が必要になりそうですね。