ElasticsearchのFull text queries・Term-level queriesの違いをヒット件数とともに見ていく

# 概要
この記事は、[情報検索・検索エンジン Advent Calendar 2019](https://qiita.com/advent-calendar/2019/search)の19日目の記事です。

本記事では、ElasticsearchのFull text queries・Term-level queriesの違いを紹介します。

クエリ自体の紹介はインターネット上に優良な記事・ドキュメント等があるので、今回はpythonのpandas.str.contains（特定の文字列を含む行を返すメソッド）のヒット件数とElasticsearchのクエリで検索した場合のヒット件数を比較していきます。
また、それぞれでヒットする・しないドキュメントの違いも紹介していきます。

# 目次
* この記事を書いた背景
* 使用したデータ・バージョン
* pandas.str.containsを使用した場合
* Elasticsearchの検索クエリで検索してみる
    * Full text queries
        * match query
    * Term-level queries
        * term query
* まとめ
* 参考にした記事

# この記事を書いた背景
特許検索では`すごい AND 携帯 AND ( Apple OR Android)`といったキーワードをAND/ORで結合した「検索式」というものを調査官が作成し、検索しています。
この検索式をいい感じに作ることが私の研究テーマですが、その評価指標にはヒット件数を用いなければなりません。
しかし、Elasticsearchのクエリの種類は多く、ドキュメントを読んでもイマイチピンとこない上に、
頑張って理解したつもりでもなぜかクエリごとにヒット件数が違うという事件が多発しました。（後から考えれば当然のことばかりでしたが…）
そこで、主要なクエリはどんな挙動をするか手を動かしながら確かめる必要があったので、その闘いの過程をここに記します。


# 使用したデータ・バージョン
## 入れたテキストデータ
* カーナビに関する特許データ約35,000件
    * 公開年月日：date型
    * 請求の範囲(テキストデータ)： text型(kuromojiによる形態素解析のみでindex付与を行なっている)
    
## バージョン
* Elasticsearch 7.4.0
* Python 3.6.6
* pandas 0.23.3

# pandas.str.containsを使用した場合
まずは、文字列一致による検索結果を取得してみます。
以下のようなヒット件数を返すメソッドを用意します。

```.py
import pandas as pd

def get_number_of_search_hit(word: str, df_orig: pd.Dataframe)->int:
    """
    word: 検索ワード
　　 df_orig: テキストデータを含むdataframe,　カラムの構成は['出願番号', '請求の範囲"]
    """
    _df = df_orig.query('請求の範囲.str.contains("'+word+'")', engine='python')
    return len(_df)
```
「地点」「携帯端末」を上のメソッドに入力して検索したところ、そのヒット件数は以下のようになりました。

|    | pandasの文字列一致  |
| ---- | :----: |
|  地点 |  7335  |
|  携帯端末  |  1361  |

# クエリの紹介
## Full text queries
入力した検索クエリに対してAnalyzeを行います。
例えば「携帯端末」を検索クエリとした場合「携帯/端末」に分割され、「携帯」あるいは「端末」を含む文章がヒットします。
本記事執筆時点では[9種のクエリがありますが](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/full-text-queries.html)、今回は代表的な「match query」を使ってみます。

### match query
matchクエリの例を以下に示します。

```json
{
  "query": {
    "match": {
      "請求の範囲": {
        "query": "地点"
      }
    }
  }
}
```
このクエリを使って「地点」「携帯端末」を検索すると、pandasの文字列一致検索とは違う件数がヒットします。


|    | pandasの文字列一致  | match query |
| ---- | :----: | :----: |
|  地点 |  7335  | 7309   |
|  携帯端末  |  1361  |7439 |


#### 「地点」のヒット件数が異なる原因の調査
「地点」ではpandasの文字列一致と比較して、26件ほど件数が減っています。そこで、「pandasの文字列検索結果ではヒットしたがmatchクエリではヒットしなかった文章の周辺単語」をみてみましょう。

```
# 地点(多すぎるので一部)
1件目　...後続車両の現在地点における先行車両...
2件目  ...構造物の所在地点および立体形状...
3件目  ...車両位置が前記再生音切換地点と一致...
```

kuromojiによるAnalyzeが「地点」をバラバラにしている可能性が考えられます。
そこで、Elasticsearch搭載のkuromojiで形態素解析を行うcurlコマンドを投げてみましょう。

```
$ curl -H "Content-Type: application/json" -X POST 'localhost:9200/patent/_analyze?pretty' -d '{"tokenizer" : "kuromoji_tokenizer","text": "後続車両の現在地点における先行車両"}'
```
1件目のAnalyze結果の一部を下に示します。

```json

    {
      "token" : "現在地",
      "start_offset" : 5,
      "end_offset" : 8,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "点",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "における",
      "start_offset" : 9,
      "end_offset" : 13,
      "type" : "word",
      "position" : 5
    },

```
やはり「現在地/点」と分割されてしまっています。
同様に2件目、3件目で検証すると「構造物の所在地点および立体形状」では「所在地/点」「車両位置が前記再生音切換地点と一致」では「音/切/換地/点」と分割されることがわかりました。

#### 「携帯端末」のヒット件数が異なる原因の調査
続いて、「携帯端末」についても検証していきます。
ヒット件数が倍以上増えているため、冒頭で書いた通り「携帯端末」「携帯」「端末」のいずれかを含む文章全てがヒットしていると考えられます。
そこで、「携帯」がヒットした集合と「端末」がヒットした集合の和集合を取り、その件数をみてみましょう。

```py
json_str = {'query': {'match':{'請求の範囲': {'query':'携帯'} }}}
match_keitai = get_result_patent_list(json.dumps(json_str))
print(len(match_keitai)) # 3563

json_str = {'query': {'match':{'請求の範囲': {'query':'端末'} }}}
match_tanmatsu = get_result_patent_list(json.dumps(json_str))
print(len(match_tanmatsu)) # 6076

print(len(set(match_keitai) | set(match_tanmatsu))) # 7439
```
matchクエリで「携帯端末」を検索した場合の件数と一致したので「「携帯端末」「携帯」「端末」のいずれかを含む文章全てがヒットしている」ことがわかりました。


まとめると、`自然な結果を得るにはmatchクエリは適切だが、完全一致と比較すると漏れやノイズが発生する恐れがある`ことが言えます。


## Term-level queries
### term query
term クエリの例を以下に示します。

```json
  {
    "query": {
      "term":{
        "請求の範囲": {
          "value":"地点"
        }
      }
    }
  }
```
このクエリを使って「地点」「携帯端末」を検索すると、pandasの文字列一致検索とmatchクエリによる検索とは違う件数がヒットします。

|    | pandasの文字列一致  | match query | term query|
| ---- | :----: | :----: |:----: |
|  地点 |  7335  | 7309   | 7309 |
|  携帯端末  |  1361  |7439 | 0 |

[termクエリについてのドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html)にはこのように書かれています。

> The term query does not analyze the search term. The term query only searches for the exact term you provide. This means the term query may return poor or no results when searching text fields.

部分的に日本語訳すると、「指定した正確な用語のみを検索します。」「テキストフィールドを検索するときに結果が低いか、結果を返さない場合があります。」
ということなので、
「地点」というインデックスはElasticsearchが持っているため検索はヒットしたが、「携帯端末」というインデックスは持っていないため0件を返した
ということが上の結果から考えられます。


# まとめ
pythonの文字列一致とElasticsearchのmatch/termクエリでは検索結果が結構異なることがわかりました。
pythonの文字列一致の結果により近づけたいならば、index付与時（mapping時）にn-gram解析を同時に行うと良さそうだな、と思いました。（残念ながら今回は時間切れ…）
近日中にn-gram解析した場合での結果も書きます！


# 参考にした記事
* [Elasticsearch Full Text Queries と Term Level Queries の違いを理解するだけで仲良くなれる](https://medium.com/veltra-engineering/elasticsearch-fulltext-termlevel-772e8a9152b1)
* [サービス特性にあった検索システムの設計戦略](https://techlife.cookpad.com/entry/2019/11/18/110000)