おいしいブログ

辞書アプリを作ろう vol.2 - Elasticsearch構築編

2017-12-31

Elasticsearch

Elasticsearch - https://www.elastic.co/products/elasticsearch

前回、辞書データの調達まで行ったのでその続きから始めます。辞書データをElasticsearchに放り込んでよしなに引いてこれればめちゃ楽やーんということで、Elasticsearchを構築します。今回はデータ取得担当のPythonとElasticsearchを同じコンテナ上に放り込むという雑な仕上がりで進めていきます。

(Elasticsearchを network.host: 0.0.0.0 で起動するとbootstrap checkが入ってファイルディスクリプタがどうのこうとか色々ちゃんと設定しないとプリプリ怒りながら落ちていった記憶があったので、ホストのEC2のリソースも少ないのでlocalhostでPythonと同梱でいいかなと思たのですが、今回は何事もなく起動したので同梱する必要もなかったですね・・?)

Elasticsearchを起動する

ベースにするDockerのイメージは、ブログでも使っている frolvlad/alpine-python3 を使って、Elasticsearchを入れていきます。今回使うElasticsearchのバージョンは6.1.1です。上記サイトのDownloadからzipを取得、任意の場所に解凍、データとログの場所をVolumeのディレクトリに変更するため、elasticsearch-6.1.1/config/elasticsearch.ymlを編集します。

#
# Path to directory where to store the data (separate multiple locations by comma):
#
#path.data: /path/to/data
path.data: /es/data
#
# Path to log files:
#
#path.logs: /path/to/logs
path.logs: /es/log

/esをVolumeにマウントします。Elasticsearchに投入したデータがここに残り、最終的にビルドしたイメージに放り込まれ、辞書データを持ったElasticsearch with Pythonのイメージが出来上がる想定です。

あとは、いくつか起動に必要な設定を行います。(Dockerfileの中に必要なコマンドの追加)

# sdd user to run es
$ adduser -D -H -s /sbin/nolgin esuser

# package update
$ apk update
$ apk add openjdk8 sudo

# add es plugins
$ bin/elasticsearch-plugin analysis-kuromoji
$ bin/elasticsearch-plugin analysis-icu

# Run Elasticsearch
$ sudo -u esuser bin/elasticsearch -d

index, mappingを作る

前回作成した日英のペアデータをそれぞれjaenのフィールドに入れて、それぞれ kuromojingramでtokenize。ひとまずこんなもので作ってみます。

{
  "settings": {
    "index": {
      "analysis" : {
        "tokenizer" : {
          "ja_tokenizer": {
            "type": "kuromoji_tokenizer",
            "mode": "search"
          },
          "en_tokenizer": {
            "type": "ngram",
            "min_gram": 1,
            "max_gram": 3,
            "token_chars": [
              "letter",
              "digit"
            ]
          }
        },
        "analyzer" : {
          "ja_analyzer" : {
            "tokenizer": "ja_tokenizer",
            "type": "custom",
            "char_filter": [
              "icu_normalizer"
            ],
            "filter": [
              "kuromoji_part_of_speech"
            ]
          },
          "en_analyzer" : {
            "tokenizer": "ja_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "en_ja": {
      "dynamic": "strict",
      "properties": {
        "en": {
          "type": "text",
          "analyzer": "en_analyzer"
        },
        "ja": {
          "type": "text",
          "analyzer": "ja_analyzer"
        }
      }
    }
  }
}

Bulk API用にデータを整形する

前回作った辞書データからBulk APIで投入できるデータを作ります。 前回のデータはこんな感じ

$ head -n 50000 edict2u.txt | tail 
因子集合 [いんししゅうごう] / factor set
因子分析 [いんしぶんせき] / factor analysis
因州弁 [いんしゅうべん] / dialects of Japanese spoken in Eastern Tottori prefecture
因習;因襲 [いんしゅう] / convention/tradition/long-established custom
因習的 [いんしゅうてき] / conventional
因習道徳 [いんしゅうどうとく] / conventional morality 
因循 [いんじゅん] / indecision/vacillation
因循姑息 [いんじゅんこそく] / dilly-dallying and temporizing 
因数 [いんすう] / factor
因数定理 [いんすうていり] / factor theorem

なので、これを

# makedata.py

# -*- coding: utf-8 -*-
import json

idx = {
  'index': {
    '_index': 'jisho',
    '_type': 'en_ja'
  }
}

with open('edict2u.txt', 'r') as r:
  for l in r:
    l = l.strip()
    d = l.split('/')
    ja = d.pop(0)
    en = '/'.join(d)
    print(json.dumps(idx))
    print(json.dumps({'ja': ja, 'en': en}))

こうして、

$ python makedata.py > data.json

こうじゃ?

head -n 100000 data.json | tail
{"index": {"_index": "jisho", "_type": "en_ja"}}
{"ja": "\u56e0\u7fd2\u9053\u5fb3 [\u3044\u3093\u3057\u3085\u3046\u3069\u3046\u3068\u304f] ", "en": " conventional morality"}
{"index": {"_index": "jisho", "_type": "en_ja"}}
{"ja": "\u56e0\u5faa [\u3044\u3093\u3058\u3085\u3093] ", "en": " indecision/vacillation"}
{"index": {"_index": "jisho", "_type": "en_ja"}}
{"ja": "\u56e0\u5faa\u59d1\u606f [\u3044\u3093\u3058\u3085\u3093\u3053\u305d\u304f] ", "en": " dilly-dallying and temporizing"}
{"index": {"_index": "jisho", "_type": "en_ja"}}
{"ja": "\u56e0\u6570 [\u3044\u3093\u3059\u3046] ", "en": " factor"}
{"index": {"_index": "jisho", "_type": "en_ja"}}
{"ja": "\u56e0\u6570\u5b9a\u7406 [\u3044\u3093\u3059\u3046\u3066\u3044\u308a] ", "en": " factor theorem"}

mappingとdataをElasticsearchに投入する

Content-Typeヘッダは6系から必須だとか。

# add mapping
$ curl -X PUT -H 'content-type: application/json' 'http://localhost:9200/jisho' -d @mapping.json

# put data
$curl -X POST -H 'content-type: application/json' 'http://localhost:9200/jisho/en_ja/_bulk' --data-binary @data.json 

検索してみる

# en to ja
$ curl -X POST -H 'content-type: application/json' 'http://localhost:9200/jisho/en_ja/_search?pretty' -d '{"query":{"match":{"en": "apple"}}}' | grep -v _type | egrep 'en|ja' 
"ja" : "林檎;苹果 [りんご;へいか;ひょうか;りゅうごう;りんきん;りんき;リンゴ] ",
"en" : " apple / apple tree /"
"ja" : "西洋林檎 [せいようりんご;セイヨウリンゴ] ",
"en" : " apple"
"ja" : "アップルソース;アップル・ソース ",
"en" : " apple sauce/apple-sauce"
"ja" : "カシューアップル;カシュー・アップル ",
"en" : " cashew apple"
"ja" : "りんご飴;林檎飴 [りんごあめ;りんごアメ] ",
"en" : " candied apple/toffee apple"
"ja" : "アップル ",
"en" : " apple/ Apple"
"ja" : "和林檎 [わりんご;ワリンゴ] ",
"en" : " Chinese apple"
"ja" : "りんご酒;リンゴ酒;林檎酒 [りんごしゅ;リンゴしゅ] ",
"en" : " apple cider"
"ja" : "アップルパイ;アップル・パイ ",
"en" : " apple pie"
"ja" : "海棠 [かいどう;カイドウ] ",
"en" : " flowering crab apple / Kaido crab apple / Chinese flowering apple / Siberian crab apple / aronia"

# ja to en
$ curl -X POST -H 'content-type: application/json' 'http://localhost:9200/jisho/en_ja/_search?pretty' -d '{"query":{"match":{"ja": "りんご"}}}' | grep -v _type | egrep 'en|ja' 
"ja" : "りんご園;林檎園 [りんごえん] ",
"en" : " apple orchard"
"ja" : "和林檎 [わりんご;ワリンゴ] ",
"en" : " Chinese apple"
"ja" : "りんご飴;林檎飴 [りんごあめ;りんごアメ] ",
"en" : " candied apple/toffee apple"
"ja" : "冬林檎 [ふゆりんご] ",
"en" : " apple sold during the winter/winter apple"
"ja" : "林檎病 [りんごびょう] ",
"en" : " slap-cheek"
"ja" : "りんご酒;リンゴ酒;林檎酒 [りんごしゅ;リンゴしゅ] ",
"en" : " apple cider"
"ja" : "リンゴの芯;りんごの芯;林檎の芯;林檎の心 [リンゴのしん;りんごのしん] ",
"en" : " apple core/core of an apple"
"ja" : "西洋林檎 [せいようりんご;セイヨウリンゴ] ",
"en" : " apple"
"ja" : "リンゴ酸;林檎酸 [リンゴさん;りんごさん] ",
"en" : " malic acid"
"ja" : "竦林檎貝 [すくみりんごがい;スクミリンゴガイ] ",
"en" : " channeled apple snail /golden apple snail"

・・・。ちょっと?雑味が漂いますが、まぁよしとしましょう?

これで聞けば答えてくれる魔法の箱がひとまず出来ました。あとはHTML, JSを書いてフロントを作り、繋ぎ目のPythonを書けばそれっぽいのが出来そうな雰囲気です。今回はここまでにして、次回公開を目指したいと思います。