ElasticsearchのSpan Near Query

## 概要

- Elasticsearchでは、フレーズ検索用クエリとして [Match phrase query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) が用意されている
- ただ、フレーズとして指定したtermの間に別のtermが入ってしまうとヒットしなくなってしまう
- リファレンスを見たところ、[span near query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-near-query.html) でフレーズ検索やフレーズ検索に近い検索を行うことが出来そうだったので、出来るかどうか今回確認する

## span near queryの使い方

以下クエリを指定することで複数単語の距離を考慮した検索を行う。

```console:kibana(devtool)
GET _search
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "field": "value1" } },
        { "span_term": { "field": "value2" } },
        { "span_term": { "field": "value3" } }
      ],
      "slop": 12,
      "in_order": false
    }
  }
}
```

- `slop` 検索にヒットさせたいterm間の最大距離（距離はterm数）
- `in_order` 指定されたterm順で検索にヒットさせたいかどうか


このクエリの `slopに0、in_orderにtrue` を指定してすることでフレーズ検索を行うことが出来そう。
span near queryはElasitcsearch内部では、luceneの[SpanNearQuery](https://lucene.apache.org/core/8_8_2/core/org/apache/lucene/search/spans/SpanNearQuery.html) を呼び出して処理しているよう。


## 動作確認

1件だけデータを登録したインデックスを作成して、実際にspan near queryリクエストを行いヒットするかどうかを確認する。

### インデックス作成、データ投入

インデックス作成（tokenizerはkuromojiを用いる）

```console:kibana(devtool)
PUT sports_news
{
  "settings": {
    "number_of_shards": 1,
    "analysis": {
      "tokenizer": {
        "my_kuromoji_tokenizer": {
          "type": "kuromoji_tokenizer",
          "mode": "search",
          "discard_punctuation": "false"
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "tokenizer": "my_kuromoji_tokenizer"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "index": true,
        "analyzer": "my_analyzer"
      }
    }
  }
}
```

データ投入

```console:kibana(devtool)
POST sports_news/_bulk
{"index":{"_id":1}}
{"title": "大谷、メジャー初外野守備"}
```




### span_near検索

インデックスに登録したデータがヒットするかどうかを確認していく。
ここでは、ヒットしたかどうかを確認すべく[Count API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html) を用いる。

#### `slopが5, in_orderがfalse` の場合

「大谷」と「外野」の距離は3なのでヒットする。

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "title": "大谷" } },
        { "span_term": { "title": "外野" } }
      ],
      "slop": 5,
      "in_order": false
    }
  }
}
```
レスポンス

```json
{
  "count" : 1,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}
``` 

#### `slopが5, in_orderがtrue` の場合

同様にヒット

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "title": "大谷" } },
        { "span_term": { "title": "外野" } }
      ],
      "slop": 5,
      "in_order": true
    }
  }
}
```
レスポンス

```json
{
  "count" : 1,
  "_shards" : {
     :
  }
}
``` 

#### `slopが2, in_orderがtrue` の場合

slopに2を指定するとヒットしなくなる。

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "title": "大谷" } },
        { "span_term": { "title": "外野" } }
      ],
      "slop": 2,
      "in_order": true
    }
  }
}
```
レスポンス

```json
{
  "count" : 0,
  "_shards" : {
     :
  }
}
``` 

####  `slopが5, in_orderがtrue` でtermの指定を逆にした場合

先程はslopに5を, in_orderにtrueを指定した場合はヒットしたのだが、termの順番を逆にするとヒットしなくなる

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "title": "外野" } },
        { "span_term": { "title": "大谷" } }
      ],
      "slop": 5,
      "in_order": true
    }
  }
}
```
レスポンス

```json
{
  "count" : 0,
  "_shards" : {
     :
  }
}
``` 

#### フレーズ検索

以下のように `slopに0、in_orderにtrue` を指定して期待通りフレーズ検索を行うことが出来た。

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "title": "外野" } },
        { "span_term": { "title": "守備" } }
      ],
      "slop": 0,
      "in_order": true
    }
  }
}
```
レスポンス

```json
{
  "count" : 1,
  "_shards" : {
     :
  }
}
``` 

試しに、「初守備」を検索すべく以下のようなクエリで検索を行ったがヒットしなかった

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "title": "初" } },
        { "span_term": { "title": "守備" } }
      ],
      "slop": 0,
      "in_order": true
    }
  }
}
```
レスポンス

```json
{
  "count" : 0,
  "_shards" : {
     :
  }
}
``` 

#### bool queryでの確認

実際に利用するときは以下のようにbool queryで利用することもあると思われるので確認してみたが、問題なくAnd検索が行われた。

```console:kibana(devtool)
GET sports_news/_count
{
  "query": {
    "bool": {
      "must": [
        { "span_near": {
          "clauses": [
            { "span_term": { "title": "大谷" } },
            { "span_term": { "title": "外野" } }
          ],
          "slop": 3,
          "in_order": true
        } },
        { "term": {"title": "守備"} }
      ]
    }
  }
}
```

## まとめ

- span near queryを用いることでフレーズ検索やフレーズ検索のような検索を行うことが出来た
- このqueryの性能面はまだ確認していないので気になるところ。文書内での出現位置も見るようになるため、AND検索よりは遅くなると思われる
- また、[span query](https://www.elastic.co/guide/en/elasticsearch/reference/current/span-queries.html)にはspan near query以外にもqueryがあるようなので他でも同様のことが出来るか見てみたい