目次
※この記事は次のブログを翻訳したものになります。
原文:The Great Mapping Refactoring
Elasticsearchのユーザの悩みの最も大きなものの一つは、 タイプとフィールドのマッピングに関する多義性です。 この多義性は、インデックス時の例外やクエリ時の例外、 正しくない結果、リクエストからリクエストへ変化する結果、 また、インデックスの故障やデータのロスを結果として引き起こします。
Elasticsearchをより強固で予測可能な振る舞いをするようにする作業において、 フィールドやタイプのマッピングをより厳格でより信頼性を高くするかといったことに 多くの変更を費やしました。 多くのケースで、Elasticsearch v2.0で新しいインデックスを作るときにのみ、 新しいルールを強制し、これまでのインデックスに関しては後方互換性を保つようにします。
しかし、幾つかのケースでは、先ほど説明したようなフィールドマッピングの コンフリクトなどが存在するため、それらを利用できないです。
コンフリクトしたフィールドのマッピングをもつインデックスはElasticsearch v2.0にはアップグレードできません。
もし、これらのインデックスのデータが必要ない場合は、インデックスを消せばいいです。 そうでない場合はマッピングを正しくして再度インデックスする必要があるでしょう。
マッピングを正しく変更することは、私たちが簡単に決めることではありません。 ここからは、現在ある問題点と、私たちがどのように実装して解決したかについて説明します。
- フィールドマッピングのコンフリクト
- あいまいなフィールドのルックアップ
- タイプのメタフィールド
- アナライザ設定
index_name
とpath
- 同期的なマッピングの更新
- マッピングの削除
- 2.0のための準備
フィールドマッピングのコンフリクト
これまで、わたしたちはドキュメントのタイプは「データベースのテーブルのようなもの」と説明していました。 タイプの目的を説明する簡単な方法だったからです。 しかし、残念なことにこれは、真実ではありません。 「同じ」インデックスの「異なるタイプ」にある同じ名前のフィールドは、 内部的に、Luceneのフィールド名が同じものになります。
もしerror
フィールドとして、ドキュメントタイプがapache
のものには数値(integer)を、
ドキュメントタイプがnginx
のものには文字列(string)を割り当てた場合、
Elasticsearchは同じLuceneのフィールドに数値と文字列のデータをもつことになります。
このフィールドに対して、検索やaggregationを行う場合、おかしな結果を受け取るか、例外が帰ってくるか、
インデックスが破損することになります。
この問題を解決するために、まず、ドキュメントタイプの名前をフィールドの名前の前に追加することを考えました。 各フィールドは完全に別のものとなります。 このアプローチの利点はドキュメントタイプが実際のテーブルのようになることです。
しかし、この方法には多くの欠点があります。
- フィールドは常に、他のタイプとは異なるものであると区別するためもしくは、複数のタイプに同じフィールドのクエリのためにワイルドカードをつけた場合、 ドキュメントタイプを前につける必要があります。
- 複数のドキュメントタイプに対して同じフィールド名で検索する場合、クエリを個別に発行しなければならなく遅くなります。
- 多くの検索で、既存の多くのクエリを壊してしまうために、単純な
match
やterm
クエリの代わりに、multi-fieldクエリを使う必要があります。 - 圧縮の効率の悪さから、ヒープ利用量、ディスク使用量、I/Oなどが、増加します。
- 複数のドキュメントタイプに対するaggregationは、global ordinalの利点を利用できなくなるために、遅くなり、メモリの使用量も増えます。
解決方法
最終的に、同じインデックスの同じ名前を持つ全てのフィールドは、同じマッピングを持つ必要があるというルールを採用することに決めました。
ただ、copy_to
やenabled
のようなパラメータはタイプごとに指定することができるようになっています。
これにより、データの破損、クエリ時の例外そして、おかしな結果が発生する問題を防ぎます。
クエリとaggregationは現在でも高速なままで、圧縮率を最大化し、ヒープ使用量やディスク使用率の低減させます。
この解決方法の欠点は、個別のテーブルとしてタイプを扱いたいユーザが彼らの考え方を変える必要があるということです。
これは、思ったよりも問題ではありません。
実際には、多くのフィールド名はデータの明確なタイプを表現しています。
created_date
は常に、日付ですし、number_of_hits
フィールドはいつも数値です。
フィールドマッピングがコンフリクトしているユーザはデータを失ったり、おかしなデータを受け取ったり、データを欠損させています。
ベストプラクティスにユーザが従っているかどうかによらず、インデックス時に正しい振る舞いを強制することが現在の違いです。
ユーザの多くがコンフリクトしていないフィールドマッピングをもっていれば、 コンフリクトが起きた場合、技術がこれらのシチュエーションを扱うことが可能になると思いませんか? そこにはいくつかの解決方法があります。
タイプの代わりにインデックスを別々に
最も簡単な解決方法です。インデックスを別々のインデックスとし、実際のデータベーステーブルのようにします。 インデックスをまたいだ検索はタイプをまたいだ検索のように動作しますし、 ソートやaggregationも同じデータタイプへのクエリのように動作します。これまでと同じ制限です。
コンフリクトしたフィールドの名前の変更
コンフリクトがごくわずかな場合、(Logstashやアプリケーションで使っているものも一緒に)よりわかりやすいフィールド名に変更することで解決できます。
例えば、2つのerror
フィールドがあった場合に、error_code
とerror_message
に変更します。
copy_to
もしくはmulti-fieldsを利用
異なるドキュメントタイプのフィールドは別々のcopy_to
を設定できます。
元のerror
フィールドはindex
の設定にno
が設定してあり、全てのドキュメントタイプで無効化されていますが、
特定のタイプだけ、error
フィールドの値を数値のerror_code
フィールドにコピーすることができます。
PUT my_index/_mapping/mapping_one
{
"properties": {
"error": {
"type": "string",
"index": "no",
"copy_to": "error_code"
},
"error_code": {
"type": "integer"
}
}
}
他のタイプでは文字列のerror_message
にコピーします。
PUT my_index/_mapping/mapping_two
{
"properties": {
"error": {
"type": "string",
"index": "no",
"copy_to": "error_message"
},
"error_message": {
"type": "string"
}
}
}
同様の解決方法としてmulti-fieldも使えます。
各データタイプに対してネストしたフィールドに
ときどき、Elasticsearchに送ったドキュメントやドキュメントがもっているフィールドを制御できない場合があります。 部分的なコンフリクトに加え、闇雲に、ユーザが送ってきたフィールドを受け入れると、マッピングが肥大化します。 タイムスタンプやIPアドレスをフィールド名に使うようなドキュメントがあると考えてください。
nested
フィールドにすることで、str_val
、int_val
、date_val
というような各データタイプを利用できます。
このアプローチによって、次のドキュメントは
{
"message": "some string",
"count": 1,
"date": "2015-06-01"
}
アプリケーションによって、次のようにフォーマットしなおす必要があります。
{
"data": [
{"key": "message", "str_val": "some_string" },
{"key": "count", "int_val": 1 },
{"key": "date", "date_val": "2015-06-01" }
]
}
この解決方法は、アプリケーションサイドでより多くの作業が必要ですが、コンフリクトの問題とマッピングの肥大化の問題を同時に解決します。
あいまいなフィールドのルックアップ
現在、フィールドの指定には"short name"、フルパス、ドキュメントタイプを前につけたフルパスが利用できます。 これらのオプションがあいまいさをもたらしています。 サンプルとして次のマッピングをご覧ください。
{
"mappings": {
"user": {
"properties": {
"title": {
"type": "string"
}
}
},
"blog": {
"properties": {
"title": {
"type": "string"
},
"user": {
"type": "object",
"fields": {
"title": {
"type": "string"
}
}
}
}
}
}
}
title
はuser.title
、blog.title
、blog.user.title
のどれでしょう?user.title
はuser.title
またはblog.user.title
のどちらでしょう?
答えは「場合によります。」です。Elasticsearchが最初に見つけたものになります。 フィールドはリクエストごとに変わるため、各ノードでマッピングがどのようにシリアライズされたかに依存します。
2.0では、フィールドを指定する時に、ドキュメントタイプを除いたフルパス名を使用するべきでしょう。
user.title
は、blog
タイプのuser.title
を意味します。title
は、user
とblog
タイプのtitle
フィールドを意味します。*title
はuser.title
とtitle
フィールドの両方にマッチします。
user
タイプのtitle
フィールドとblog
タイプのtitle
の違いはどのように指定するのでしょう?
指定できません。フィールドマッピングのコンフリクトで説明した変更により、
title
フィールドは両方のタイプで同じフィールドになります。
本質的にtitle
と呼ばれる1つのフィールドになります。
user.
やblog.
のようなタイプのプレフィックスはタイプを指定することによるフィルタリングで効果があります。
クエリのblog.title
フィールドはblog
タイプのドキュメントだけを検索し、user
タイプのドキュメントを検索しません。
このシンタックスは誤解を招きやすいです。なぜなら、いつでも動作するわけではないからです。
aggregationやsuggestionはすべてのタイプに関する結果を含みます。
この利用のため、上記の例のあいまいさがあるので、タイプのプレフィックスはサポートしません。
重要 short nameやタイププレフィックスを利用したpercolatorは更新する必要があります。
タイプのメタフィールド
すべてのタイプはメタフィールドを持っています。_id
、_index
、_routing
、_parent
、_timestamp
などです。
これらのほとんどはindex
、store
、path
のような幾つかの設定をサポートしています。
これらの設定について次のようにシンプルにしました。
_id
と_type
は変更不可_index
は、ドキュメントのもつインデックスを保存するためにenabled
_routing
はrequired
のみを指定_size
はenabled
のみ_timestamp
はデフォルトで保存される_boost
と_analyzer
は廃止。古いインデックスのものは無視される
ドキュメントのフィールドから_id
と_routing
と_timestamp
の値を抽出することができました。
この機能は廃止されます。これは、ドキュメントのパースとコンフリクトを起こすためです。
代わりに、これらの値はURLもしくはquery stringで指定可能です。
_boost
と_analyzer
フィールドは例外で、すでにあるメタフィールドの設定は古いインデックスのものが採用されます。
アナライザ設定
これまで、indexとsearchのアナライザがインデックス、タイプ、フィールド、ドキュメント(_analyzer
フィールドで)の
それぞれのレベルで指定可能でした。
同じフィールドに対して異なるanalysis chainの組み合わせができることにより、おかしな関連度を引き起こしていました。
フィールドマッピングのコンフリクトを解消することに加え、アナライザの設定も簡略化します。
- Analyzedな文字列フィールドは、
analyzer
設定とsearch_analyzer
設定(analyzer
設定の値をデフォルトとする)を指定できます。index_analyzer
設定はanalyzer
となります。 - 複数のタイプで同じ名前のフィールドがある場合、フィールドはすべて、同じアナライザの設定を持たなければなりません。
- タイプレベルのデフォルト設定の
analyzer
、index_analyzer
、search_analyzer
設定は廃止されます。 - デフォルトアナライザはインデックスごとにインデックスの
analysis
設定で設定します。これらはdefault
もしくはdefault_search
という名前で設定します。 - ドキュメントごとの
_analyzer
フィールドはサポートしません。既存のインデックスのものは無視されます。
index_name
とpath
index_name
とpath
設定は(Elasticsearch v1.0.0から利用できる)copy_to
によって置き換わりました。
既存のインデックスについてはこれらは機能しますが、新しいインデックスでは指定できません。
同期的なマッピングの更新
現在、これまで存在していないフィールドを含むドキュメントをインデキシングするとき、
フィールドはローカルのマッピングに追加され、それから、マスターに変更(新しいマッピングをすべてのシャードに適用する更新)が送信されていました。
同時に2つのシャードに同じフィールドを追加することができます。
また、そのとき、異なる2つのマッピングがある可能性があります。
1つはdouble
でもう1つはlong
だったり、string
とdate
だったりと。
このような場合、マスターに最初に届いたマッピングが採用されます。 しかし、「負けた」マッピングをもつシャードでは、すでに異なるデータのタイプを利用しているため、 これを利用し続けます。 そのご、ノードをリスタートしたときに、シャードが別のノードに移動し、マスターにあるマッピングを適用します。 このとき、インデックスが破損したりデータを失ったりします。
これを防ぐために、シャードはインデキシングを続ける前に、新しいマッピングがマスターによって採用されるかどうかを待つようになりました。 これはすべてのマッピングが安全に更新されます。 新しいフィールドをもっているドキュメントをインデキシングすると、前よりも処理が遅くなるでしょう。 受け入れられることを待つ必要があるためです。 しかし、クラスタの状態の更新処理のスピードが次の2つの新しい機能によって大きく改善されています。
- クラスタ状態の差分:可能であれば、クラスタの状態の変更はクラスタ状態全体の変更ではなく、部分的なものとする。
- シャードへのリクエストの非同期化:シャードアロケーション処理中に、マスタノードは、 割り当てられていないシャードのコピーの日付が最新のものを持っているかを見つけるために、リクエストをデータノードに対して送信します。 ここで、クラスタ状態を変更する呼び出しがブロッキングで行われていました。v1.6.0から、このリクエストはバックグラウンドで非同期で実行されます。 これにより、マッピング更新のようなペンディングタスクをより早く処理できるようになります。
マッピングの削除
(そのタイプのドキュメントがある場合)タイプマッピングを削除できないようにします。 マッピングを削除した後に、削除されたフィールドの情報は、Luceneレベルでは存在し続け、 もし、後から同じ名前のフィールドが追加されたときにインデックスの破損を引き起こします。 そのようなマッピングは残しておくか、新しいインデックスに再インデックスすることができます。
2.0のための準備
マッピングがコンフリクトしているかどうかを決めることは、手動で行うには慎重に行う必要があります。 私たちは、Elasticsearch Migration Pluginを提供します。 これは、2.0で非推奨になったり廃止された機能を利用しているかどうかを見つけるために役に立つでしょう。
もし、コンフリクトしたマッピングを持っている場合、 正しいマッピングを持つ新しいインデックスにデータを再インデックスするか、 必要ないなら削除します。 これらのコンフリクトを解決しない限り2.0にはアップグレードできないでしょう。
comments powered by Disqus
See Also by Hugo
- 辞書の更新についての注意点
- Kuromojiのカスタム辞書をインデックスの設定で指定
- Delete by Query APIはプラグインへ(日本語訳)
- インデックステンプレートとLogstash
- Elasticsearch 1.4.0および1.3.5リリース(日本語訳)