Elasticsearchで多対多&階層化されたカテゴリーを検索対象に含める方法

## 前提
以下の前提で、ひとまずローカル環境（`Rails s`）での検索を実現する

- Ruby on Rails で多対多のモデル（例としてPostとCategoryとします）を使っている
- Ancestryを使ってモデルを多階層にしている（Categoryが多階層です）
- Elasticsearch と Kibana がインストール済み
- Searchkick（Elasticsearchを簡単にするGem）を使って検索をしたい
- Postを検索するときに検索対象にCategory.nameを含めたい

`Post`と`Category`が中間テーブルを使って多対多の関係であり、Categoryだけ多階層になっている。

![データベース ER 図 .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242610/851a9a56-05ec-6360-ccc0-2594720de074.png)


### 構築がまだの方へのおすすめ記事

中間テーブル実装後にカテゴリ別ページを表示
https://qiita.com/otterminal/items/a8859a1fad0027a9bc5b

多階層カテゴリでancestryを使ったら便利すぎた
https://qiita.com/Sotq_17/items/120256209993fb05ebac

ElasticsearchとKibanaのインストール（HomebrewでOSS版をインストール）
https://qiita.com/maztak/items/0f722ad01c982a96de59

速攻で検索機能を実装できるsearchkickを調べた（Ruby）
https://qiita.com/kentosasa/items/f0af67f62f4692d68370

ただしElasticsearch、Ancestry、Searchkickなどは更新により変更が大きいので色んな方法がネットに転がっているので注意。

### 公式リポジトリ

Searchkick
https://github.com/ankane/searchkick

Ancestry
https://github.com/stefankroes/ancestry

## コード
普通に`search_data`の中で`categories.map(&:name)`としてやれば良い。

```post.rb
class Post < ApplicationRecord
    searchkick
    has_many :post_category_relations
    has_many :categories, through: :post_category_relations

    def search_data
        {
            name: name,
            description: description,
            category_name: categories.map(&:name)
        }
    end
end

```

Categoryのインデックスはまだしてないのだが、今のところ以下のようになっている。`after_commit`以降のコードはカテゴリーが更新された時にPostをReindexしてね、というコードなので、今回の件とは直接は関係しない。

```category.rb
class Category < ApplicationRecord
    has_many :post_category_relations
    has_many :posts, through: :post_category_relations
    has_ancestry

    after_commit :reindex_post

    def reindex_post
        post.reindex
    end
end

```


Searchkick開発者の回答👇

Rails: Elasticsearch :through association mapping
https://stackoverflow.com/questions/19721200/rails-elasticsearch-through-association-mapping/19751244

## Reindex は Rails Console の再起動の必要あり

あくまで開発環境（ローカル・developmentインデックス？）での話だが、Modelで`search_data`メソッドやMappingを変更した後に Rails Console を再起動せずに`Post.reindex`しても、変更前の情報でインデックスしてしまうので注意。

## Kibanaでの検証Tips

[http://localhost:5601/app/kibana#/dev_tools/console](http://localhost:5601/app/kibana#/dev_tools/console)
<img width="1440" alt="スクリーンショット 2020-07-08 15.26.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242610/07b48b9f-ed42-6af8-7445-1205d8c75f39.png">

Kibanaでのインデックス一覧やマッピング、検索結果の確認・検証において、上記コンソールを使うが、ここは（SwiftのPlaygroundのように）記述したブロックごとの実行が可能。なのでいちいちHistroyからメソッドを全部書き換えないでも、よく使うメソッドを全部貼り付けて適宜ブロックごとに実行すればいい。

```console

# kiban_console

# インデックスの削除（エイリアスでの指定は不可）
DELETE /posts_development_20200708003504586

# インデックス一覧の確認
GET /_aliases?pretty

# マッピングの確認
GET /posts_development/_mapping?pretty

# すべてのドキュメントを返す（上限100件）
GET /posts_development/_search
{
  "query": { 
    "match_all": {}
  },
  "size": 100
}

# 複数のフィールドを検索対象にして検索
GET /posts_development/_search
{
  "query": {
    "multi_match": {
      "fields": [ "name", "description", "category_name"],
      "query": "美容"
    } 
  }
}

#  
GET posts_development/_analyze
{
  "text" : "美容"
}

```

その他のメソッドはこちらを参照
- [初心者のためのElasticsearchその1](https://dev.classmethod.jp/articles/es-01/)
- [初心者のためのElasticsearchその1](https://dev.classmethod.jp/articles/es-02/)
- [Analyze API 公式ドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html)


## その他
- [Searchkick 2 nesting 'has_many'](https://stackoverrun.com/ja/q/10193643)

`as_json`を使った[関連付けされたデータの追加](https://qiita.com/akinov/items/e29c3f3802c3d7ef5ac5#%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91%E3%81%95%E3%82%8C%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E8%BF%BD%E5%8A%A0)というQiitaもあったが、これは1対多で中間テーブルがない場合の記法のようである。