Index Cloud Datastore

App Engine prédéfinit un index simple sur chaque propriété d'une entité. Une application App Engine peut définir d'autres index personnalisés dans un fichier de configuration d'index nommé datastore-indexes.xml, généré dans le répertoire /war/WEB-INF/appengine-generated de votre application. Le serveur de développement ajoute automatiquement des suggestions à ce fichier lorsqu'il est confronté à des requêtes qui ne peuvent pas être exécutées avec les index existants. Vous pouvez définir des index manuellement en modifiant le fichier avant de transférer l'application.

Pour une présentation approfondie des index et des requêtes, consultez la page Sélection des index et recherche avancée.

Remarque : Le mécanisme de requête basé sur les index permet l'exécution d'un large éventail de requêtes et convient à la plupart des applications. Cependant, il n'est pas compatible avec certains types de requêtes couramment rencontrés dans d'autres technologies de base de données. Plus précisément, les requêtes de jointure et d'agrégation ne sont pas acceptées dans le moteur de requêtes Cloud Datastore. Pour connaître les limites relatives aux requêtes Cloud Datastore, consultez la page Requêtes Datastore.

Définition et structure des index

Un index est défini sur une liste de propriétés d'un genre d'entité donné, avec un ordre correspondant (croissant ou décroissant) pour chaque propriété. Pour une utilisation avec des requêtes ascendantes, l'index peut également inclure les ancêtres d'une entité.

Une table d'index contient une colonne pour chaque propriété nommée dans la définition de l'index. Chaque ligne de la table représente une entité dans Cloud Datastore, qui constitue un résultat potentiel pour les requêtes basées sur l'index. Une entité n'est incluse dans l'index que si un ensemble de valeurs indexées est défini pour chaque propriété utilisée dans l'index. Si la définition de l'index fait référence à une propriété pour laquelle l'entité n'a pas de valeur, cette entité n'apparaîtra pas dans l'index et ne sera donc jamais renvoyée comme résultat pour une requête basée sur l'index.

Remarque : Cloud Datastore fait la distinction entre une entité qui ne possède pas de propriété donnée et une entité qui possède la propriété avec une valeur null. Si vous attribuez explicitement une valeur "null" à la propriété d'une entité, cette entité peut être incluse dans les résultats d'une requête faisant référence à cette propriété.

Remarque : Les index composés de plusieurs propriétés nécessitent que chaque propriété individuelle ne soit pas définie comme non indexée.

Les lignes d'une table d'index sont d'abord triées par ancêtre, puis par valeur de propriété, dans l'ordre spécifié dans la définition d'index. L'index parfait pour une requête, permettant de l'exécuter le plus efficacement, est défini dans les propriétés ci-dessous. Dans l'ordre :

  1. Propriétés utilisées dans les filtres d'égalité
  2. Propriété utilisée dans un filtre d'inégalité (il ne peut y en avoir qu'une seul)
  3. Propriétés utilisées dans les ordres de tri

Cela garantit que tous les résultats pour chaque exécution possible de la requête apparaissent dans des lignes consécutives de la table. Cloud Datastore exécute une requête avec un index parfait en procédant comme suit :

  1. Il identifie l'index correspondant au genre de la requête, aux propriétés du filtre, aux opérateurs de filtre et aux ordres de tri.
  2. Il effectue une analyse en partant du début de l'index jusqu'à la première entité répondant à toutes les conditions de filtrage de la requête.
  3. Il continue l'analyse de l'index, en renvoyant tour à tour chaque entité, jusqu'à ce qu'il :
    • rencontre une entité ne répondant pas aux conditions de filtre ;
    • atteigne la fin de l'index ;
    • ait collecté le nombre maximal de résultats demandés par la requête.

Par exemple, examinez la requête suivante :

Java 8

Query q1 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
                new FilterPredicate("height", FilterOperator.EQUAL, 72)))
        .addSort("height", Query.SortDirection.DESCENDING);

Java 7

Query q1 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
                new FilterPredicate("height", FilterOperator.EQUAL, 72)))
        .addSort("height", Query.SortDirection.DESCENDING);

L'index parfait pour cette requête est une table de clés pour les entités de genre Person, avec des colonnes pour les valeurs des propriétés lastName et height. L'index est d'abord trié par ordre croissant de lastName, puis par ordre décroissant de height.

Deux requêtes ayant la même forme mais des valeurs de filtrage différentes utilisent le même index. Par exemple, la requête suivante utilise le même index que ci-dessus :

Java 8

Query q2 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
                new FilterPredicate("height", FilterOperator.EQUAL, 63)))
        .addSort("height", Query.SortDirection.DESCENDING);

Java 7

Query q2 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
                new FilterPredicate("height", FilterOperator.EQUAL, 63)))
        .addSort("height", Query.SortDirection.DESCENDING);

Les deux requêtes suivantes utilisent également le même index, bien que leurs formes soient différentes :

Java 8

Query q3 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
                new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
        .addSort("height", Query.SortDirection.ASCENDING);

Java 7

Query q3 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
                new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
        .addSort("height", Query.SortDirection.ASCENDING);

et

Java 8

Query q4 =
    new Query("Person")
        .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
        .addSort("firstName", Query.SortDirection.ASCENDING)
        .addSort("height", Query.SortDirection.ASCENDING);

Java 7

Query q4 =
    new Query("Person")
        .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
        .addSort("firstName", Query.SortDirection.ASCENDING)
        .addSort("height", Query.SortDirection.ASCENDING);

Configuration des index

Par défaut, Cloud Datastore prédéfinit automatiquement un index pour chaque propriété de chaque genre d'entité. Les index intégrés suffisent à exécuter de nombreuses requêtes simples, telles que les requêtes comportant uniquement des égalités ou les requêtes à inégalité simples. Pour toutes les autres requêtes, l'application doit définir les index nécessaires dans un fichier de configuration d'index nommé datastore-indexes.xml. Si l'application tente d'exécuter une requête qui ne peut pas être exécutée avec les index disponibles (intégrés ou spécifiés dans le fichier de configuration d'index), la requête échoue avec une erreur DatastoreNeedIndexException.

Datastore crée des index automatiques pour les requêtes qui se présentent sous les formes suivantes :

  • Requêtes sans genre n'utilisant que des filtres d'ancêtre et de clé
  • Requêtes n'utilisant que des filtres d'ancêtre et d'égalité
  • Requêtes n'utilisant que des filtres d'inégalité (limités à une seule propriété)
  • Requêtes n'utilisant que des filtres d'ancêtre, des filtres d'égalité sur les propriétés et des filtres d'inégalité sur les clés
  • Requêtes n'utilisant pas de filtre et un seul ordre de tri sur une propriété, croissant ou décroissant

Les autres formes de requêtes nécessitent que leurs index soient spécifiés dans le fichier de configuration d'index, en particulier :

  • Requêtes avec filtres d'ancêtre et d'inégalité
  • Requêtes avec un ou plusieurs filtres d'inégalité sur une propriété, et un ou plusieurs filtres d'égalité sur d'autres propriétés
  • Requêtes avec un ordre de tri décroissant sur les clés
  • Requêtes avec plusieurs ordres de tri

Index et propriétés

Vous trouverez ci-dessous quelques considérations spécifiques à prendre en compte au sujet des index et de leur relation avec les propriétés des entités dans Cloud Datastore :

Propriétés dotées de types de valeurs différents

Lorsque deux entités possèdent des propriétés portant le même nom, mais dont les types de valeurs sont différents, un index de la propriété les trie d'abord par type de valeur, puis dans un ordre secondaire approprié à chaque type. Par exemple, si deux entités possèdent chacune une propriété nommée age, l'une avec une valeur entière et l'autre avec une valeur de chaîne, l'entité avec la valeur entière précède toujours celle avec la valeur de chaîne lorsqu'elles sont triées suivant la propriété age, indépendamment des valeurs de propriété.

Cela vaut particulièrement pour les entiers et les nombres à virgule flottante, qui sont considérés comme des types distincts par Cloud Datastore. Comme les entiers sont triés avant les nombres à virgule flottante, cela signifie qu'une propriété avec la valeur entière 38 précède une propriété avec la valeur à virgule flottante 37.5.

Propriétés non indexées

Si vous savez que vous ne devrez jamais filtrer ou trier une propriété spécifique, vous pouvez indiquer à Cloud Datastore de ne pas conserver les entrées d'index de cette propriété en déclarant la propriété non indexée. Cela permet de réduire le coût d'exécution de votre application en diminuant le nombre d'écritures qu'elle doit réaliser dans Cloud Datastore. Une entité avec une propriété non indexée se comporte comme si la propriété n'était pas définie : les requêtes avec un filtre ou un ordre de tri sur la propriété non indexée ne correspondront jamais à cette entité.

Remarque : Si une propriété apparaît dans un index comportant plusieurs propriétés, le fait de la définir en tant que propriété non indexée l'empêchera d'être indexée dans l'index composite.

Par exemple, supposons qu'une entité possède les propriétés a et b, et que vous souhaitiez créer un index capable de satisfaire des requêtes telles que WHERE a ="bike" and b="red". Supposons également que vous ne vous souciez pas des requêtes WHERE a="bike" et WHERE b="red". Si vous définissez a en tant que propriété non indexée et que vous créez des index pour a et b, Cloud Datastore ne créera pas d'entrées d'index pour les index a et b. Par conséquent, la requête WHERE a="bike" and b="red" ne fonctionnera pas. Pour que Cloud Datastore crée des entrées pour les index a et b, a et b doivent être indexés.

Dans l'API Datastore de bas niveau pour Java, les propriétés sont définies comme indexées ou non indexées par entité, suivant la méthode utilisée pour les définir (setProperty() ou setUnindexedProperty()) :

Java 8

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Key acmeKey = KeyFactory.createKey("Company", "Acme");

Entity tom = new Entity("Person", "Tom", acmeKey);
tom.setProperty("name", "Tom");
tom.setProperty("age", 32);
datastore.put(tom);

Entity lucy = new Entity("Person", "Lucy", acmeKey);
lucy.setProperty("name", "Lucy");
lucy.setUnindexedProperty("age", 29);
datastore.put(lucy);

Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);

Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);

// Returns tom but not lucy, because her age is unindexed
List<Entity> results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Key acmeKey = KeyFactory.createKey("Company", "Acme");

Entity tom = new Entity("Person", "Tom", acmeKey);
tom.setProperty("name", "Tom");
tom.setProperty("age", 32);
datastore.put(tom);

Entity lucy = new Entity("Person", "Lucy", acmeKey);
lucy.setProperty("name", "Lucy");
lucy.setUnindexedProperty("age", 29);
datastore.put(lucy);

Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);

Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);

// Returns tom but not lucy, because her age is unindexed
List<Entity> results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());

Vous pouvez passer une propriété d'indexée à non indexée en réinitialisant sa valeur à l'aide de la méthode setUnindexedProperty() ou, inversement, de non indexée à indexée en la réinitialisant avec la méthode setProperty().

Toutefois, notez que le fait d'indexer une propriété initialement non indexée n'a aucune incidence sur les entités éventuellement créées avant la modification. Les requêtes filtrant cette propriété ne renverront pas ces entités existantes, car elles n'ont pas été écrites dans l'index de la requête lors de leur création. Pour rendre ces entités accessibles aux requêtes ultérieures, vous devez les réécrire dans Cloud Datastore afin de les inclure dans les index appropriés. En d'autres termes, vous devez effectuer les opérations suivantes pour chacune de ces entités existantes :

  1. Récupérer (commande "get") l'entité dans Cloud Datastore
  2. Écrire (commande "put") de nouveau l'entité dans Cloud Datastore

De même, le fait de modifier une propriété initialement indexée en tant que propriété non indexée n'a une incidence que sur les entités écrites ultérieurement dans Cloud Datastore. Les entrées d'index pour toute entité existante avec cette propriété continueront d'exister jusqu'à ce que les entités soient mises à jour ou supprimées. Pour éviter des résultats indésirables, vous devez purger votre code de toutes les requêtes de filtrage ou de tri basées sur cette propriété (désormais non indexée).

Limites relatives aux index

Datastore impose des limites sur le nombre et la taille globale des entrées d'index pouvant être associées à une même entité. Ces limites sont élevées et la plupart des applications ne sont pas affectées. Toutefois, il existe des circonstances dans lesquelles vous pourriez y être confronté.

Comme décrit ci-dessus, Cloud Datastore crée une entrée dans un index prédéfini pour chaque propriété de chaque entité, à l'exception des chaînes de caractères longues (Text), des chaînes d'octets longues (Blob), des entités intégrées (EmbeddedEntity), ainsi que des propriétés que vous avez explicitement déclarées comme non indexées. La propriété peut également être incluse dans des index personnalisés supplémentaires, déclarés dans le fichier de configuration d'index datastore-indexes.xml. Si une entité ne possède pas de propriétés de liste, elle affichera au maximum une entrée dans chacun de ces index personnalisés (pour les index non ascendants) ou une entrée pour chacun des ancêtres de l'entité (pour les index ascendants). Chacune de ces entrées d'index doit être mise à jour chaque fois que la valeur de la propriété change.

Pour une propriété ayant une valeur unique pour chaque entité, chaque valeur possible doit être stockée une seule fois par entité dans l'index prédéfini de la propriété. Même dans ce cas, une entité ayant un grand nombre de propriétés dotées d'une seule valeur est susceptible de dépasser la limite d'entrées ou de taille de l'index. De même, une entité pouvant avoir plusieurs valeurs pour la même propriété nécessite une entrée d'index distincte pour chaque valeur. Là encore, si le nombre de valeurs possibles est important, une entité de ce type peut dépasser la limite d'entrées.

La situation s'aggrave dans le cas d'entités ayant plusieurs propriétés, chacune pouvant utiliser plusieurs valeurs. Pour accepter les entités de ce type, l'index doit inclure une entrée pour chaque combinaison possible de valeurs de propriété. Les index personnalisés faisant référence à plusieurs propriétés, chacune ayant plusieurs valeurs, peuvent générer une "explosion" combinatoire, car ils nécessitent un grand nombre d'entrées pour une entité n'ayant, au final, qu'un nombre relativement réduit de valeurs de propriétés possibles. Ces index exponentiels peuvent considérablement augmenter le coût lié à l'écriture d'une entité dans Cloud Datastore en raison du grand nombre d'entrées d'index à mettre à jour, et entraîner le dépassement de la limite d'entrées ou de taille de l'index de l'entité.

Considérons la requête suivante :

Java 8

Query q =
    new Query("Widget")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("x", FilterOperator.EQUAL, 1),
                new FilterPredicate("y", FilterOperator.EQUAL, 2)))
        .addSort("date", Query.SortDirection.ASCENDING);

Java 7

Query q =
    new Query("Widget")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("x", FilterOperator.EQUAL, 1),
                new FilterPredicate("y", FilterOperator.EQUAL, 2)))
        .addSort("date", Query.SortDirection.ASCENDING);

Elle amène le SDK à suggérer l'index suivant :

Java 8

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget" ancestor="false" source="manual">
    <property name="x" direction="asc"/>
    <property name="y" direction="asc"/>
    <property name="date" direction="asc"/>
  </datastore-index>
</datastore-indexes>

Java 7

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget" ancestor="false" source="manual">
    <property name="x" direction="asc"/>
    <property name="y" direction="asc"/>
    <property name="date" direction="asc"/>
  </datastore-index>
</datastore-indexes>
Cet index nécessitera un total de |x| * |y| * |date| entrées pour chaque entité (où |x| désigne le nombre de valeurs associées à l'entité pour la propriété x). Par exemple, le code ci-dessous :

Java 8

Entity widget = new Entity("Widget");
widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
widget.setProperty("y", Arrays.asList("red", "green", "blue"));
widget.setProperty("date", new Date());
datastore.put(widget);

Java 7

Entity widget = new Entity("Widget");
widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
widget.setProperty("y", Arrays.asList("red", "green", "blue"));
widget.setProperty("date", new Date());
datastore.put(widget);

crée une entité avec quatre valeurs pour la propriété x, trois valeurs pour la propriété y, et la valeur de date définie à la date actuelle. Douze entrées d'index sont requises, une pour chaque combinaison possible des valeurs des propriétés :

(1, "red", <now>) (1, "green", <now>) (1, "blue", <now>)

(2, "red", <now>) (2, "green", <now>) (2, "blue", <now>)

(3, "red", <now>) (3, "green", <now>) (3, "blue", <now>)

(4, "red", <now>) (4, "green", <now>) (4, "blue", <now>)

Lorsque la même propriété est répétée plusieurs fois, Cloud Datastore peut détecter les index exponentiels et suggérer un autre index. Toutefois, dans tous les autres cas (comme la requête définie dans cet exemple), Cloud Datastore génère un index exponentiel. Dans ce cas, vous pouvez contourner l'index exponentiel en configurant manuellement un index dans le fichier de configuration d'index :

Java 8

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget">
    <property name="x" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
  <datastore-index kind="Widget">
    <property name="y" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
</datastore-indexes>

Java 7

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget">
    <property name="x" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
  <datastore-index kind="Widget">
    <property name="y" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
</datastore-indexes>
Cela permet de réduire le nombre d'entrées nécessaires à seulement (|x| * |date| + |y| * |date|), soit 7 entrées au lieu de 12 :

(1, <now>) (2, <now>) (3, <now>) (4, <now>)

("red", <now>) ("green", <now>) ("blue", <now>)

Toute opération de type put susceptible de faire dépasser la limite d'entrées ou de taille de l'index échouera avec une exception IllegalArgumentException. Le texte de l'exception indique quelle limite a été dépassée ("Too many indexed properties" ou "Index entries too large") et quel index personnalisé en est à l'origine. Si vous créez un nouvel index qui, lors de sa création, dépasse les limites pour n'importe quelle entité, toute requête sur l'index échoue et l'index présente un état Error dans la console GCP. Pour résoudre le problème d'un index en état Error :

  1. Supprimez l'index en état Error de votre fichier datastore-indexes.xml.

  2. Exécutez la commande suivante à partir du répertoire contenant votre fichier datastore-indexes.xml, afin de supprimer cet index de Cloud Datastore :

    appcfg.sh vacuum_indexes [YOUR_APP_DIR]
    
  3. Résolvez la cause de l'erreur. Exemple :

    • Reformulez la définition de l'index et les requêtes correspondantes.
    • Supprimez les entités à l'origine de l'index exponentiel.
  4. Replacez l'index dans votre fichier datastore-indexes.xml.

  5. Exécutez la commande suivante à partir du répertoire contenant votre fichier datastore-indexes.xml afin de recréer cet index dans Cloud Datastore :

    appcfg.sh update_indexes datastore-indexes.xml
    

En utilisant une propriété de liste, vous pouvez éviter les requêtes qui nécessitent un index personnalisé, et ainsi éviter les index exponentiels. Comme décrit ci-dessus, cette opération inclut les requêtes avec plusieurs ordres de tri ou les requêtes combinant des filtres d'égalité et d'inégalité.

Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Java