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.0
2014年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
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する
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.rb
require 'elasticsearch_mysql_importer'
importer = ElasticsearchMysqlImporter::Importer.new
importer.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
page
使い方
50
# ネスト構造にする際に設定(オプション)
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'
page
使い方
50
# 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_file
puts 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" : [
{
page
ネスト構造化の仕組み
52
"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 skills
FROM
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",
page
ネスト構造化の仕組み
56
{
"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
ご清聴ありがとうございました。

MySQLと組み合わせて始める全文検索プロダクト"elasticsearch"