趣味サービスにElasticsearchを導入しようとしたまとめ

# はじめに

趣味でわいわい作っているサービスがあります。このサービスに検索機能をつけたいという話になりました。

[最新の人気Webサービス・アプリが見つかる Service Safari](http://www.service-safari.com/)

このサービスは新サービスがキュレーターによって毎日投稿されて、それがアプリとかWebとかメールで見れるというサービスです。現状でも名前で検索したり、タグで検索したりはできるのですが、今回は複数タグでの検索や紹介文の全文検索機能をつけたいという要件です。MySQLだとつらそうなのでElasticsearchを使ってみることにしました。

* サービス名の部分一致で検索できる
* サービス紹介文の全文検索ができる
* 複数のタグでの検索ができる
* サービスURLのドメインで検索ができる

上記の項目で複合検索ができることをゴールとします。

まったくのElasticsearch初心者がやっていることなど間違っているところもあるかもしれません。あらかじめご了承くださいm(__)m

# VPSに環境構築

## Elasticsearchインストール

インストール自体はめちゃくちゃ簡単でyumでできます。Javaが必要です。

```/etc/yum.repos.d/elasticsearch.repo
[elasticsearch-2.x]
name=Elasticsearch repository for 2.x packages
baseurl=http://packages.elastic.co/elasticsearch/2.x/centos
gpgcheck=1
gpgkey=http://packages.elastic.co/GPG-KEY-elasticsearch
enabled=1
```

```
# yum install java
# yum install elasticsearch
```

## プラグインのインストール

Elasticsearchにはプラグインの仕組みがあり、簡単にいろん以下の3つのプラグインをインストールします。

* [analysis-kuromoji](https://github.com/elastic/elasticsearch-analysis-kuromoji): 日本語を全文検索用
* [elasticsearch-analysis-url](https://github.com/jlinn/elasticsearch-analysis-url): URL解析用
* [elasticsearch-kopf](https://github.com/lmenezes/elasticsearch-kopf): 管理画面

```
# cd /usr/share/elasticsearch/
# bin/plugin install analysis-kuromoji
# bin/plugin install lmenezes/elasticsearch-kopf/v2.1.1
# bin/plugin install https://github.com/jlinn/elasticsearch-analysis-url/releases/download/v5.0.0.0/elasticsearch-analysis-url-5.0.0.0.zip
```

バージョンなどを合わせないとインストール失敗するらしいのでgithubのページなどを参考にしつついれます。

# Elasticsearchの基礎

インストールが終わったら、Elasticsearchの基礎について学んでおきます。

## 用語や概念

[Elasticsearch システム概要 – Hello\! Elasticsearch\. – Medium](https://medium.com/hello-elasticsearch/elasticsearch-afd52d72711#.w9x78v7in)

この辺の記事を読めばなんとなくわかった気になれます。要点としては、

* Index: RDBのデータベース
* Type: RDBのテーブル
* Document: RDBのレコード

--

* Cluster: Elasticsearchのプロセスの集合体（Nodeの集まり）
* Node: Elasticsearchプロセスの1つ１つ
* Shard: Indexを物理領域的に分割したもの

--

* Filter: 与えられた文字列を前後処理するもの。例えばスペースを取り除くなど。
* Tokenizer: 与えられた文字列を分割するもの。分割した文字列でインデックスが作成される。例えばURLを受け取って、ドメインに分割するなど。
* Analyzer: FilterとTokenizerを組み合わせた処理機構。


という感じでしょうか。正直Node数やShard数をどのくらいに設定するのが適切なのかはまったくわかってません。今回試した例では、2 Nodeでやってますが、お遊びサービスなので本番では1 Nodeで運用しようかと思っています。

## Index設計について

[Elasticsearchのインデックス定義を設計する手順 \- $shibayu36\->blog;](http://blog.shibayu36.org/entry/2016/08/31/110000)

こちらの記事がとても参考になりました。要は想定される入力を、

* どのように整形するか
* どのように分割するか

みたいなことを考えてそれを設定に落とし込む作業をします。

# 設定

## Elasticsearchの設定

ザクッと書くとこんな感じになりました。

* network.bind_host: ElasticsearchがbindするIPアドレスで複数設定できる。
* network.publish_host: これは同クラスタの他のノードと通信するようのIPアドレス（たぶん）
* discovery.zen.ping.unicast.hosts: 同クラスタの他のノードのIP（もっとスマートな方法はないものか）
* index以下はアナライザーの設定になります

今回は2Nodeを想定しているので、2つのサーバに同じ設定で ```discovery.zen.ping.unicast.hosts``` だけ変更したものを設定します。つまりどころとしては、Firewallで9200と9300ポートを開けるのを忘れないこと。あと今回はグローバルIP降ってますが、通常運用の場合は開けないと思います。

``` /etc/elasticsearch/elasticsearch.yml 
cluster.name: service-safari
node.name: ss1
network.bind_host: ["グローバルIP", "プライベートIP"]
network.publish_host: "プライベートIP"
discovery.zen.ping.multicast.enabled: false
discovery.zen.ping.unicast.hosts: ["クラスタリングするサーバのプライベートIP"]

index :
    analysis :
        analyzer :
            ja :
                type : custom
                tokenizer : ja_tokenizer
                char_filter : [
                    html_strip,
                    kuromoji_iteration_mark
                ]
                filter : [
                    lowercase,
                    cjk_width,
                    katakana_stemmer,
                    kuromoji_part_of_speech
                ]
            
            ja_ngram :
                type : custom
                tokenizer : ngram_ja_tokenizer
                char_filter : [html_strip]
                filter : [
                    cjk_width,
                    lowercase
                ]
            
            url_host:
                tokenizer: url_host
                
        tokenizer :
            ja_tokenizer :
               type : kuromoji_tokenizer
               mode : search
               # user_dictionary : /etc/elasticsearch/userdict_ja.txt
            
            ngram_ja_tokenizer :
                type : nGram
                min_gram : 2
                max_gram : 3
                token_chars : [letter, digit]
            
            url_host:
                type: url
                part: host

        filter :
            katakana_stemmer :
                type : kuromoji_stemmer
```

## Index設定

```
サービスサファリという名前のIndexを作る場合
PUT /servicesafari

{
  "settings": {
    "index": {
      "number_of_shards": "3",
      "number_of_replicas": "1"
    }
  },
  "mappings": {
    "posts": {
      "_source": {
        "enabled": false
      },
      "_all": {
        "enabled": false
      },
      "properties": {
        "name": {
          "analyzer": "ja_ngram",
          "type": "string"
        },
        "tags": {
          "type": "string"
        },
        "description": {
          "analyzer": "ja_ngram",
          "type": "string"
        }, 
        "url": {
          "type": "string",
          "analyzer": "url_host"
        },
        "related_links": {
          "type": "string",
          "analyzer": "url_host"
        },
        "created_at": {
          "format": "YYYY-MM-dd HH:mm:ss",
          "type": "date"
        }
      }
    }
  }
}
```

## データの登録

```
POST /servicesafari/posts/1

{
  "created_at" : "2016-11-01 12:00:00",
  "name": "サービスサファリ - ServiceSafari",
  "description": "最新のサービスが知れるメディアです。",
  "tags": ["メディア", "ポータル"],
  "url": "http://www.service.safari.com",
  "related_links": [
    "https://www.producthunt.com/",
    "https://techcrunch.com/",
    "http://eiei19.hatenablog.com/"
  ]
}
```

## データの検索

```
POST /servicesafari/posts/_search

{
  "query":{"match":{"name":"Service"}}
}
```

この辺の記事が参考になります。

[elasticsearchとSQL対比しながら理解 \- Qiita](http://qiita.com/kieaiaarh/items/5ea4e8a188bd9814000d)
[Elasticsearchのbool queryを利用してAND OR NOTを書いてみる \- Qiita](http://qiita.com/vanhuyz/items/04a6871ae5f53ba5a97f)

## bulkでインポートとか

```requests.json
{ "create" : { "_index" : "ss", "_type" : "posts", "_id" : "1" } }
{ "created_at" : "2016-11-01 12:00:00",  "name": "サービスサファリ - ServiceSafari",  "description": "最新のサービスが知れるメディアです。",  "url": "http://www.service.safari.com",  "related_links": [    "https://www.producthunt.com/",    "https://techcrunch.com/",    "http://eiei19.hatenablog.com/"  ]}
{ "create" : { "_index" : "ss", "_type" : "posts", "_id" : "2" } }
{ "created_at" : "2016-11-01 12:00:00",  "name": "Yahoo Japan",  "description": "昔からある老舗のポータル",  "url": "http://yahoo.co.jp",  "related_links": [    "http://news.yahooo.com",    "http://nifty.com",    "http://google.co.jp"  ]}
```

```
curl -s -XPOST http://IPアドレス:9200/_bulk --data-binary "@requests.json"; echo
```

公式ドキュメントを読むべし。

[Bulk API \| Elasticsearch Reference \[5\.0\] \| Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html)

# まとめ

以上、Elasticsearchの設定をまとめてみました。あとはアプリケーション側から操作する仕組みを整えていけば使えそうな感じがします。

[最新の人気Webサービス・アプリが見つかる Service Safari](http://www.service-safari.com/)

Service Safariは趣味のサービスですが、一緒に開発やサービスグロースしてくれるメンバーを募集しています。未経験歓迎なので興味があれば @eiei19 までご連絡ください。Twitterも同じです。