minimum_should_match を使って関連性の低い結果を除外する

# はじめに

以下の記事の続きです。

https://qiita.com/s-itou/items/f5f85ae16b8143db6a9c

記事の最後で、以下の課題があると述べました。

> **関係のない結果が検索結果に表示されることがある。**

これを解決するために、**minimum_should_match** というパラメータ設定を試してみました。
パラメータの概要や利用方法、使ってみた感触などを備忘録として書き記しておきます。

# 直面していた課題

当初は以下のようなクエリを実行して、ドキュメントを検索していました。

```json-doc: GET my-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "(検索語句)",
            "fields": [
              "title.ngram^1",              
              "contents.ngram^1"
            ],
            "type": "best_fields"
          }
        }
      ],
      "should": [
        {
          "multi_match": {
            "query": "(検索語句)",
            "fields": [
              "title^3",
              "contents^3"
            ],
            "type": "best_fields"
          }
        }
      ]
    }
  }
}
```

検索漏れを防ぐためにN-gram解析(N=2) のフィールドの検索条件をmust句に設定していますが、その影響で検索ノイズが大きくなってしまうケースがありました。

例えば「Qiita」で検索すると、上記のクエリでは「Qi」「ii」「it」「ta」の少なくとも1つを含むドキュメントが検索にヒットします。
この場合、「Qiita」に全く関係がないにも関わらず、本文内のURLやサンプルコードに上記の単語が含まれているようなドキュメントが検索にヒットしてしまいます。

should句で「Qiita」を含むドキュメントが上位に表示されるようにスコアを補正してはいるのですが、**数回スクロールすると全く関係のない結果が表示されてしまう**状態で、若干の使いづらさを感じていました。

元々あった「**検索語句や検索対象のドキュメントに少々の誤字・脱字や表示揺れがあっても検索できるようにしたい**」という要求を実現しつつ、**検索結果に全く関係のない結果を除外する**方法を探していたところ、「minimum_should_match」に辿り着きました。

# minimum_should_match とは
N個以上の検索条件を満たすドキュメントのみを検索にヒットさせるためのパラメータ設定です。
どのクエリで利用するかによって振る舞いが変わるため、整理しておきます。

## Boolean query
should句内の検索条件のうち、少なくともN個の検索条件を満たすドキュメントを、検索結果に表示します。
例えば以下のクエリを実行した場合、field1・field2・field3の検索条件のうち少なくとも2つを満たすドキュメントが検索にヒットします。

```json-doc: GET my-index/_search
{
  "query": {
    "bool": {
      "should": [
        {"match": {"field1": "value1"}},
        {"match": {"field2": "value2"}},
        {"match": {"field3": "value3"}}
      ],
      "minimum_should_match": 2
    }
  }
}
```

### 参考資料

https://www.elastic.co/guide/en/elasticsearch/reference/8.13/query-dsl-bool-query.html#bool-min-should-match

## Multi-match query (```type = best_fields, most_fields```) 
text型のフィールドを検索する際に、検索にヒットした単語の数がN個以上のドキュメントを、検索結果に表示します。
例えば以下のクエリを実行した場合、「Qi」「ii」「it」「ta」のうち少なくとも3つをfield1またはfield2に含むドキュメントが検索にヒットします。

```json-doc: GET my-index/_search
{
  "query": {
    "multi_match" : {
      "query": "Qiita",
      "type": "best_fields",
      "fields": [ "field1.ngram", "field2.ngram" ],
      "minimum_should_match": 3 
    }
  }
}
```

### 参考資料

https://www.elastic.co/guide/en/elasticsearch/reference/8.13/query-dsl-multi-match-query.html#operator-min

## Multi-match query (```type = cross_field```)
検索にヒットしたフィールドの数がN個以上のドキュメントを、検索結果に表示します。
例えば以下のクエリを実行した場合、「Qi」「ii」「it」「ta」のうち少なくとも1つを含むフィールドを2つ以上持つドキュメントが検索結果にヒットします。

```json-doc: GET my-index/_search
{
  "query": {
    "multi_match" : {
      "query": "Qiita",
      "type": "cross_field",
      "fields": [ "field1.ngram", "field2.ngram", "field3.ngram" ],
      "minimum_should_match": 2 
    }
  }
}
```

### 参考資料

https://www.elastic.co/guide/en/elasticsearch/reference/8.13/query-dsl-multi-match-query.html#cross-field-analysis

## 利用可能なパラメータ

minimum_should_matchは、固定値だけでなく割合でも値を設定できます。
設定可能な値の書式の一部を、以下に示します。

| 書式 | 設定例 | 説明 |
|:-:|:-:|:-|
|正の整数|3|指定した値(個) を下限値とします。|
|負の整数|-2|合計数 - 指定した値(個) を下限値とします。|
|正の割合|75%|合計数 × 指定した割合(%) を下限値とします。|
|負の割合|-25%|合計数 × (1 - 指定した割合(%)) を下限値とします。|

### 参考資料

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html

# 試してみる

今回は、multi_matchクエリ (```type = best_field, most_field```) を使ってみました。

text型のフィールドはアナライザによる解析後の単語の数が動的に変動するため、整数ではなく割合(%)で値を指定します。

```diff_jsondoc: GET my-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "(検索語句)",
            "fields": [
              "title.ngram^1",              
              "contents.ngram^1"
            ],
            "type": "best_fields",
+           "minimum_should_match": "70%"
          }
        }
      ],
      "should": [
        {
          "multi_match": {
            "query": "(検索語句)",
            "fields": [
              "title^3",
              "contents^3"
            ],
            "type": "best_fields"
          }
        }
      ]
    }
  }
}
```

修正前に10000件以上ヒットしていた語句で検索してみたところ、141件まで減りました。
表示順は修正前と同じだったので、これでやりたかったことが実現できそうです。

# まとめ

N-gram解析のフィールドの検索条件にminimum_should_matchの設定を追加することで、関連性の低い結果を除外することができました。
設定値についてはまだ改善の余地がありそうなので、実際に利用しながら調整していこうと思います。