Technique

Modification du mapping à chaud

Update November 2, 2015: Make sure to check out the updates with Elasticsearch mappings introduced in the 2.0 release.

Un développeur que je connais m'a envoyé le tweet suivant :

Mon plus gros problème lorsque j'utilise Elasticsearch en tant que modèle est que je dois réindexer à chaque fois que je modifie un schéma. Au vu du volume de données, cela prend un sacré bout de temps, ce qui entraîne chez moi de longues indisponibilités. Trop longues pour la plupart des applications.

Il est tout à fait possible de procéder à des modifications au niveau du schéma/mapping sans arrêt de service, mais le nombre d'options possibles est bien trop important pour toutes les expliquer dans un tweet, d'où ce billet.

Le problème : pourquoi est-il impossible de modifier les mappings ?

Vous ne pouvez chercher que les données ayant été indéxées. Afin que vous puissiez chercher vos données, votre base de données doit connaître le type des données contenues dans chaque champ, ainsi que la façon dont elles doivent être indexées. Si vous modifiez le type d'un champ, par exemple si c'est une chaîne et que vous en faites une date, toutes les données de ce champ que vous aurez déjà indexées deviendront inutiles. D'une manière ou d'une autre, vous devrez réindexer ce champ.

Cela s'applique non seulement à Elastisearch, mais également à toutes les bases de données qui utilisent des index pour exécuter des recherches. Celles qui n'utilisent pas les index sacrifient la vitesse au profit de la flexibilité.

Elasticsearch (et Lucene) stockent leurs index dans des segments immuables ; chaque segment peut être considéré comme un « mini » index inversé. Ces segments ne sont jamais mis à jour tels quels. La mise à jour d'un document crée en fait un nouveau document et marque l'ancien document comme ayant été supprimé. Lorsque vous ajoutez de nouveaux documents (ou mettez à jour des documents existants), de nouveaux segments sont créés. Un processus de fusion (dit "merge") s'exécute en arrière-plan, et fusionne plusieurs petits segments en un nouveau segment plus volumineux. Ensuite, les segments obsolètes sont définitivement supprimés.

En règle générale, un index Elasticsearch comporte différents types de documents. Chaque _type possède son propre schéma ou son propre mapping. Un seul segment peut contenir des documents de types différents. Ainsi, si vous souhaitez modifier la définition d'un seul champ pour un seul type, vous n'aurez pas vraiment d'autre choix que de réindexer tous les documents de votre index.

L'ajout de champs est sans conséquence

Un segment ne contient des index que pour les champs qui existent réellement dans ses documents. Cela signifie que vous pouvez ajouter de nouveaux champs sans que cela porte à conséquence, à l'aide de l'API put_mapping. Il est alors inutile de réindexer.

Réindexation de vos données

Le processus de réindexation de vos données est très simple. Tout d'abord, créez un nouvel index avec le nouveau mapping et les nouveaux paramètres :

curl -XPUT localhost:9200/new_index -d '
{
    "mappings": {
        "my_type": { ... new mapping definition ...}
    }
}
'

Ensuite, extrayez tous les documents de votre ancien index, à l'aide d'une recherche scroll et indexez-les dans le nouvel index à l'aide de l'API bulk. Bon nombre des API client proposent une méthode de réindexation reindex() qui effectue toutes ces actions à votre place. Une fois fait, vous pouvez supprimer l'ancien index.

Remarque : assurez-vous d'inclure le critère search_type=scan dans votre requête de recherche. Cela permet de désactiver le tri et d'améliorer l'efficacité du parcours de l'ensemble du résultat.

Le problème de cette approche est que le nom de l'index va changer, ce qui signifie que vous devrez modifier votre application afin qu'elle utilise le nouveau nom d'index.

Réindexation de vos données sans indisponibilité

Les alias d'index nous permettent de réindexer les données en arrière-plan, de manière totalement transparente pour notre application. Un alias est comme un lien symbolique qui peut pointer vers un ou plusieurs index réels.

La procédure se déroule généralement comme suit. Vous commencez par créer un index, en ajoutant un numéro de version ou un horodatage à son nom :

curl -XPUT localhost:9200/my_index_v1 -d '
{ ... mappings ... }
'

Create an alias which points to the index:

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index",
            "index": "my_index_v1"
        }}
    ]
}
'

Votre application peut désormais communiquer avec my_index comme s'il s'agissait d'un véritable index.

Lorsque vous devez réindexer vos données, vous pouvez créer un nouvel index en ajoutant un nouveau numéro de version ;

curl -XPUT localhost:9200/my_index_v2 -d '
{ ... mappings ... }
'

Réindexez ensuite les données de my_index_v1 dans le nouvel index my_index_v2, puis modifiez l'alias myindex pour qu'il pointe vers le nouvel index, en une seule étape atomique :

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "remove": {
            "alias": "my_index",
            "index": "my_index_v1"
        }},
        { "add": {
            "alias": "my_index",
            "index": "my_index_v2"
        }}
    ]
}
'

Enfin, supprimez l'ancien index :

curl -XDELETE localhost:9200/my_index_v1

Vous avez réindexé toutes les données en arrière-plan sans aucune indisponibilité. Votre application n'a même pas noté que l'index a été modifié.

Il s'agit donc de l'approche de gestion standard des modifications de schéma. Il existe toutefois diverses autres options que je vais maintenant exposer.

Mes anciennes données ne me sont d'aucune utilité

Comment procéder si vous souhaitez modifier le type de données d'un seul champ et que vous vous moquiez qu'il soit impossible d'effectuer des recherches dans vos anciennes données ? Dans ce cas, plusieurs choix s'offrent à vous :

Supprimer le mapping

Update November 2, 2015: Please note that delete mappings are not supported in Elasticsearch 2.0+.

Si vous supprimez le mapping d'un type spécifique, vous pouvez utiliser l'API put_mapping pour créer un mapping pour ce type dans l'index actuel.

Remarque : lorsque vous supprimez un mapping pour un type, vous supprimez également tous les documents associés à ce type dans l'index.

Cette option est particulièrement utile lorsque vous souhaitez modifier le mapping d'un type qui contient peu de documents.

Renommer le champ

L'ajout de nouveaux champs est sans conséquence. Vous pourriez donc ajouter un champ associé à un nom avec des paramètres différents afin de l'utiliser dans vos documents futurs. Bien entendu, cela implique de modifier le nom de champ utilisé par votre application.

Passer au multi-field

Les multi-fields permettent d'utiliser un même champ pour différents besoins. Classiquement, il est possible d'indexer un champ de titre par exemple, de deux manières : sous forme de chaîne analysée pour l'établissement de requêtes et sous une forme non analysée pour le tri.

Tout champ scalaire (c'est-à-dire à l'exclusion des champs de type objet ou nested) peut être converti en multi-field sans réindexation à l'aide de l'API put_mapping. Par exemple, si nous disposons d'un champ nommé created actuellement défini comme une chaîne :

{
    "created": { "type": "string"}
}

Nous pouvons le convertir en multi-field et lui ajouter un sous-champ date :

curl -XPUT localhost:9200/my_index/my_type/_mapping -d '
{
    "my_type": {
        "properties": {
            "created": {
                "type":   "multi_field",
                "fields": {
                    "created": { "type": "string" },
                    "date":    { "type": "date"   }
                }
            }
        }
    }
}
'

Le champ created d'origine existe encore, sous la forme du sous-champ principal et peut faire l'objet d'une requête sur created ou created.created. La nouvelle version de ce champ de type date peut faire l'objet d'une requête sur created.date et ne sera renseignée que pour les nouveaux documents.

Utilisation des alias pour une meilleure flexibilité

Parfois, les approches ci-dessus peuvent ne pas être suffisantes. Imaginons que votre application inclue 100 000 documents utilisateur et 10 000 000 documents de blog. Vous souhaitez modifier le mapping des documents utilisateurs sans devoir réindexer tous les blogs.

Vous pouvez tout à fait stocker différents types dans différents index. Elasticsearch peut effectuer des recherches aussi bien sur plusieurs index que sur un seul. Ainsi, vous ne devez réindexer que l'index contenant le type que vous souhaitez modifier. En utilisant judicieusement les alias, vous pouvez faire en sorte que la réindexation soit entièrement transparente pour votre application.

Dans cette approche, votre application doit utiliser un alias distinct pour chaque type. Par exemple, au lieu d'indexer tous les documents dans my_index, vous devez indexer les documents utilisateur dans my_index_user et les documents de blog dans my_index_blog :

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_v2"
        }},
        { "add": {
            "alias": "my_index_blog",
            "index": "my_index_v2"
        }}
    ]
}
'

Pour effectuer des recherches dans les documents utilisateur et blog, vous pouvez simplement indiquer les deux alias :

curl localhost:9200/my_index_blog,my_index_user/_search

Lorsque vous souhaitez modifier le mapping des utilisateurs, commencez par créer un index juste pour les utilisateurs, puis choisissez le nombre approprié de shards primaires uniquement pour les documents utilisateur :

curl -XPUT localhost:9200/my_index_users_v1 -d '
{
    "settings": {
        "index": {
            "number_of_shards": 1
        }
    },
    "mappings": {
        "user": { ... new user mapping ... }
    }
}
'

Réindexez uniquement les documents utilisateur de l'ancien index vers le nouveau :

curl 'localhost:9200/my_index_user/user?scroll=1m&search_type=scan' -d '
{
    "size": 1000
}
'

Et mettez à jour l'alias :

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "remove": {
            "alias": "my_index_user",
            "index": "my_index_v2"
        }},
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_user_v1"
        }}
    ]
}
'

Vous pouvez utiliser une requête delete-by-query pour supprimer les documents utilisateur de l'ancien index :

curl -XDELETE localhost:9200/my_index_v1/user

À présent, vous pouvez utiliser l'approche de réindexation standard décrite plus haut à chaque fois que vous souhaitez modifier le mapping des documents utilisateur.

Utilisation d'alias sans réindexation

Si vous souhaitez que vos modifications ne s'appliquent qu'aux nouveaux documents, vous pouvez tout de même utiliser les alias sans avoir à procéder à une réindexation. Vous devez alors toujours créer un index my_index_user_v1, mais également deux index supplémentaires : my_index_user pour l'indexation et my_index_users (pluriel) pour la recherche :

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_user_v1"
        }},
        { "add": {
            "alias": "my_index_users",
            "index": "my_index_user_v1"
        }},
        { "add": {
            "alias": "my_index_users",
            "index": "my_index_v1"
        }},
    ]
}
'

L'alias my_index_user pointe vers le nouvel index et tous les nouveaux documents utilisateur sont indexés avec cet alias. L'alias my_index_users pointe vers le nouvel index ET vers l'ancien index. Vous pouvez ainsi effectuer des recherches sur ces deux index en même temps. L'ancien index utilisera l'ancien mapping et le nouvel index le nouveau.

Comme vous pouvez le voir, Elasticsearch propose de nombreuses options de gestion des index. En prenant le temps de bien planifier sa stratégie, il devient possible de gérer les modifications sans entraîner d'indisponibilité.