【Elasticsearch】30分でポケモンを類推する検索エンジンつくる

[エイチーム引越し侍・エイチームコネクト Advent Calendar 2019](https://qiita.com/advent-calendar/2019/hikkoshi) 、22日目は@hinoraが担当します！２回目！

## 🖥 デモ
![](https://i.gyazo.com/e073c966e0943cf7771b98983264ddf3.gif)

## 🧐 なにこれ？
名前を入力したポケモンに似ているポケモンを類推して表示します。

デモでは下記のような結果になっています。

|IN|OUT|
|:-:|:-:|
|ピカチュウ|ライチュウ, エレキッド, ピチュー|

## ⚙️ 仕組み
Elasticsearchの`More Like This Query`を利用します。

More Like This Queryを使うと「**指定したドキュメントやキーワードから同類のものを抽出**」することができます。

レコメンドとしては機械学習などを利用したものに精度は劣りますが、「おすすめの記事」や「おすすめの商品」などを**それなりの精度で手軽に実装したいケース**にぴったりです！

## ✊ 実際にやってみる

### 1. ElasticsearchとKibana準備
docker-composeで立ち上げます、結構メモリを食うのでお気をつけください。
一緒にkuromojiも入れておきます。

ボリューム用のディレクトリなどはよしなに作ってください。

<details><summary>docker-compose.yml</summary><div>

```yml:docker-compose.yml
version: '2'

services:
  elasticsearch:
    build: elasticsearch
    volumes:
      - ./docker/es/data:/usr/share/elasticsearch/data
      - ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
    ports:
      - 9200:9200
    environment:
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  kibana:
    image: docker.elastic.co/kibana/kibana:7.5.1
    ports:
      - 5601:5601

volumes:
  elasticsearch-data:
    driver: local
```

```dockerfile:elasticsearch/Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:7.5.1

RUN elasticsearch-plugin install analysis-kuromoji
```

</div></details>

### 2. データの取得と流し込み
今回はポケモンごとの以下のパラメータを利用していきます。

|パラメータ名|説明|例|
|---|---|---|
|name|名前|オオタチ|
|color|色|茶色|
|flavorText|説明文|ははおやは ほそながい からだでこどもを...|
|eggGroup|タマゴグループ|りくじょう|
|genus|○○ポケモン|どうながポケモン|

データセットがあればよかったですが、いい感じのものがなかったので[PokeAPI](https://github.com/PokeAPI/pokeapi)をクローンしてdocker-composeで立ち上げて、APIから持ってきます。

#### 取得する
JSでfetchします。

```javascript:fetch.js
/** 指定したIDのポケモンの情報を取得する
  *  - これを800くらいまで取ってきます
  *  - jsonのArrayをJSON.stringifyしながらjoin("\n")するとあとで使いやすいです
  */
const fetchPokemonById = async id => {
  const res = await fetch(`http://localhost/api/v2/pokemon-species/${id}`).then(res => res.json());
  const { flavor_text: flavorText } = findByLanguage(res.flavor_text_entries);
  const { name } = findByLanguage(res.names);
  const { genus } =  findByLanguage(res.genera);
  return {
    id,
    name,
    flavorText,
    genus,
    color: res.color.name,
    eggGroup: res.egg_groups[0].name,
  };
};
```

#### 整形して流し込む
インポートに対応する形式はJSONとCSVがありますが、今回はJSONで。

JSONと言っても普通のJSONファイルではなく、`Newline-delimited JSON`と呼ばれるものです。

この形式はその名の通り、改行で区切られた複数のJSONから構成されています。


```js:pokemons.json
{"id":1,"name":"フシギダネ","flavorText":"~~~~~~~","genus":"たねポケモン","color":"green","eggGroup":"plant"}
{"id":2,"name":"フシギソウ","flavorText":"~~~~~~~","genus":"たねポケモン","color":"green","eggGroup":"plant"}
// ...
```
Kibanaの`Machine Learning` -> `Data Visualizer` -> `Import data`へ移動して、先ほど作ったJSONファイルをインポートします。

ファイルを選択して`Import`を押すと、Indexをどうするかと聞かれるので`pokemon`というindexを作成します。

これで準備は完了です！

### 3. 類推するクエリを投げる
試しに「フシギソウ」に似たポケモンを取得してみます。
先にKibanaの`Discover`などで、ドキュメントのIDを控えておきます。

ドキュメントのIDをコピーしたら、Kibanaの`Dev Tools`から`More Like This Query`を投げてみます。

#### 類推するクエリ
`fields`に比較するパラメータ名を、likeで類推元を指定します。

```js:query
GET pokemon/_search
{
  "_source": "name", 
  "query": {
    "more_like_this": {
      "fields": [
        "name",
        "flavorText",
        "color",
        "eggGroup",
        "genus"
      ],
      "like": [
        {
          "_id": "Th1ALW8BEmAvY9Q5hJWt" // フシギソウのドキュメントID
        }
      ]
      ,
      "min_term_freq": 1,
      "max_query_terms": 12
    }
  }
}
```

#### 結果
```js:Result
{
  // ～ 省略 ～
    "hits" : [
      {
        "_index" : "pokemon",
        "_type" : "_doc",
        "_id" : "UB1ALW8BEmAvY9Q5hJWt",
        "_score" : 23.025995,
        "_source" : {
          "name" : "フシギバナ"
        }
      },
      {
        "_index" : "pokemon",
        "_type" : "_doc",
        "_id" : "DR1ALW8BEmAvY9Q5hJa0",
        "_score" : 22.985844,
        "_source" : {
          "name" : "キマワリ"
        }
      },
   // ...
```

これで「ポケモンを類推する検索エンジン」の完成です！
Webサービスなどで使う場合は好きなクライアントからリクエストして表示すればOKです。

ちなみに業務ではRailsのChewyというGemを使ってやっています。
とっても使いやすいのでオススメ！

## おまけ：ビュー側
今回はフロントから直接叩く実装にしましたが、[公式クライアント](https://www.npmjs.com/package/elasticsearch)があったので思ったより簡単でした。

```javascript:es.js
const es = require('elasticsearch');
const client = new es.Client({
  host: 'localhost:9200',
});

// 全てのドキュメントを取得するクエリを投げる
function fetchAll() {
  return client.search({
    index: 'pokemon',
    body: {
      size: 50,
      query: { match_all: {} },
    },
  });
}
```

## 完成品
![](https://i.gyazo.com/df4486b6bb0ff8e92f87ef4fdbb3ec39.png)

サンドとピカチュウは同じ**ねずみポケモン**だから、**こねずみポケモン**のピチューよりスコアが高くなっちゃってますね笑

タイプを比較パラメータ対象に入れておけば、より高精度だったかもしれません！

## まとめ
レコメンデーションと聞くと難しい印象がありますが、More Like This Queryを利用すればとっても簡単に実装できます。

また、より精度をあげるためのチューニングも`max_query_terms`などのパラメータを変更する事で一定可能です。

ぜひやってみてくださいね！

---

明日は@halktさんです！お楽しみに〜！