@johtaniの日記 2nd

@johtani ‘s blog 2nd edition

Mappingのすばらしいリファクタリング(日本語訳)

※この記事は次のブログを翻訳したものになります。

原文:The Great Mapping Refactoring

Elasticsearchのユーザの悩みの最も大きなものの一つは、 タイプとフィールドのマッピングに関する多義性です。 この多義性は、インデックス時の例外やクエリ時の例外、 正しくない結果、リクエストからリクエストへ変化する結果、 また、インデックスの故障やデータのロスを結果として引き起こします。

Elasticsearchをより強固で予測可能な振る舞いをするようにする作業において、 フィールドやタイプのマッピングをより厳格でより信頼性を高くするかといったことに 多くの変更を費やしました。 多くのケースで、Elasticsearch v2.0で新しいインデックスを作るときにのみ、 新しいルールを強制し、これまでのインデックスに関しては後方互換性を保つようにします。

しかし、幾つかのケースでは、先ほど説明したようなフィールドマッピングの コンフリクトなどが存在するため、それらを利用できないです。

コンフリクトしたフィールドのマッピングをもつインデックスはElasticsearch v2.0にはアップグレードできません。

もし、これらのインデックスのデータが必要ない場合は、インデックスを消せばいいです。 そうでない場合はマッピングを正しくして再度インデックスする必要があるでしょう。

マッピングを正しく変更することは、私たちが簡単に決めることではありません。 ここからは、現在ある問題点と、私たちがどのように実装して解決したかについて説明します。

フィールドマッピングのコンフリクト

これまで、わたしたちはドキュメントのタイプは「データベースのテーブルのようなもの」と説明していました。 タイプの目的を説明する簡単な方法だったからです。 しかし、残念なことにこれは、真実ではありません。 「同じ」インデックスの「異なるタイプ」にある同じ名前のフィールドは、 内部的に、Luceneのフィールド名が同じものになります。

もしerrorフィールドとして、ドキュメントタイプがapacheのものには数値(integer)を、 ドキュメントタイプがnginxのものには文字列(string)を割り当てた場合、 Elasticsearchは同じLuceneのフィールドに数値と文字列のデータをもつことになります。 このフィールドに対して、検索やaggregationを行う場合、おかしな結果を受け取るか、例外が帰ってくるか、 インデックスが破損することになります。

この問題を解決するために、まず、ドキュメントタイプの名前をフィールドの名前の前に追加することを考えました。 各フィールドは完全に別のものとなります。 このアプローチの利点はドキュメントタイプが実際のテーブルのようになることです。

しかし、この方法には多くの欠点があります。

  • フィールドは常に、他のタイプとは異なるものであると区別するためもしくは、複数のタイプに同じフィールドのクエリのためにワイルドカードをつけた場合、 ドキュメントタイプを前につける必要があります。
  • 複数のドキュメントタイプに対して同じフィールド名で検索する場合、クエリを個別に発行しなければならなく遅くなります。
  • 多くの検索で、既存の多くのクエリを壊してしまうために、単純なmatchtermクエリの代わりに、multi-fieldクエリを使う必要があります。
  • 圧縮の効率の悪さから、ヒープ利用量、ディスク使用量、I/Oなどが、増加します。
  • 複数のドキュメントタイプに対するaggregationは、global ordinalの利点を利用できなくなるために、遅くなり、メモリの使用量も増えます。

解決方法

最終的に、同じインデックスの同じ名前を持つ全てのフィールドは、同じマッピングを持つ必要があるというルールを採用することに決めました。 ただ、copy_toenabledのようなパラメータはタイプごとに指定することができるようになっています。 これにより、データの破損、クエリ時の例外そして、おかしな結果が発生する問題を防ぎます。 クエリとaggregationは現在でも高速なままで、圧縮率を最大化し、ヒープ使用量やディスク使用率の低減させます。

この解決方法の欠点は、個別のテーブルとしてタイプを扱いたいユーザが彼らの考え方を変える必要があるということです。 これは、思ったよりも問題ではありません。 実際には、多くのフィールド名はデータの明確なタイプを表現しています。 created_dateは常に、日付ですし、number_of_hitsフィールドはいつも数値です。 フィールドマッピングがコンフリクトしているユーザはデータを失ったり、おかしなデータを受け取ったり、データを欠損させています。 ベストプラクティスにユーザが従っているかどうかによらず、インデックス時に正しい振る舞いを強制することが現在の違いです。

ユーザの多くがコンフリクトしていないフィールドマッピングをもっていれば、 コンフリクトが起きた場合、技術がこれらのシチュエーションを扱うことが可能になると思いませんか? そこにはいくつかの解決方法があります。

タイプの代わりにインデックスを別々に

最も簡単な解決方法です。インデックスを別々のインデックスとし、実際のデータベーステーブルのようにします。 インデックスをまたいだ検索はタイプをまたいだ検索のように動作しますし、 ソートやaggregationも同じデータタイプへのクエリのように動作します。これまでと同じ制限です。

コンフリクトしたフィールドの名前の変更

コンフリクトがごくわずかな場合、(Logstashやアプリケーションで使っているものも一緒に)よりわかりやすいフィールド名に変更することで解決できます。 例えば、2つのerrorフィールドがあった場合に、error_codeerror_messageに変更します。

copy_toもしくはmulti-fieldsを利用

異なるドキュメントタイプのフィールドは別々のcopy_toを設定できます。 元のerrorフィールドはindexの設定にnoが設定してあり、全てのドキュメントタイプで無効化されていますが、 特定のタイプだけ、errorフィールドの値を数値のerror_codeフィールドにコピーすることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT my_index/_mapping/mapping_one
{
  "properties": {
    "error": {
      "type": "string",
      "index": "no",
      "copy_to": "error_code"
    },
    "error_code": {
      "type": "integer"
    }
  }
}

他のタイプでは文字列のerror_messageにコピーします。

1
2
3
4
5
6
7
8
9
10
11
12
13
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_valint_valdate_valというような各データタイプを利用できます。

このアプローチによって、次のドキュメントは

1
2
3
4
5
{
  "message": "some string",
  "count":   1,
  "date":    "2015-06-01"
}

アプリケーションによって、次のようにフォーマットしなおす必要があります。

1
2
3
4
5
6
7
{
  "data": [
    {"key": "message", "str_val":  "some_string" },
    {"key": "count",   "int_val":  1             },
    {"key": "date",    "date_val": "2015-06-01"  }
  ]
}

この解決方法は、アプリケーションサイドでより多くの作業が必要ですが、コンフリクトの問題とマッピングの肥大化の問題を同時に解決します。

あいまいなフィールドのルックアップ

現在、フィールドの指定には”short name”、フルパス、ドキュメントタイプを前につけたフルパスが利用できます。 これらのオプションがあいまいさをもたらしています。 サンプルとして次のマッピングをご覧ください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "mappings": {
    "user": {
      "properties": {
        "title": {
          "type": "string"
        }
      }
    },
    "blog": {
      "properties": {
        "title": {
          "type": "string"
        },
        "user": {
          "type": "object",
          "fields": {
            "title": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
  • titleuser.titleblog.titleblog.user.titleのどれでしょう?
  • user.titleuser.titleまたはblog.user.titleのどちらでしょう?

答えは「場合によります。」です。Elasticsearchが最初に見つけたものになります。 フィールドはリクエストごとに変わるため、各ノードでマッピングがどのようにシリアライズされたかに依存します。

2.0では、フィールドを指定する時に、ドキュメントタイプを除いたフルパス名を使用するべきでしょう。

  • user.titleは、blogタイプのuser.titleを意味します。
  • titleは、userblogタイプのtitleフィールドを意味します。
  • *titleuser.titletitleフィールドの両方にマッチします。

userタイプのtitleフィールドとblogタイプのtitleの違いはどのように指定するのでしょう?

指定できません。フィールドマッピングのコンフリクトで説明した変更により、 titleフィールドは両方のタイプで同じフィールドになります。 本質的にtitleと呼ばれる1つのフィールドになります。

user.blog.のようなタイプのプレフィックスはタイプを指定することによるフィルタリングで効果があります。 クエリのblog.titleフィールドはblogタイプのドキュメントだけを検索し、userタイプのドキュメントを検索しません。 このシンタックスは誤解を招きやすいです。なぜなら、いつでも動作するわけではないからです。 aggregationやsuggestionはすべてのタイプに関する結果を含みます。 この利用のため、上記の例のあいまいさがあるので、タイプのプレフィックスはサポートしません。

重要 short nameやタイププレフィックスを利用したpercolatorは更新する必要があります。

タイプのメタフィールド

すべてのタイプはメタフィールドを持っています。_id_index_routing_parent_timestampなどです。 これらのほとんどはindexstorepathのような幾つかの設定をサポートしています。 これらの設定について次のようにシンプルにしました。

  • _id_typeは変更不可
  • _indexは、ドキュメントのもつインデックスを保存するためにenabled
  • _routingrequiredのみを指定
  • _sizeenabledのみ
  • _timestampはデフォルトで保存される
  • _boost_analyzerは廃止。古いインデックスのものは無視される

ドキュメントのフィールドから_id_routing_timestampの値を抽出することができました。 この機能は廃止されます。これは、ドキュメントのパースとコンフリクトを起こすためです。 代わりに、これらの値はURLもしくはquery stringで指定可能です。

_boost_analyzerフィールドは例外で、すでにあるメタフィールドの設定は古いインデックスのものが採用されます。

アナライザ設定

これまで、indexとsearchのアナライザがインデックス、タイプ、フィールド、ドキュメント(_analyzerフィールドで)の それぞれのレベルで指定可能でした。 同じフィールドに対して異なるanalysis chainの組み合わせができることにより、おかしな関連度を引き起こしていました。 フィールドマッピングのコンフリクトを解消することに加え、アナライザの設定も簡略化します。

  • Analyzedな文字列フィールドは、analyzer設定とsearch_analyzer設定(analyzer設定の値をデフォルトとする)を指定できます。index_analyzer設定はanalyzerとなります。
  • 複数のタイプで同じ名前のフィールドがある場合、フィールドはすべて、同じアナライザの設定を持たなければなりません。
  • タイプレベルのデフォルト設定のanalyzerindex_analyzersearch_analyzer設定は廃止されます。
  • デフォルトアナライザはインデックスごとにインデックスのanalysis設定で設定します。これらはdefaultもしくはdefault_searchという名前で設定します。
  • ドキュメントごとの_analyzerフィールドはサポートしません。既存のインデックスのものは無視されます。

index_namepath

index_namepath設定は(Elasticsearch v1.0.0から利用できる)copy_toによって置き換わりました。 既存のインデックスについてはこれらは機能しますが、新しいインデックスでは指定できません。

同期的なマッピングの更新

現在、これまで存在していないフィールドを含むドキュメントをインデキシングするとき、 フィールドはローカルのマッピングに追加され、それから、マスターに変更(新しいマッピングをすべてのシャードに適用する更新)が送信されていました。 同時に2つのシャードに同じフィールドを追加することができます。 また、そのとき、異なる2つのマッピングがある可能性があります。 1つはdoubleでもう1つはlongだったり、stringdateだったりと。

このような場合、マスターに最初に届いたマッピングが採用されます。 しかし、「負けた」マッピングをもつシャードでは、すでに異なるデータのタイプを利用しているため、 これを利用し続けます。 そのご、ノードをリスタートしたときに、シャードが別のノードに移動し、マスターにあるマッピングを適用します。 このとき、インデックスが破損したりデータを失ったりします。

これを防ぐために、シャードはインデキシングを続ける前に、新しいマッピングがマスターによって採用されるかどうかを待つようになりました。 これはすべてのマッピングが安全に更新されます。 新しいフィールドをもっているドキュメントをインデキシングすると、前よりも処理が遅くなるでしょう。 受け入れられることを待つ必要があるためです。 しかし、クラスタの状態の更新処理のスピードが次の2つの新しい機能によって大きく改善されています。

  • クラスタ状態の差分:可能であれば、クラスタの状態の変更はクラスタ状態全体の変更ではなく、部分的なものとする。
  • シャードへのリクエストの非同期化:シャードアロケーション処理中に、マスタノードは、 割り当てられていないシャードのコピーの日付が最新のものを持っているかを見つけるために、リクエストをデータノードに対して送信します。 ここで、クラスタ状態を変更する呼び出しがブロッキングで行われていました。v1.6.0から、このリクエストはバックグラウンドで非同期で実行されます。 これにより、マッピング更新のようなペンディングタスクをより早く処理できるようになります。

マッピングの削除

(そのタイプのドキュメントがある場合)タイプマッピングを削除できないようにします。 マッピングを削除した後に、削除されたフィールドの情報は、Luceneレベルでは存在し続け、 もし、後から同じ名前のフィールドが追加されたときにインデックスの破損を引き起こします。 そのようなマッピングは残しておくか、新しいインデックスに再インデックスすることができます。

2.0のための準備

マッピングがコンフリクトしているかどうかを決めることは、手動で行うには慎重に行う必要があります。 私たちは、Elasticsearch Migration Pluginを提供します。 これは、2.0で非推奨になったり廃止された機能を利用しているかどうかを見つけるために役に立つでしょう。

もし、コンフリクトしたマッピングを持っている場合、 正しいマッピングを持つ新しいインデックスにデータを再インデックスするか、 必要ないなら削除します。 これらのコンフリクトを解決しない限り2.0にはアップグレードできないでしょう。

Comments