Slide 1

Slide 1 text

藤堂 淳也 / @junya_todo クエリとスキーマ定義の細かい話 \すごい/ Elasticsearch 2015/02/13

Slide 2

Slide 2 text

先日、静岡の焼津へ旅行してきました 資料は @junya_todo

Slide 3

Slide 3 text

すばらしいオーシャンビュー! 資料は @junya_todo

Slide 4

Slide 4 text

仕事も忘れ・・? 資料は @junya_todo

Slide 5

Slide 5 text

!!(そうだ、ES勉強会だ) 資料は @junya_todo

Slide 6

Slide 6 text

自己紹介 藤堂 淳也 株式会社ドワンゴ Twitter Qiita jtodo (@junya_todo) jtodo 検索チームに所属、ES勉強会は聴く側に5回 プラットフォーム事業部 サービス基盤開発部 汎用コアセクション

Slide 7

Slide 7 text

発 表 の テ ー マ Elasticsearchは検索基盤として運用しやすい 1 検 索 基 盤 と し て 運 用 し や す い 理 由 要件に柔軟|よく気がつく公式ドキュメント 2 要 件 対 応 | 簡 単 な ケ ー ス フィールド追加|データ受け入れ|クエリ受け入れ 4 まとめ 3 要 件 対 応 | 複 雑 な ケ ー ス クエリとスキーマ定義の細かい話|負荷検証

Slide 8

Slide 8 text

の全サービスを統合する検索基盤へ移行中 統合検索基盤は、Elasticsearchに支えられている これまでは、各サービスで個別に検索を開発していた Elasticsearch 背景

Slide 9

Slide 9 text

Elasticsearchは要件に柔軟 ...etc  新しいコンテンツが増えたから検索のフィルタを増やしたい  他サービスのデータを増やしたい ドワンゴの検索基盤は、多くのサービスと連携している 当然、運用していくなかでサービス側から要望が出てくる フィールドの追加の対応が多いため、追加が楽だと運用も楽  多様な無停止スキーマ変更の手段  RESTなので手順が書きやすい|自動化しやすい

Slide 10

Slide 10 text

よく気がつく公式ドキュメント 困りごとはオンラインの公式ガイドラインで調べれば大抵みつかる 特に検索が便利 Elasticsearch Reference Elasticsearch - The Definitive Guide 初心者から中級者向けガイド 初心者から上級者向けガイド イメージ

Slide 11

Slide 11 text

要件への対応|簡単なケース

Slide 12

Slide 12 text

要件への対応|簡単なケース フィールド 追加 データ 受け入れ 完了 検索基盤では、フィールド追加は、以下のような手順で実施する クエリ 受け入れ

Slide 13

Slide 13 text

フィールドの追加 データ長を決める int(10), varchar(128), text index.mapper.dynamic: false { } { } DDL 検索基盤スキーマ(json) ES Index Mysqlへ Elasticsearchへ PUT _mapping API PUT _settings API Rolling restart ALTER TABLE indexするか|storeだけするか 文字列型ならアナライザを決める 検索対象なのか格納するだけなのか 追加するフィールドの型を決める boolean, integer, long, float, date, string 社内製アナライザ(kuromoji移行中?) 前提として ○ フィールド追加時に決めること

Slide 14

Slide 14 text

データの受け入れ ○ データ送信フロー (niconicoの検索を支えるElasticsearch by @shoito から引用)

Slide 15

Slide 15 text

データの受け入れ ○ データ送信フロー (niconicoの検索を支えるElasticsearch by @shoito から引用) Indexer  Java api transport client で簡単に bulk API Elasticsearch  refresh_interval 15s まとまった数をキューから 取り出して、indexing 1分以内反映という 性能要件から

Slide 16

Slide 16 text

データの受け入れ ○ データ送信フロー (niconicoの検索を支えるElasticsearch by @shoito から引用) ① ② ① サービス側から新しいフィールドのデータを送信してもらう ② 検索基盤スキーマと比べて正しいフィールドのみElasticsearchへ デプロイするとElasticsearchへIndexingが開始される

Slide 17

Slide 17 text

クエリの受け入れ ○ 同様に検索基盤スキーマで管理 デプロイするとElasticsearchへのクエリに含めることができるようになる ○ 利用しているクエリ query, filter, sort, aggregations など基本的なものだけ

Slide 18

Slide 18 text

クエリの受け入れ ○ 利用しているクエリ Query match query フィルタない時に利用 filtered query フィルタある時に利用 bool query Filter term filter query filter not_analyzrdな文字列でのフィルタなど range filter and / or / not filter Sort sort / function score sort 整数、日時、not_analyzrdな文字列など Aggregations Bucketing terms aggs Metric top hits aggs max / min aggs cardinality aggs グルーピング後の件数試算

Slide 19

Slide 19 text

要件への対応|簡単なケース フィールド 追加 データ 受け入れ 完了 検索基盤では、フィールド追加は、以下のような手順で実施する クエリ 受け入れ

Slide 20

Slide 20 text

要件への対応|複雑なケース

Slide 21

Slide 21 text

要件への対応|複雑なケース フィールド 追加 データ 受け入れ 完了 フィールド追加のフローは同じだが、事前に調査や検証が必要になる 調査 要件定義 検証 クエリ 受け入れ 事前に

Slide 22

Slide 22 text

様々な要件|調査 テキスト検索のノイズを減らしたい 環境依存文字(Unicode)で検索したい ある時点の検索結果を並べ替えつつ全件取得したい フィールドの追加についての調査 クエリの受け入れについての調査

Slide 23

Slide 23 text

テキスト検索のノイズを減らしたい 『マリオ』で検索したら『リオでカーニバルが始まりました』が引っかかった マリオ出てない!ナンデ!! 起きたこと ノイズなので引っかからないようにしたい ↓

Slide 24

Slide 24 text

ドキュメントに書いてある フレーズ検索を使うと、隣り合ったトークンの位置まで比較するので、 『quick brown fox』が『quick fox』で引っかからないようにできる 意訳 ニ コ コ ニ ニ コ コ ラ ラ ボ ボ 企 企 画 boolean ○ ○ - ○ ○ phrase 1 2 3 ×  boolean – 順不同でも一致 – 位置情報不要  phrase (フレーズ検索) – 順通りで一致 – 位置情報が必須 コンテンツ「ニコニココラボ企画」の場合 ⇒ phraseならノイズが減る 調査 例)「ニコニコラボ」でbigram検索

Slide 25

Slide 25 text

ただし副作用がある phraseの副作用: booleanと比べて検索時の負荷が高い 全文検索の世界では一般的、フレーズ検索はパフォーマンスに影響する これらのコストは、単語が出現する回数によって異なり、出現回数が低い単語での 検索であれば、非常に高速という特徴がある

Slide 26

Slide 26 text

実は、それもドキュメントに書いてある Luceneのベンチマークでは、フレーズクエリは単純な term クエリの20倍遅い けど、これは termがそれほど早いということを表しているもので、 フレーズ検索のコストは、この数字で見るより怖いものではない 意訳 負荷検証したら、その通りで、よく使われるキーワードと フィールドで負荷検証したところ、負荷は問題なかった ↓

Slide 27

Slide 27 text

ただ、すんなりはいかなかった Luceneのベンチマークでは、フレーズクエリは単純な term クエリの20倍遅い けど、これは termがそれほど早いということを表しているもので、 フレーズ検索のコストは、この数字で見るより怖いものではない 意訳 けど、ステージングで試したらpositionsがないと怒られた ↓ 一体なにが・・・

Slide 28

Slide 28 text

なんと、ドキュメントに書いてある フレーズには位置情報が必要 index_optionsでインデックスサイズを削減してたのが原因 フレーズ検索が必要なく、精密なスコア必要ないのなら、 転置インデックスから、トークンの位置情報や出現数を省くことで、 インデックスサイズの節約が可能 その設定を行っていたが、ここで問題になった

Slide 29

Slide 29 text

フレーズ検索はフィールド単位で検討することにした index options 文章 ID 単語 出現 回数 位置 情報 オフ セット docs ○ freqs ○ ○ positions (default) ○ ○ ○ offsets ○ ○ ○ ○  長文の用途を検討する – スコアに影響するか – フレーズ検索を行うか – フィールド長はどの程度か  docsが可能なら削減する – Wikipedia『アニメ』の冒頭4000文字 9.7%の削減になる * サイズはデフォルトアナライザによる計測 index optionsと転置インデックスの関係

Slide 30

Slide 30 text

様々な要件|調査 テキスト検索のノイズを減らしたい 環境依存文字(Unicode)で検索したい ある時点の検索結果を並べ替えつつ全件取得したい フィールドの追加についての調査 クエリの受け入れについての調査

Slide 31

Slide 31 text

環境依存文字(Unicode)で検索したい  ユーザーは人と違うことをやりたがる – ㋷㋜㋤㊀、ココロオ㌦  シリーズものコンテンツが多い – パート①パート⑳、実況動画のまとめ㊤㊦ この手の表記ゆれの対策をすることも検索基盤として重要

Slide 32

Slide 32 text

やっぱり、ドキュメントに書いてある 調査 ドキュメントの検索に「ICU」って何気なく打ち込んだら 書いてあった!!

Slide 33

Slide 33 text

アナライザを作らなくて済んだ elasticsearch-analysis-icu ICUを用いたES用アナライザプラグイン 以下を用いてNFKC正規化を行う ICUを用いて正規化を行う 正規化で要求されているキーワードと等価性のあるコンテンツを返す やったこと icu_normalyzer filterのみ、アナライザに組み込んで利用

Slide 34

Slide 34 text

様々な要件|調査 テキスト検索のノイズを減らしたい 環境依存文字(Unicode)で検索したい ある時点の検索結果を並べ替えつつ全件取得したい フィールドの追加についての調査 クエリの受け入れについての調査

Slide 35

Slide 35 text

ある時点の検索結果を並べ替えつつ全件取得したい  以下が合わなかった 1. ソートとディープページングが必要だったが、scanではソートが実現できず、 要件に合わなかった 2. scrollはリアルタイムなユーザーリクエストのためのものではない  scanとは scanは、ディープページングのコストを安くするためにソートを無効にする検索タイプ  scrollとは scrollは、ある時点のスナップショットを作成して、結果を順次返していくAPI scan & scrollでは要件を満たせなかった

Slide 36

Slide 36 text

細かいことでもドキュメントに書いてある なぜできないのか、そもそも検索ってなんなのか 調査が必要と思ったが、それもドキュメントに書いてある 調査

Slide 37

Slide 37 text

1. リクエストをうけたノード(コーディネーティングノード)は、必要なデータを持つ シャードに対して、from+size 件を要求する 2. 要求をうけたシャードは、from+size 件のdocidとソートに使うvalueなどを返す 3. 結果を受け取ったコーディネーティングノードは、全てをマージして、グローバル ソートを行って、from~size 件のdocidを生成する from: 90, size: 10 100件ずつ 100件ずつ Query phase (Definitive Guideから引用) 「scanとは」の前に 検索タイプ query then fetch

Slide 38

Slide 38 text

1. Query phaseで生成したdocidのドキュメントを要求する 2. 要求をうけたシャードは、ドキュメントをロードしてdocidと合わせて返す 3. 全てのドキュメントがfetchされた後、クライアントに結果を返す Fetch phase 10件 合計10件 合計10件 「scanとは」の前に 検索タイプ query then fetch (Definitive Guideから引用)

Slide 39

Slide 39 text

Query phase from: 1000000, size: 10 1000010件ずつ 1000010件ずつ ディープページングを行う度に グラフが跳ねる様子 =負荷のため、通常検索での ディープページング提供は難しい 「scanとは」の前に だからディープページングは重い

Slide 40

Slide 40 text

検索タイプ scanとは(私の理解) 1. Query phase同様、コーディネーティングノードが from+size 件を要求する 2. 要求をうけたシャードは、from+size 件の文章をロードして、ソートせずに返す 3. 結果を受け取ったシャードは、ソートせずにクライアントに結果を返す (このケースでは、10件要求して20件返される) from: 1000000, size: 10 10件ずつ 10件ずつ ソートしないのでディープページングに強い検索タイプ、という理解

Slide 41

Slide 41 text

>ある時点のスナップショットを作成して、結果を順次返していくAPI search context を保持しつづける特殊なAPI。オプションに有効期限 Marvelで確認できる scrollとは

Slide 42

Slide 42 text

search contextで参照があたっているデータは残す必要がある merge & remove A D Segments A B C × scrollとは ⇒ scroll中もmergeは行うが、 removeだけ停止する

Slide 43

Slide 43 text

Segments A B C E F ・・・ scroll中はどんどんセグメントらしき(本来削除された)ものが溜まっていく よって、リアルタイムなユーザーリクエストのためのものではない、という理解 ドキュメントには、scrollは異なる構成のデータストアに大量のデータを送り込むときに使うも のであると、最初の最初に書かれている D =リソース(ファイルディスクリプタなど)の消費も増えていく scrollとは(私の理解)

Slide 44

Slide 44 text

ある時点の検索結果を並べ替えつつ全件取得したい 別のインデックスへdumpした scan & scrollでは要件を満たせなかったが、dumpで解決できた やったこと

Slide 45

Slide 45 text

要件への対応|複雑なケース フィールド 追加 データ 受け入れ 完了 フィールド追加のフローは同じだが、事前に調査や検証が必要になる 調査 要件定義 検証 クエリ 受け入れ 事前に

Slide 46

Slide 46 text

複雑な要件では、調査と同時に負荷検証を行う 負荷検証には、『es crasher』という自作の負荷検証ツールを使っている 人によって手順やツールが違う 共通して言えることは、負荷検証では、キャッシュを知ることが重要 負荷検証

Slide 47

Slide 47 text

$ curl -XPOST localhost:9200/video/_search -d '{ "query": { "match": { "title": { "query": "ゲーム", "type": "boolean", "operator": "AND", "boost": 5 } } } }' round took 1回目 3550 2回目 11 3回目 16 結果 round took 1回目 92 2回目 7 3回目 11 結果 (再起動後) なぜmatchクエリは、二回目の方が早いのか 疑問 起動直後のElasticsearchへ、matchクエリを実行したときの 検索実行時間(ms) 1

Slide 48

Slide 48 text

検索の効率化に必要なセグメントをディスクからメモリに読みこんでいる MarvelのLUCENE MEMORYは、total.segments.memory_in_bytes を描画しており、 indices _segments APIでも確認できる 二度目以降、読み込みが必要なくなり実行が早くなる 再起動後は、ファイルI/OにOSのページキャッシュが使われている Marvel なぜmatchクエリは、二回目の方が早いのか 疑問 1 メモリへのセグメントの読み込みが要因

Slide 49

Slide 49 text

$ curl -XPOST localhost:9200/video/_search?pretty -d ' { "query": { "filtered": { "query": { "match": { "title": { "query": "ゲーム", "type": "boolean", "operator": "AND", "boost": 5 } } }, "filter": { "term": { "view_counter": "0" } } } }, "sort": [ { "view_counter": { "order": "desc" } } ] }' round took 1回目 75 2回目 4 3回目 4 filter のみ結果 round took 1回目 537 2回目 4 3回目 5 sort のみ結果 なぜfilterやsortは、二回目の方が早いのか 疑問 2 そのまま Elasticsearch へ、filter と sort を実行したときの 検索実行時間(ms) 表示の都合上、同一クエリとして記載

Slide 50

Slide 50 text

Marvel なぜfilterやsortは、二回目の方が早いのか 疑問 2 filter 句ごとに filter cache を作成している 別のクエリでも、filter 句さえ同じなら、キャッシュHitする bitset で容量的にはたいしたことない filter 高速化の要因 - filter cache の生成

Slide 51

Slide 51 text

sort 対象フィールドごとに fielddataを作成している 二度目以降、fielddataが利用されるため、高速化する 別のクエリでも、ソート対象さえ同じなら、キャッシュHitする なぜfilterやsortは、二回目の方が早いのか 疑問 2 sort 高速化の要因 - fielddataの生成 Marvel 容量がでかい、作り直しに時間がかかる

Slide 52

Slide 52 text

• ソートに限らず、広く利用される – field valuesへのアクセスを必要とする問い合わせで必要 – Aggregations、geolocation filters等のフィルタ、scriptで参照したとき・・ • 構造は非転置インデックス – 転置インデックスは単語からdoc idを探すのは得意だけど、逆にdoc idから単語 を取得するのが苦手なので、キャッシュに持つ • field valuesやシャードを跨いだGlobal ordinalsという序数を持つ – 容量は大きく、文字列フィールドだと1フィールドで500MBを超えることもある • デフォルトのフォーマットでは問い合わせ時にメモリ上に作成される – メモリに置かずかつ高速なdoc valueというフォーマットもあるが、まだデフォ ルトにはなってない – こちらはLUCENE 4.0で追加されたDocValuesを利用しているとのこと – デフォルトと違いIndex時に作成されるらしい なぜfilterやsortは、二回目の方が早いのか 疑問 2 f i e l d d a t a と は

Slide 53

Slide 53 text

Segments A B C ドキュメントの更新によりキャッシュはどうなるのか 疑問 3 filter cache|fielddata は、更に細かくはセグメント単位になっている ESのドキュメントは不変。作成や更新は新しいセグメントに積まれていく 新しいセグメント (lucene flush) 3. New Query 結果をCが持ってる 1. Update Aにあった文章は削除 Cに作り直したい 2. Refresh

Slide 54

Slide 54 text

更新が発生しても、全てのキャッシュを作り直す必要はなく、新しいセグメントの キャッシュを追加すればよい Segments A B C ドキュメントの更新によりキャッシュはどうなるのか 疑問 3 filter cache|fielddata は、更に細かくはセグメント単位になっている Cだけ作り直し

Slide 55

Slide 55 text

ドキュメントの更新によりキャッシュはどうなるのか 疑問 3 refreshは、index.refresh_interval の間隔で実施 デフォルトで1s index.translog.flush_threshold_sizeのサイズを超えるとflash デフォルトで200MB index.translog.flush_threshold_periodの間隔でflash デフォルトで30m cacheを無効にすると、refresh後の問い合わせの度キャッシュが作り直される 検証として、リアルな数値が必要なときは、キャッシュを無効にしないほうがいい 更新の反映は、refresh, flashのタイミング

Slide 56

Slide 56 text

Segments 削除は文章へのマークのみ。実削除はmerge時に行われる A B C 文章1に削除マーク (lucene flush) 1. Remove Bにあった文章1は削除 2. Refresh 1 × 2... ドキュメントの削除によりキャッシュはどうなるのか 疑問 4 filter cache|fielddata は、更に細かくはセグメント単位になっている

Slide 57

Slide 57 text

Segments A B C 文章1に削除マーク (lucene flush) 1. Remove Bにあった文章1は削除 2. Refresh 3. New Query ヒット対象にならない 1 × 2... ドキュメントの削除によりキャッシュはどうなるのか 疑問 4 filter cache|fielddata は、更に細かくはセグメント単位になっている 削除は文章へのマークのみ。実削除はmerge時に行われる どうしてなの?

Slide 58

Slide 58 text

mergeされるまでは、ディスク容量も使うし、fielddataも残ることになる このスキップのために、文章が削除されたときからスループットは低くなっていく ドキュメントの削除によりキャッシュはどうなるのか 疑問 4 mergeされるまで、問い合わせ時にフラグを見てスキップしてるらしい って、最近のelasticsearch blogに書いてあった!

Slide 59

Slide 59 text

負荷検証で心にとめていること フィルタのキャッシュを無効にしない 試験前に何度か動かして温めておく 更新をかけつつ 複数シャードのインデックスで行う GCの検証も兼ねて30分くらい苛める 実際に使われるキーワードを使う ソートはランダム 〆 〆 〆 〆 〆 〆 〆 応答時間|CPU|メモリ|GCなど総合的に監視して、 問題がなければ、フィールドの追加を行う

Slide 60

Slide 60 text

Benchmark API APIで負荷検証ができる クエリの指定 リクエスト回数や繰り返し回数 キャッシュの細かい制御 ウォーミングアップ など揃ってるので、人によって違う問題は解決できるかも この機能は実験中で、 1.4.2では使えない おまけ

Slide 61

Slide 61 text

発 表 の テ ー マ Elasticsearchは検索基盤として運用しやすい 負荷検証やクエリやスキーマ定義の細かい話は、 ほとんどドキュメントに書いてあること 深い内容までドキュメントに記載されているので、 複雑な要件時の細かい挙動を調査しやすい 要件対応にはスキーマ定義が付きまとうが、 Elasticsearchはフィールド追加の対応が、とても楽 まとめ

Slide 62

Slide 62 text

ご清聴、ありがとうございました。 ご質問やご指摘ございましたら、頑張って答えます。

Slide 63

Slide 63 text

企業PR • たぶん、本物のエンジニアを募集してる • ブラック働きとかない、自由な社風 • 詳しくはホームページを見てください