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

Preview:

DESCRIPTION

2014年5月開催の最新インフラエンジニア技術勉強会での発表スライドです。 本邦初公開のelasticsearch_mysql_importerの紹介をしました。 https://github.com/y-ken/elasticsearch_mysql_importer

Citation preview

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

ご清聴ありがとうございました。