searchkickのデフォルト設定では、日本語のもしかして検索ができない件を調査する



![image](https://qiita-image-store.s3.amazonaws.com/0/30079/bd6e86e1-071a-6ca3-e57c-3e6064cbb4ef.png)

[レストランデータを使った検索](http://qiita.com/wapa5pow/private/c293ac8c685aee9065f6)でsearchkickを使ったときに```Restaurant.reindex```のあとに、この検索がうまくいかなかったので、どのようにしたらうまくいくか調べてみます。

だいたいsenseで検証しています。

# 候補データ自体はどのようになっているか

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/f4e594bc-5e3e-e69e-1099-29681c131233.png)


# フィールドはどうなっているか

```json
GET restaurants_development/restaurant/_mapping

結果は以下
...
          "name": {
            "type": "string",
            "index": "not_analyzed",
            "fields": {
              "suggest": {
                "type": "string",
                "analyzer": "searchkick_suggest_index"
              },
              "word_start": {
                "type": "string",
                "analyzer": "searchkick_word_start_index"
              }
            },
            "ignore_above": 256
          },
...
```

searchkick_suggest_indexとsearchkick_word_start_indexはどうなっているかというと以下です。

```json
GET restaurants_development/_settings

結果は以下
...
            "searchkick_suggest_shingle": {
              "max_shingle_size": "5",
              "type": "shingle"
            },
            "searchkick_edge_ngram": {
              "type": "edgeNGram",
              "min_gram": "1",
              "max_gram": "50"
            },
...
            "searchkick_word_start_index": {
              "filter": [
                "lowercase",
                "asciifolding",
                "searchkick_edge_ngram"
              ],
              "type": "custom",
              "tokenizer": "standard"
            },
...
            "searchkick_suggest_index": {
              "filter": [
                "lowercase",
                "asciifolding",
                "searchkick_suggest_shingle"
              ],
              "type": "custom",
              "tokenizer": "standard"
            },
...
```

# 実際にトークンがどのように分割されているのか確かめる

## 日本語である「すきやばしこじろう」で行った場合

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/91dcd4a6-46a9-d666-810c-cf9dafe8bf28.png)

Tokenizerはstandardを使っているので日本語の場合は、１文字ずつ分かれています。Token Filterではshingleのmax_shingle_sizeが5であるため、最大でも5文字でしかトークンとして保存されていません。


## 英語である「Summer Sonic」で行った場合

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/cd63e859-5df3-1eae-7ffc-362a6e87dbe5.png)

Tokenizerはstandardでわけているので正しく単語にわかれていて意図どおりに動いています

# 実際searchkickはどのようにsuggestionを検索しているのか

pryなどで以下のように検索してみます。

```rb
Restaurant.search "すきやばしこじろう", suggest: true
```

すると以下のクエリを出していました。

```json
GET restaurants_development/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
          "match": {
            "name.word_start": {
              "query": "すきやばしこじろう",
              "boost": 10,
              "operator": "and",
              "analyzer": "searchkick_word_search"
            }
          }
        },
        {
          "match": {
            "name.word_start": {
              "query": "すきやばしこじろう",
              "boost": 1,
              "operator": "and",
              "analyzer": "searchkick_word_search",
              "fuzziness": 1,
              "prefix_length": 0,
              "max_expansions": 3,
              "fuzzy_transpositions": true
            }
          }
        }
      ]
    }
  },
  "size": 1000,
  "from": 0,
  "suggest": {
    "text": "すきやばしこじろう",
    "name": {
      "phrase": {
        "field": "name.suggest"
      }
    }
  },
  "fields": []
}
```

# 日本語でsuggestを働かせるためにはどのようにしたらいいか

[Elasticsearch キーワードサジェスト日本語のための設計](https://medium.com/hello-elasticsearch/elasticsearch-%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E3%82%B5%E3%82%B8%E3%82%A7%E3%82%B9%E3%83%88%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E8%A8%AD%E8%A8%88-352a230030dd#.irv04eiks)を参考にして、searchkickのAdvanced Mappingと、Advanced Searchを利用するといけました。

ただ、Advanced Searchはそのままではオブジェクトがかえってこないで、```Searchkick::Result```がかえってくるので```records```をみると、普通にsearchした場合と同じものがかえってきます。

Stations.reindexでindexを作った後にsense上ですが以下のようにクエリをだすと、望んだ結果を得ることができました。

```json
# クエリ
GET stations_development/station/_search
{
  "size": 3,
  "query": {
    "bool": {
      "should": [{
        "match": {
          "name.autocomplete": {
            "query": "ひがりなかの"
          }
        }
      }, {
        "match": {
          "name.readingform": {
            "query": "ひがりなかの",
            "fuzziness": "AUTO",
            "operator": "and"
          }
        }
      }]
    }
  }
}

# 結果
{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 2.3801262,
    "hits": [
      {
        "_index": "stations_development_20160831152228025",
        "_type": "station",
        "_id": "2874",
        "_score": 2.3801262,
        "_source": {
          "id": 2874,
          "pref_id": 13,
          "name": "東中野",
          "name_kana": "ひがしなかの",
          "property": "ＪＲ中央線",
          "created_at": "2016-08-21T08:17:28.000+09:00",
          "updated_at": "2016-08-21T08:17:28.000+09:00"
        }
      },
      {
        "_index": "stations_development_20160831152228025",
        "_type": "station",
        "_id": "5832",
        "_score": 2.3102236,
        "_source": {
          "id": 5832,
          "pref_id": 27,
          "name": "針中野",
          "name_kana": "はりなかの",
          "property": "近鉄南大阪線",
          "created_at": "2016-08-21T08:17:28.000+09:00",
          "updated_at": "2016-08-21T08:17:28.000+09:00"
        }
      }
    ]
  }
}
```

# 参考

## [Elasticsearch キーワードサジェスト日本語のための設計](https://medium.com/hello-elasticsearch/elasticsearch-%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E3%82%B5%E3%82%B8%E3%82%A7%E3%82%B9%E3%83%88%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E8%A8%AD%E8%A8%88-352a230030dd#.irv04eiks)

* Completion Suggesterを使わないで実装する方法の紹介
* インデックス・データ設計
    * サジェスト専用のインデックスを作成
    * サジェスト対象のデータは検索履歴単位でデータをストアする
* 検索方法
    * 1データ1ドキュメントとして入れる
    * Terms Aggregationをつかって集計・ソートする
* データの保存・削除
    * 対象が20GB超えそうなら日別や月別にインデックスを作成する
    * ドキュメントにTTLを設定したりバッチで削除してもいい
* マッピング設計: 1つのフィールドに対して３つのタイプのフィールドを使う
    * 集計表示用: 正規化のみ。Tokenは区切らない。
    * 前方一致検索用: 正規化と、edge NGram方式で前方一致用のデータ作成
    * 前方一致検索用(読み): 日本語でトークナイズして、ローマ字変換。そしてedge NGram方式で展開