Elasticsearch で分割されたトークンを全て含むドキュメントだけを検索したい

## 結論

分割されたトークンを全て含むドキュメントだけを検索したい場合、下記のようなクエリを書く事で実現できます。

```
GET operator_learning/_search
{
  "query": {
    "match": {
      "message": {
        "query": "良い天気",
        "operator": "and"  <-- ここを追加
      }
    }
  }
}
```

## Elasticsearch における複数トークンでの検索の振る舞い

Elasticsearch を使った全文検索をする中で、N-gram や形態素解析によって分割されたトークンを使って検索するのはよくあると思います。
この時 Elasticsearch のデフォルトの挙動としては、**分割されたトークンがいずれかでも一致する**ドキュメントを返します。
この挙動が適切なユースケースもあれば、それだと逆に適合率が下がってしまうケースもあります。
これは検索エンジンに求める要件によって変わるため、一概にどちらが良い・悪いとは決められません。
なので今回そこを言及することはしません。

今回は、**分割されたトークンが全て含まれている**ドキュメントだけを検索したい場合の検索クエリの書き方を紹介します。

## 準備

まず、インデックス定義を準備。
（今回は analyzer として kuromoji を使用）

```
PUT operator_learning
{
  "mappings": {
    "properties": {
      "user_name": {
        "type": "text"
      },
      "message": {
        "type": "text",
        "analyzer": "kuromoji"
      }
    }
  }
}
```

検索対象のドキュメントを用意。

```
POST operator_learning/_doc/
{
  "user_name": "鈴木 一郎",
  "message": "今日は天気がとても良いので散歩に行きました。"
}
```
```
POST operator_learning/_doc/
{
  "user_name": "佐藤 次郎",
  "message": "今日は天気が悪いので散歩は諦めました。"
}
```
```
POST operator_learning/_doc/
{
  "user_name": "田中 三郎",
  "message": "昨日の天気予報では曇と言っていた。"
}
```
```
POST operator_learning/_doc/
{
  "user_name": "中村 四郎",
  "message": "良いニュースと悪いニュースどっちから聞きたい？"
}
```

## デフォルトの動きを見てみる

では、このドキュメントに対して「**良い天気**」で検索するとどうなるか。
ちなみに今回の Mapping 定義では、キーワードとしての「良い天気」は「**良い**」「**天気**」に分割されている。

##### ※「良い天気」の analyze 結果

```
GET _analyze
{
  "text": "良い天気",
  "analyzer": "kuromoji"
}
```
```json
{
  "tokens" : [
    {
      "token" : "良い",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "天気",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    }
  ]
}
```
検索クエリはこちら。

```
GET operator_learning/_search
{
  "query": {
    "match": {
        "message": {
          "query": "良い天気"
        }
    }
  }
}
```


この場合、分割されたトークンである「良い」と「天気」のいずれかを含んでいるドキュメントがヒットするため、結果的に全てのドキュメントがヒットします。
（レスポンス一部省略）

```json
{
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 1.0121791,
    "hits" : [
      {
        "_index" : "operator_learning",
        "_score" : 1.0121791,
        "_source" : {
          "user_name" : "鈴木 一郎",
          "message" : "今日は天気がとても良いので散歩に行きました。"
        }
      },
      {
        "_index" : "operator_learning",
        "_score" : 0.6682933,
        "_source" : {
          "user_name" : "中村 四郎",
          "message" : "良いニュースと悪いニュースどっちから聞きたい？"
        }
      },
      {
        "_index" : "operator_learning",
        "_score" : 0.3704521,
        "_source" : {
          "user_name" : "佐藤 次郎",
          "message" : "今日は天気が悪いので散歩は諦めました。"
        }
      },
      {
        "_index" : "operator_learning",
        "_score" : 0.3704521,
        "_source" : {
          "user_name" : "田中 三郎",
          "message" : "昨日の天気予報では曇と言っていた。"
        }
      }
    ]
  }
}
```

しかし、このケースの場合、明らかに「良い天気」に関するドキュメントを探しているのに、天気が悪い場合のドキュメントや、そもそも天気に関係ないドキュメントまでヒットしてしまいます。

今回のサンプルドキュメントだと極端な例ですが、似たようなケースは考えられるかなと思います。

もし「天気」に関するドキュメントを検索するのであれば、きっとユーザは「天気」と検索するはずで、わざわざキーワードに「良い天気」と入力してるのに、逆の意味のドキュメントや、天気とは無関係のドキュメントはきっとヒットして欲しくないはずですよね。

## そこで解決策

「良い」「天気」の両方のトークンが含まれているドキュメントだけを検索したい場合は `operator` というパラメータ[^1] をつける事で解決できます。
（ちなみにデフォルトでは `operator` の値が `“or”` になっている）

[^1]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html#match-field-params

```
GET operator_learning/_search
{
  "query": {
    "match": {
        "message": {
          "query": "良い天気",
          "operator": "and"  <-- ここを追加
        }
    }
  }
}
```

すると、検索結果も「良い天気」に関するドキュメントだけがヒットするようになります。
（レスポンス一部省略）

```json
{
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0121791,
    "hits" : [
      {
        "_index" : "operator_learning",
        "_score" : 1.0121791,
        "_source" : {
          "user_name" : "鈴木 一郎",
          "message" : "今日は天気がとても良いので散歩に行きました。"
        }
      }
    ]
  }
}
```

いかがでしたか。
今回のケースのように、分割されたトークンを全て含むドキュメントだけを検索したい場合などはこのような検索の仕方があることを覚えておくとよさそうですね。