dynamic templateを使うと動的に追加されるフィールドのmappingを指定できる

サマリ

dynamic templateを使えば、mappingを動的に決められます。

決め方は次の3つがあります。

  • match_mapping_type:デフォルトのmappingをもとに、mappingを割り当てます。
  • match , unmatch:フィールド名から、mappingを割り当てます。
  • path_match , path_unmatch:フィールドのパスから、mappingを割り当てます。

https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-templates.html

型からmappingを決める

elasticsearchはJSONの型から、mappingをどう設定するかデフォルトで決まっています。

https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-templates.html#match-mapping-type

この設定を上書きするのが、 match_mapping_type です。

次の例では、 string , long とmappingされるフィールドの場合、 text , integer とmappingされるようにしたものです。text はさらに、マルチフィールドになるようにしています。

PUT demo-index
{
    "mappings": {
        "dynamic_templates": [
            {
                "integers": {
                    "match_mapping_type": "long",
                    "mapping": {
                        "type": "integer"
                    }
                }
            },
            {
                "strings": {
                    "match_mapping_type": "string",
                    "mapping": {
                        "type": "text",
                        "fields": {
                            "raw": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    }
                }
            }
        ]
    }
}

実際にdocumentを保存して、mappingがどうなるか確認してみました。

PUT demo-index/_doc/1
{
    "demo-integer": 1,
    "demo-string": "Hello, Elasticsearch"
}

GET demo-index/_mapping

// レスポンス
{
    "demo-index": {
        "mappings": {
            "dynamic_templates": [
                {
                    "integers": {
                        "match_mapping_type": "long",
                        "mapping": {
                            "type": "integer"
                        }
                    }
                },
                {
                    "strings": {
                        "match_mapping_type": "string",
                        "mapping": {
                            "fields": {
                                "raw": {
                                    "ignore_above": 256,
                                    "type": "keyword"
                                }
                            },
                            "type": "text"
                        }
                    }
                }
            ],
            "properties": {
                "demo-integer": {
                    "type": "integer"
                },
                "demo-string": {
                    "type": "text",
                    "fields": {
                        "raw": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                }
            }
        }
    }
}

demo-integer はintegerに、 demo-string はtextに、それぞれmappingされていることがわかります。これらのフィールドに対するmappingは事前に無かったのでdynamic templateによりmappingが動的に決定されたことになります。

フィールド名からmappingを決める

match , unmatch を使うと、フィールド名から型を決定できます。

match を使って、 long_ から始まるフィールドをlong型にmappingするよう設定してみました。

PUT demo-index
{
    "mappings": {
        "dynamic_templates": [
            {
                "longs_as_string": {
                    "match": "long_*",
                    "mapping": {
                        "type": "long"
                    }
                }
            }
        ]
    }
}

この場合、文字列でドキュメントを作成したとしても、フィールド名が一致する場合はlong型にmappingされます。

PUT demo-index/_doc/1
{
    "long_number": "1" <-①
}

GET demo-index/_mapping

// レスポンス
{
    "demo-index": {
        "mappings": {
            "dynamic_templates": [
                {
                    "longs_as_string": {
                        "match": "long_*",
                        "mapping": {
                            "type": "long"
                        }
                    }
                }
            ],
            "properties": {
                "long_number": {
                    "type": "long" <-②
                }
            }
        }
    }
}

long_ から始まるフィールド名 long_number で、文字列の値を保存します。

long_number が、long型にmappingされています。

ドキュメントを取得する場合には、文字列として返されます。あくまでもmappingをlong型にしているのであり、値を変換しているのではない、ということですかね。

GET demo-index/_doc/1/_source

// レスポンス
{
    "long_number": "1"
}

mappingでlong型になっているので、rangeで検索できます。

GET demo-index/_search
{
    "query": {
        "range": {
            "long_number": {
                "gte": 0,
                "lte": 2
            }
        }
    }
}

// レスポンス
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1,
        "hits": [
            {
                "_index": "demo-index",
                "_type": "_doc",
                "_id": "1",
                "_score": 1,
                "_source": {
                    "long_number": "1"
                }
            }
        ]
    }
}

では、数値ではない値を指定するとどうなるのでしょうか?

PUT demo-index/_doc/2
{
    "long_number": "abc"
}

// レスポンス
{
    "error": {
        "root_cause": [
            {
                "type": "mapper_parsing_exception",
                "reason": "failed to parse field [long_number] of type [long] in document with id '2'. Preview of field's value: 'abc'"
            }
        ],
        "type": "mapper_parsing_exception",
        "reason": "failed to parse field [long_number] of type [long] in document with id '2'. Preview of field's value: 'abc'",
        "caused_by": {
            "type": "illegal_argument_exception",
            "reason": "For input string: \"abc\""
        }
    },
    "status": 400
}

ステータス400で、エラーが返ってきました。 error.reason に値をパースできないと書いてあります。long値としてパースできる文字列でなければドキュメントを保存することができなくなるようですね。

パスからmappingを決める

ドキュメントのJSONパスからmappingを決めることもできます。

以下は、 *.middle を除く name.* のパスに一致するフィールドをtext型にmappingする例です。 path_matchpath_unmatch を使ってそれらの条件を指定しています。

PUT demo-index
{
    "mappings": {
        "dynamic_templates": [
            {
                "full_name": {
                    "path_match": "name.*", <--①
                    "path_unmatch": "*.middle", <--②
                    "mapping": {
                        "type": "keyword"
                    }
                }
            }
        ]
    }
}

① 一致する条件をパスで指定します。

② 一致しない条件をパスで指定します。

ドキュメントを保存してmappingの状態を確認してみました。

PUT demo-index/_doc/1
{
    "name": {
        "first": "John",
        "middle": "Winston",
        "last": "Lennon"
    }
}

GET demo-index/_mapping

// レスポンス
{
    "demo-index": {
        "mappings": {
            "dynamic_templates": [
                {
                    "full_name": {
                        "path_match": "name.*",
                        "path_unmatch": "*.middle",
                        "mapping": {
                            "type": "keyword"
                        }
                    }
                }
            ],
            "properties": {
                "name": {
                    "properties": {
                        "first": {
                            "type": "keyword" <--①
                        },
                        "last": {
                            "type": "keyword" <--①
                        },
                        "middle": {
                            "type": "text", <--②
                            "fields": {
                                "keyword": {
                                    "type": "keyword",
                                    "ignore_above": 256
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

① dynamic_templateで指定した、パスに一致しているフィールドのmappingが、keywordになっています。

② 一方、path_unmatch で明示的に除いているパスに一致しているフィールドのmappingは、Elasticsearchのデフォルトのままになっています。

ここで、 path_match に一致するがkeywordとして解釈できない値(例えばJSONオブジェクト)を与えるとどうなるのでしょうか。

PUT demo-index/_doc/2
{
    "name": {
        "first": "John",
        "middle": "Winston",
        "last": "Lennon",
        "other": {
            "nickname": "JWL"
        }
    }
}

// レスポンス
{
    "error": {
        "root_cause": [
            {
                "type": "mapper_parsing_exception",
                "reason": "failed to parse field [name.other] of type [keyword] in document with id '2'. Preview of field's value: '{nickname=JWL}'"
            }
        ],
        "type": "mapper_parsing_exception",
        "reason": "failed to parse field [name.other] of type [keyword] in document with id '2'. Preview of field's value: '{nickname=JWL}'",
        "caused_by": {
            "type": "illegal_state_exception",
            "reason": "Can't get text on a START_OBJECT at 6:18"
        }
    },
    "status": 400
}

エラーが返され保存できませんでした。 error.reason に、 name.other の値をkeyword型にパースできないと書かれています。これはフィールド名でmappingを決めたときにも同じようなエラーがありましたが、パスでmappingを決める時も同様にパースできない値が含まれているとエラーになるようですね。今回のようにワイルドカードを使って一定の範囲のパスに対してmappingを決める場合には注意が必要です。