【elasticsearch】python+mysqlをelasticsearchで高速化【mysql】


はじめに

普段は機械学習系を勉強しているのですが、暇な時はWeb系の技術に触れています。

今回は、研究室内に電子図書の検索エンジンを作ろうと思って取り組んでいたのですが、DBからのデータの読み込みが遅いという点が気になりました。

さくさくっと作ろうと思ったので、pythonでサーバーのスクリプトを書いてmysqlから読み込んでいたのですが、少しデータ量が多くなると描画が遅くなります。

そこで、読み込みを高速化するのにelasticsearchを導入してボトルネックになっている部分を解決しようと思いました。

覚え書き程度なので、詳しいことには触れません。

環境

  • OSX
  • python ver3.6.0
  • elasticsearch ver2.3.4
  • elasticsearch-jdbc ver2.3.4.1
  • msyql

導入

前提として、pythonでmysqlとデータのやりとりができているとします。

elasticsearchに関する導入です。
まず、elasticseach本体のインストールなのですが、2通りあります。

1つは、brewを使った方法で、

brew install elasticsearch

でインストールできます。

もう一つは公式のhttps://www.elastic.co/downloads/past-releases からダウンロードする方法です。

今回は公式からダウンロードする方法で。バージョンもお好みでいいと思いますが、今後jdbcが必要になる場合は上記の環境の組み合わせだといいかなと思います。

起動

elasticsearchを起動するのは簡単で、ダウンロードして解凍したフォルダの中でbin/elasticsearchで起動できます。
binにパスを通しておくといいと思います。

ただ、これだけだとelasticsearchにどんなデータが入っているか把握しづらいので、elasticsearch-headというツールをインストールします。

binにパスを通している場合
plugin install mobz/elasticsearch-head

でインストールできます。elasticsearchを起動した状態でhttp://localhost:9200/_plugin/head でブラウザからelasticsearch内のデータを見ることができます。

上の画像のsearch_degital_libraryは、自分が作ったインデックス(RDBでいうデータベース的な)です。
これでelsticsearchを使う準備ができました。

mysqlとの連携

このelasticsearchにmysqlのデータをマッピングするのですが、いくつかやり方があるみたいです。

①プログラムに任せる

今回はそこまでデータ量が多くなかったので、これでやりました。
方法としては、pythonでmysqlからデータ読み込んでelasticsearch用のライブラリを使って挿入するやり方です。

②jdbcを使う

elasticsearch用のjdbcをダウンロードして、専用のスクリプトで流し込む方法です。
これを使うとインデックス名がjdbcになってしまったりと、うまく扱えなかったので断念しました。

③logstashを用いる

興味ある方は調べてみてください。色々使い勝手やさそうです。

データの流し込み

ということで、mysqlのデータをelasticsearchに流し込んでいきます。

pythonからmysqlのデータ読み込みには、datasetというORMを使いました。

terminal
pip install dataset
pip install mysqlclient

多分、これで使えるようになると思います。
ついでに、接続についても簡単に。

insert_elasticsearch.py
import dataset

DBMS = 'mysql'
USER = '__user_name'
PASS = '__pass_word'
HOST = '__db_host'
DB   = 'search_degital_library'

db = dataset.connect('{0}://{1}:{2}@{3}/{4}?charset=utf8'.format(DBMS, USER, PASS, HOST, DB))

charsetの部分は適当な文字コードに直してください。

流し込みのスクリプトが以下になります。
mysqlのbooksテーブルに電子図書のid,name,created,modifiedが入ってるのでこれを読み込んで、elasticsearch用のライブラリで流し込みます。

terminal
pip install elasticsearch
insert_elasticsearch.py
~~

from elasticsearch import Elasticsearch
es = Elasticsearch("localhost:9200")

# db['接続したいテーブル名']
table_books = db['books']

for data in table_books.find():
    es.index(index="search_degital_library", doc_type="books", id=data['id'], body={"name":data['name'], "created":data['created'], "modified":data['modified']})

indexメソッドでelasticsearchにデータを挿入できます。

引数は、indexがインデックス名、doc_typeはRDBでいうテーブルにあたるのでelasticsearch内にもbooksを作ります。あらかじめindexやdoc_typeがelasticsearchになくても指定するだけで作ってくれます。
bodyには辞書型でカラムとデータを指定します。ちなみに、RDBでのカラムはelasticsearchではフィールドと呼びます。

上記を各テーブルに適用して、流し込み完了です。
データ量が多い場合はツール使った方がいいかもです。

elasticsearch-headで見ると、

Browserタブで流し込んだデータを確認できます。

次に、pythonからelasticsearchのデータ呼び出す点をまとめます。最低限の使い方って感じです。

pythonからelasticseachのデータにアクセス

データ取得

ライブラリ読み込み
from elasticsearch import Elasticsearch
es = Elasticsearch("localhost:9200")

単一レコード

上記の画像の1行をレコードと呼ぶことにします(違うかもしれませんが)。elastisearch内の1レコードを読み込むときはgetを使います。

1レコード読み込み
# categoriesのidが42のデータを読み込む
es.get(index="search_degital_library", doc_type="categories", id=42)
結果
{
    '_source': 
    {
        'created': '2017-11-21T16:50:58',
        'name': 'スクレイピング', 
        'modified': '2017-11-21T16:50:58'
    }, 
    '_version': 1, 
    '_index': 'search_degital_library', 
    '_type': 'categories', 
    'found': True, 
    '_id': '42'
}

json形式で帰ってきます。get_sourceを使うとsource内のデータだけ返ってきます。

複数レコード取得

複数のレコードを読み込むときは、searchを使います。

複数レコード読み込み
# categoriesのデータを全て読み込む
es.search(index="search_degital_library", doc_type="categories", body={"query": {"match_all": {}}})
結果
{
    '_shards': 
    {
        'failed': 0, 
        'total': 5, 
        'successful': 5
    }, 
    'took': 4, 
    'hits': 
    {
        'max_score': 1.0, 
        'total': 28, 
        'hits': 
        [{
             '_type': 'categories', 
             '_source': 
             {
                 'created': '2017-11-20T15:40:10', 
                 'name': 'データ分析', 
                 'modified': '2017-11-20T15:40:10'
             }, 
             '_score': 1.0,  
             '_index': 'search_degital_library', 
             '_id': '14'
         },
          ・・・・,
         { 
             '_source': 
             {
                 'created': '2017-11-20T15:40:10', 
                 'name': '強化学習', 
                 'modified': '2017-11-20T15:40:10'
             }, 
             '_type': 'categories', 
             '_score': 1.0, 
             '_index': 'search_degital_library', 
             '_id': '10'
         }]
    }, 
    'timed_out': False
}

これで全件読み込みできると思ったのですが、デフォルトだと10件しか読み込んでくれませんでした。searchのパラメーターにsizeがあるのでそこで指定するとその分を取得してれくます。

複数レコード読み込み
es.search(index="search_degital_library", doc_type="categories", body={"query": {"match_all": {}}, "size": 200})

最大値なのでおおめに設定するか、何かしらで件数を取得して指定するかって感じですかね。

条件取得

フィールド指定をして、複数取得します。

条件つき複数レコード読み込み
# nameが最適化のcategoriesのデータを読み込む
es.search(index="search_degital_library", doc_type="categories", body={"query": {"match": {"name":"最適化"}}}, "size"=100)

データ追加

上記でも書きましたが、

データ追加
es.index(index=インデックス名, doc_type=タイプ名, id=id_, body={"name":name_, ~})

データ削除

削除したいレコードのidを取得して、

データ追加
es.delete(index=インデックス名, doc_type=タイプ名, id=id_)

これができれば基本的な操作はできるかなと思います。

おわりに

これをボトルネック(特にデータの全件取得)となっていた部分に適用したら、描画に5秒ほどかかっていたのが1秒以内に終わるようになりました!よかった!

elasticsearchはいたるところで名前を聞いていて気になっていたので、今回触れてよかったです。

よれけば、参考にしてください。