mysqlと組み合わせて始める全文検索プロダクト"elasticsearch"
DESCRIPTION
2014年5月開催の最新インフラエンジニア技術勉強会での発表スライドです。 本邦初公開のelasticsearch_mysql_importerの紹介をしました。 https://github.com/y-ken/elasticsearch_mysql_importerTRANSCRIPT
page
May, 2014
23th
MySQLと組み合わせて始める全文検索プロダクト”elasticsearch”Kentaro Yoshida in 最新インフラエンジニア技術勉強会@ドリコム
1
page
1. 自己紹介2. はじめに3. 今回のテーマ4. Yamabikoの紹介5. 新作の紹介6. まとめ
本日の流れ
2
page
1. 自己紹介
3
page
自己紹介
4
•よしけんさん• (株)リブセンス•Web系インフラの研究開発エンジニア
• elasticsearch歴:2013年 初夏~
好きなプロダクト
お知らせ
page
2. はじめに
7
page
こんなお悩みを抱えていませんか?
8
page
MySQLを利用している
9
page
だけれども、
10
page
モダンな検索機能が欲しい
11
page
インクリメンタルサーチ
12
page
ファセット検索
13
page
サジェスト機能
14
page
位置情報検索
15
page 16
ネスト構造を用いた検索
案件に紐づく最寄り駅情報等に便利(elasticsearchにあってSolrには無い機能)
page 17
RestfulなAPI
page
そして、
18
page
検索漏れが少ない日本語全文検索
“Kuromoji”を使いたい!
19
Searchモード・Extendedモードが秀逸
page
そんな時には
20
page 21
page
“elasticsearch”がいまアツいです
22
page
“elasticsearch” v1.0.02014年2月にリリース
23
page 24
page
“elasticsearch”の時代がやってきた
25
page
これは使いたい!
26
page
しかし課題が残る
27
page
“MySQL”とのデータ連携28
page
3.今回のテーマ
29
page
今回のテーマ
30
実データを用いて手軽にelasticsearchと連携した検索を行いたいelasticsearchをスモールスタートで使い始めたい既存プログラムの更新系処理に触れずに小さく始めたい
メインRDBはMySQLではあるが、検索のみelasticsearchを使う構成Amazon RDS for MySQLにも応用できる手離れの良い構成にしたい
MySQLサーバの管理無しに冗長化構成を実現できる (Multi-AZ)
page
MySQLのレコードをelasticsearchへ同期したい
31
page
つまり異種RDB間のデータ同期
32
page
そこで!
33
page
欲しいものが無いので作りました
34
page
4. Yamabiko
35
page
Yamabiko
36
https://github.com/y-ken/yamabiko
���������� ����� � ����������������� ����� � ����
����������
��� �������� ��
���� ������������������ ��
�������������
���� ������������������ ��
��������
����������� �������������������
������������ !"#$���������%&'(��")*
� �� ����������+,-./0�1�2345%6789:
�������������
���
���
������������
page
Yamabiko
37
概要MySQLからelasticsearchへデータを非同期に逐次反映Amazon RDS・MariaDB・PerconaServer等の互換DBにも対応
elasticsearchとは別の単体ミドルウェアとして動作
CentOS 6.x向けのRPMパッケージとして配布中任意のSQL文の結果の差分から、insert/update/deleteイベントを検知
SELECT * FROM contents WHERE DATE_ADD(updated_at, INTERVAL 5 MINUTE) > NOW(); といったクエリで差分同期も可能
page
Yamabiko
38
ユニークな特徴:delete検知が出来るPrimaryKeyのギャップ判定を行うことで実現行が物理削除されてしまうケースでも追従可能
数十万行単位でも動作します
なぜ更新ログ(BinaryLog)ではなくSQLの結果を同期するのか?JOIN無しで検索するnoSQL的概念に対応させるため非正規化VIEWテーブルを作ることを想定
page
Yamabikoシステム構成例
39
mysql_replicator_multi を利用する場合Yamabikoが使うメタデータを格納するためのMySQLを指定同期情報管理テーブル更新/削除判定用のハッシュテーブル
同期する行数がさほど無ければデータ参照元に相乗りしても良い
INSERT/SELECT
全文検索
page
しかし新たな課題が生まれる
40
page
Yamabikoの差分検知速度が遅い問題
41
各行のハッシュ値の比較を行うため
page
そこで作りました!
42
page
5. 新作の紹介
43
page
elasticsearch_mysql_importer
44
特徴MySQLからelasticsearchへデータを流し込む手間を最小化するツールYamabiko同様に、ドキュメントのネスト構造化が可能
Yamabikoで実現した差分検知を行い、差分更新/削除をするよりも、indexをその都度作り直し、都度完全同期する方が高速であったelasticsearchのBulk APIを利用するためのファイルを生成する機能基本的にそれだけのため、とてもシンプル
GitHub.com・ RubyGems.org にて「本日」公開!
page
elasticsearch_mysql_importer
45
https://github.com/y-ken/elasticsearch_mysql_importer
page
利用例
46
実装無しにelasticsearchにMySQLのレコードを流し込んで検索したい
1レコードに複数紐付く属性情報(最寄り駅・友達リスト)などを、ネスト構造で持たせて検索したい
非リアルタイム更新で差し支えないWebサービスでの利用
求人情報・賃貸物件情報・グルメ情報・商品情報・口コミ情報など
都度インデックスを作り直すため、小規模~中規模のWebサービスに最適
100MB / 100万件程度のデータボリュームを想定
page
indexの設計例
47
全文検索情報を更新する度に、利用するindexを切り替える手法稼働中のindexには触れずに、都度新たにindexを生成する RDBに接続先のindex名を保存してアプリ側から動的に利用する例: index名-group_a, index名-group_b の2つをローテーション 例: index名-2014.05.23_210020(2014年5月23日 21:00:20)index毎にLuceneのshardが作られるため、影響の限定化が可能(更新中の不正終了等でデータが壊れるときはindex単位のため)
page
想定システム構成
48
利用サーバをデプロイ毎に切り替える、blue-green deployment手法
page
使い方
49
# レポジトリをクローンする$ git clone https://github.com/y-ken/elasticsearch_mysql_importer.git$ cd elasticsearch_mysql_importer$ bundle install --path vendor/bundle
# exampleファイルのMySQLの接続先やクエリを書き換える$ vim example.rb
# スクリプトを実行し、Bulk APIで登録するファイルを生成$ bundle exec ruby example/example.rb
# 生成された”requests.json”をelasticsearchへPOSTする$ curl -s -XPOST localhost:9200/_bulk --data-binary @example/requests.json
page
使い方
49
# レポジトリをクローンする$ git clone https://github.com/y-ken/elasticsearch_mysql_importer.git$ cd elasticsearch_mysql_importer$ bundle install --path vendor/bundle
# exampleファイルのMySQLの接続先やクエリを書き換える$ vim example.rb
# スクリプトを実行し、Bulk APIで登録するファイルを生成$ bundle exec ruby example/example.rb
# 生成された”requests.json”をelasticsearchへPOSTする$ curl -s -XPOST localhost:9200/_bulk --data-binary @example/requests.json
page
使い方
50
$ cat example.rbrequire 'elasticsearch_mysql_importer'
importer = ElasticsearchMysqlImporter::Importer.newimporter.configure do |config| config.mysql_host = 'localhost' config.mysql_username = 'your_mysql_username' config.mysql_password = 'your_mysql_password' config.mysql_database = 'some_database'
# ネスト構造にする際に設定(オプション) config.prepared_query = 'CREATE TEMPORARY TABLE ...snip...'
# 取り込むクエリを指定(必須) config.query = 'SELECT ...'
# elasticsearchのユニークキーに使うキーを指定(必須) config.primary_key = 'member_id'
# elasticsearchに登録するindexとtypeを指定(必須) config.elasticsearch_index = 'importer_example' config.elasticsearch_type = 'member_skill'
# ファイル出力先のパスを指定(必須) config.output_file = 'requests.json'end
importer.write_fileputs importer.output_file
page
使い方
50
$ cat example.rbrequire 'elasticsearch_mysql_importer'
importer = ElasticsearchMysqlImporter::Importer.newimporter.configure do |config| config.mysql_host = 'localhost' config.mysql_username = 'your_mysql_username' config.mysql_password = 'your_mysql_password' config.mysql_database = 'some_database'
# ネスト構造にする際に設定(オプション) config.prepared_query = 'CREATE TEMPORARY TABLE ...snip...'
# 取り込むクエリを指定(必須) config.query = 'SELECT ...'
# elasticsearchのユニークキーに使うキーを指定(必須) config.primary_key = 'member_id'
# elasticsearchに登録するindexとtypeを指定(必須) config.elasticsearch_index = 'importer_example' config.elasticsearch_type = 'member_skill'
# ファイル出力先のパスを指定(必須) config.output_file = 'requests.json'end
importer.write_fileputs importer.output_file
page
使い方
50
$ cat example.rbrequire 'elasticsearch_mysql_importer'
importer = ElasticsearchMysqlImporter::Importer.newimporter.configure do |config| config.mysql_host = 'localhost' config.mysql_username = 'your_mysql_username' config.mysql_password = 'your_mysql_password' config.mysql_database = 'some_database'
# ネスト構造にする際に設定(オプション) config.prepared_query = 'CREATE TEMPORARY TABLE ...snip...'
# 取り込むクエリを指定(必須) config.query = 'SELECT ...'
# elasticsearchのユニークキーに使うキーを指定(必須) config.primary_key = 'member_id'
# elasticsearchに登録するindexとtypeを指定(必須) config.elasticsearch_index = 'importer_example' config.elasticsearch_type = 'member_skill'
# ファイル出力先のパスを指定(必須) config.output_file = 'requests.json'end
importer.write_fileputs importer.output_file
page
ネスト構造化の仕組み
51例としてこれらのテーブルを用いて解説します
page
ネスト構造化の仕組み
52
$ curl -XGET http://localhost:9200/sample/member_skill/1?pretty{ "_index" : "sample", "_type" : "member_skill", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "member_id" : 1, "member_name" : "ユーザA", "skills" : [ { "skill_name" : "PHP", "skill_url" : " /" }, { "skill_name" : "Ruby", "skill_url" : " /" } ] }}
page
ネスト構造化の仕組み
52
$ curl -XGET
{ "_index" : "sample", "_type" : "member_skill", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "member_id" : 1, "member_name" : "ユーザA", "skills" : [ { "skill_name" : "PHP", "skill_url" : "http://php.net/" }, { "skill_name" : "Ruby", "skill_url" : "https://www.ruby-lang.org/" } ] }}
page
ネスト構造化の仕組み
53
-- prepared_query設定に記述する一時テーブル作成クエリCREATE TEMPORARY TABLE tmp_member_skill SELECT members.id AS member_id, skills.name AS skill_name, skills.url AS skill_url FROM members LEFT JOIN member_skill_relation ON members.id = member_id LEFT JOIN skills ON skills.id = skill_id;
page
ネスト構造化の仕組み
53
-- prepared_query設定に記述する一時テーブル作成クエリCREATE TEMPORARY TABLE tmp_member_skill SELECT members.id AS member_id, skills.name AS skill_name, skills.url AS skill_url FROM members LEFT JOIN member_skill_relation ON members.id = member_id LEFT JOIN skills ON skills.id = skill_id;
page
ネスト構造化の仕組み
54
-- query設定に記述する、elasticsearchへ登録するドキュメントを生成するクエリ。SELECT members.id AS member_id, members.name AS member_name, "SELECT skill_name, skill_url FROM tmp_member_skill WHERE member_id = ${member_id}" AS skillsFROM members ここを展開してネスト構造化します
page
ネスト構造化の仕組み
55
-- クエリ実行結果にあるmember_idの値である1をプレースホルダに代入し、SQLクエリを実行します-- 実行結果を先ほどのskillsの値として代入しますSELECT skill_name, skill_url FROM tmp_member_skill WHERE member_id = 1
page
ネスト構造化の仕組み
56
-- skillsの中を事前に作成したテンポラリテーブルから問い合わせた結果に置き換えてネスト構造化は完了です{ "member_id" : 1, "member_name" : "ユーザA", "skills" : [ { "skill_name" : "PHP", "skill_url" : "http://php.net/" }, { "skill_name" : "Ruby", "skill_url" : "https://www.ruby-lang.org/" } ]}
page
ネスト構造化の仕組み
56
-- skillsの中を事前に作成したテンポラリテーブルから問い合わせた結果に置き換えてネスト構造化は完了です{ "member_id" : 1, "member_name" : "ユーザA", "skills" : [ { "skill_name" : "PHP", "skill_url" : "http://php.net/" }, { "skill_name" : "Ruby", "skill_url" : "https://www.ruby-lang.org/" } ]}
page
とても便利!
57
page
7. まとめ
58
page
まとめ
59
elasticsearch が本格的に使えるプロダクトへと成長した
elasticsearch_mysql_importer を使えば手軽に始められる
elasticsearch の国内トレーニングや日本語書籍もあります
page 60
http://purchases.elasticsearch.com/class/elasticsearch/core-elasticsearch/tokyo/2014-05-20
2014年7月14日~16日に開催されます
page 61
http://ascii.asciimw.jp/books/books/detail/978-4-04-866202-4.shtml
お知らせ
お知らせ
page
Thanks!
68
ご清聴ありがとうございました。