Índices de Datastore

App Engine predefine un índice simple en cada propiedad de una entidad. Una aplicación de App Engine puede definir más índices personalizados en un archivo de configuración de índice llamado datastore-indexes.xml, que se genera en el directorio /war/WEB-INF/appengine-generated de tu aplicación. El servidor de desarrollo agrega sugerencias a este archivo de forma automática cuando encuentra consultas que no pueden ejecutarse con los índices existentes. Para ajustar los índices de forma manual, edita el archivo antes de subir la aplicación.

Nota: El mecanismo de consulta basado en índices admite un rango amplio de consultas y es apto para la mayoría de las aplicaciones. Sin embargo, no admite algunos tipos de consultas comunes en otras tecnologías de bases de datos. En particular, las uniones y consultas agregadas no son compatibles con el motor de consultas de Datastore. Visita Consultas de Datastore para conocer sus limitaciones.

Definición y estructura de los índices

Un índice se define en una lista de propiedades de un tipo de entidad determinada, con un orden correspondiente (ascendente o descendente) para cada propiedad. El índice también puede incluir las entidades principales de la entidad para usarlas en las consultas principales.

Una tabla de índice contiene una columna por cada propiedad que figura en la definición del índice. Cada fila de la tabla representa una entidad en Datastore que es un resultado potencial de las consultas basadas en el índice. Una entidad solo se incluye en el índice si tiene un valor indexado configurado para cada propiedad usada en el índice. Si la definición del índice hace referencia a una propiedad cuya entidad no tiene ningún valor, esa entidad no aparecerá en el índice y nunca se mostrará como resultado de ninguna consulta basada en el índice.

Nota: Datastore distingue entre una entidad que no posee una propiedad y una que la posee con un valor nulo (null). Si asignas de forma explícita un valor nulo a una propiedad de una entidad, esa entidad puede incluirse en los resultados de una consulta que haga referencia a esa propiedad.

Nota: Ninguna de las propiedades individuales de los índices compuestos por varias propiedades debe configurarse como no indexada.

Las filas de una tabla de índice se ordenan primero por su principal y, luego, por los valores de la propiedad, en el orden especificado en la definición del índice. El índice perfecto de una consulta, que permite que esta se ejecute de manera más eficiente, se define según las siguientes propiedades y en este orden:

  1. Propiedades usadas en filtros de igualdad
  2. Propiedad usada en un filtro de desigualdad (no puede haber más de uno)
  3. Propiedades usadas en órdenes de clasificación

Esto garantiza que todos los resultados para cada ejecución posible de la consulta aparezcan en filas consecutivas de la tabla. Datastore ejecuta una consulta con un índice perfecto mediante estos pasos:

  1. Identifica el índice correspondiente al tipo, las propiedades de filtro, los operadores de filtro y los órdenes de clasificación de la consulta.
  2. Busca desde el principio del índice hasta la primera entidad que cumple con todas las condiciones de filtro de la consulta.
  3. Continúa la búsqueda del índice y muestra cada entidad hasta que se cumple alguna de las siguientes condiciones:
    • Encuentra una entidad que no cumple con las condiciones del filtro.
    • Llegue al final del índice.
    • Recopile la cantidad máxima de resultados solicitados por la consulta.

Por ejemplo, considera la siguiente consulta:

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);

El índice perfecto de esta consulta es una tabla de claves para entidades de tipo Person, con columnas que contienen los valores de las propiedades lastName y height. El índice se ordena primero en orden ascendente por lastName y, luego, en orden descendente por height.

Para generar estos índices, configúralos de la siguiente manera:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Person" ancestor="false" source="manual">
    <property name="lastName" direction="asc"/>
    <property name="height" direction="desc"/>
  </datastore-index>
</datastore-indexes>

Dos consultas del mismo tipo, pero con valores de filtro diferentes, usan el mismo índice. Por ejemplo, la siguiente consulta usa el mismo índice que la consulta anterior:

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);

Las dos consultas siguientes también usan el mismo índice, a pesar de tener formas diferentes:

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);

y

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

Configuración de índices

De forma predeterminada, Datastore predefine un índice de manera automática para cada propiedad de cada tipo de entidad. Estos índices predefinidos son suficientes para realizar varias consultas sencillas, como consultas solo de igualdad y consultas de desigualdad simples. Para todas las demás consultas, la aplicación debe definir los índices que necesita en un archivo de configuración de índices llamado datastore-indexes.xml. Si la aplicación intenta realizar una consulta que no puede ejecutarse con los índices disponibles (predefinidos o especificados en el archivo de configuración de índices), la consulta fallará y mostrará una DatastoreNeedIndexException.

Datastore compila índices automáticos para consultas de los siguientes tipos:

  • Consultas sin categoría que usan solo filtros de entidad principal y de clave
  • Consultas que usan solo filtros de entidad principal y de igualdad
  • Consultas que usan solo filtros de desigualdad (los que están limitados a una única propiedad)
  • Consultas que usan solo filtros de entidad principal, filtros de igualdad en propiedades y filtros de desigualdad en claves
  • Consultas sin filtros y solo con un orden en una propiedad, ya sea ascendente o descendente

Otros tipos de consulta requieren que se especifiquen sus índices en el archivo de configuración de índices. Se incluyen las siguientes:

  • Consultas con filtros de entidad principal y de desigualdad
  • Consultas con uno o más filtros de desigualdad en una propiedad y uno o más filtros de igualdad en otras propiedades
  • Consultas con un orden de clasificación por claves descendente
  • Consultas con varios órdenes de clasificación

Índices y propiedades

Estas son algunas consideraciones especiales sobre los índices y cómo se relacionan con las propiedades de las entidades de Datastore:

Propiedades con tipos de valores mixtos

Cuando dos entidades tienen propiedades con el mismo nombre, pero tipos de valor diferentes, un índice de la propiedad clasifica las entidades primero por el tipo de valor y, luego, por un orden secundario adecuado para cada tipo. Por ejemplo, si dos entidades tienen una propiedad llamada age, una con un valor de número entero y otra con un valor de string, la entidad con el valor de número entero siempre precede a la que tiene el valor de string cuando se ordena por la propiedad age, sin importar los valores de la propiedad en sí.

Esto es muy importante en el caso de los números enteros y los números de punto flotante, que Datastore trata como tipos diferentes. Debido a que todos los números enteros se ordenan antes que los números flotantes, una propiedad con el valor de número entero 38 aparece antes que una con el valor de punto flotante 37.5.

Propiedades no indexadas

Si sabes que nunca tendrás que ordenar o filtrar una propiedad en particular, puedes declarar que la propiedad es no indexada para que Datastore no conserve las entradas de índice de esa propiedad. Esto reduce el costo de ejecución de la aplicación, ya que disminuye la cantidad de operaciones de escritura que Datastore debe realizar. Una entidad con una propiedad no indexada se comporta como si la propiedad no estuviera configurada: las consultas con un filtro o un orden de clasificación en la propiedad no indexada nunca coincidirán con esa entidad.

Nota: Si una propiedad figura en un índice compuesto por varias propiedades, configurarla como no indexada evitará que se indexe en el índice compuesto.

Por ejemplo, supongamos que una entidad tiene propiedades ab, y que deseas crear un índice capaz satisfacer consultas como WHERE a ="bike" and b="red". Supongamos también que no te interesan las consultas WHERE a="bike" ni WHERE b="red". Si estableces a como no indexada y creas un índice para ab, Datastore no creará entradas de índice en los índices de ab y, por lo tanto, la consulta WHERE a="bike" and b="red" no funcionará. A fin de que Datastore cree entradas para los índices de ab, a y b deben estar indexadas.

En la API de Datastore de Java de nivel bajo, las propiedades se definen como indexadas o no indexadas por entidad, según el método que uses para configurarlas (setProperty() o setUnindexedProperty()):

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());

Para cambiar una propiedad de indexada a no indexada, restablece su valor con setUnindexedProperty(). Si deseas hacer lo contrario, restablécelo con setProperty().

Sin embargo, ten en cuenta que cambiar una propiedad de no indexada a indexada no afecta a ninguna entidad existente que se haya creado antes del cambio. Las consultas que filtren según la propiedad no mostrarán esas entidades existentes, ya que las entidades no estaban escritas en el índice de la consulta cuando se crearon. A fin de que las consultas futuras puedan acceder a las entidades, debes volver a escribirlas en Datastore para que se ingresen en los índices correspondientes. Es decir, debes hacer lo siguiente para cada una de esas entidades:

  1. Recuperar (get) la entidad de Datastore
  2. Escribir (put) la entidad de nuevo en Datastore

Del mismo modo, cambiar una propiedad de indexada a no indexada solo afecta a las entidades que se escribirán en Datastore más adelante. Las entradas de índice de cualquier entidad actual con esa propiedad existirán hasta que se actualicen o borren las entidades. Para evitar resultados no deseados, debes borrar definitivamente todas las consultas de tu código que filtran o clasifican según la propiedad (ahora no indexada).

Límites de índice

Datastore impone límites al número y tamaño general de las entradas de índice que se pueden asociar con una sola entidad. Estos límites son grandes y la mayoría de las aplicaciones no se ven afectadas. Sin embargo, hay circunstancias en las que puedes alcanzar estos límites.

Como se describió antes, Datastore crea una entrada en un índice predefinido para cada propiedad de cada entidad, excepto las strings de texto largas (Text), las strings de bytes largas (Blob) y las entidades incorporadas (EmbeddedEntity), y las que declaraste de forma explícita como no indexadas. La propiedad también se puede incluir en índices personalizados adicionales declarados en el archivo de configuración datastore-indexes.xml. Siempre que una entidad no tenga propiedades de lista, tendrá como máximo una entrada en cada índice personalizado (para índices que no sean principales) o una para cada una de las principales de la entidad (para índices principales). Cada una de estas entradas de índice debe actualizarse cada vez que cambie el valor de la propiedad.

En el caso de una propiedad que tiene un único valor para cada entidad, cada valor posible debe almacenarse solo una vez por entidad en el índice predefinido de la propiedad. Pese a ello, es posible que una entidad con un gran número de estas propiedades de un único valor supere el límite de entradas o de tamaño del índice. De manera similar, una entidad que puede tener múltiples valores para la misma propiedad requiere una entrada de índice distinta para cada valor. Igualmente, si el número de valores posibles es grande, esta entidad puede superar el límite de entradas.

La situación empeora en el caso de las entidades con varias propiedades, cada una de las cuales puede asumir diversos valores. A fin de alojar una entidad de ese tipo, el índice debe incluir una entrada para cada combinación posible de valores de propiedad. Los índices personalizados que hacen referencia a varias propiedades, cada una con múltiples valores, son susceptibles a un “crecimiento” combinatorio y pueden requerir una gran cantidad de entradas para una entidad con una cantidad relativamente pequeña de valores de propiedad posibles. Estos índices con alto crecimiento pueden aumentar de forma considerable el costo de escritura de una entidad en Datastore, debido a la gran cantidad de entradas de índice que se deben actualizar y, también, pueden hacer que la entidad exceda el límite de tamaño o entrada de índice con facilidad.

Considera la consulta

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);

Esta hace que el SDK sugiera el siguiente índice:

<?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>
Este índice requerirá un total de |x| * |y| * |date| entradas para cada entidad (en el que |x| denota la cantidad de valores asociados con la entidad de la propiedad x). Por ejemplo, observa el siguiente código
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);

Con él, se crea una entidad con cuatro valores para la propiedad x, tres valores para la propiedad y, y date establecida en la fecha actual. Esto requerirá 12 entradas de índice, una para cada combinación posible de valores de propiedad:

(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>)

Cuando la misma propiedad se repite varias veces, Datastore puede detectar índices con alto crecimiento y sugerir un índice alternativo. Sin embargo, en todas las demás circunstancias (como la consulta definida en este ejemplo), Datastore generará un índice con alto crecimiento. En este caso, puedes evitar el índice con alto crecimiento si configuras de forma manual un índice en tu archivo de configuración de índices:

<?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>
Esto reduce la cantidad de entradas necesarias a solo (|x| * |date| + |y| * |date|), o 7 entradas en lugar de 12:

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

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

Cualquier operación put que haga que un índice supere los límites de entradas o de tamaño, fallará con una IllegalArgumentException. En el texto de la excepción, se describe qué límite se superó ("Too many indexed properties" o "Index entries too large") y qué índice personalizado fue la causa. Si creas un nuevo índice que excede los límites para cualquier entidad cuando se compila, las consultas en el índice fallarán y aparecerá con el estado Error en la consola de Google Cloud. Para resolver los índices con el estado Error, sigue estos pasos:

  1. Quita el índice en el estado Error del archivo datastore-indexes.xml.

  2. Ejecuta el siguiente comando desde el directorio en el que se encuentra el datastore-indexes.xml para quitar ese índice de Datastore:

    gcloud datastore indexes cleanup datastore-indexes.xml
    
  3. Resuelve la causa del error. Por ejemplo:

    • Reformula la definición del índice y las consultas correspondientes.
    • Quita las entidades que generan el alto crecimiento del índice.
  4. Vuelve a agregar el índice al archivo datastore-indexes.xml.

  5. Ejecuta el siguiente comando desde el directorio en el que se encuentra el datastore-indexes.xml para crear el índice en Datastore:

    gcloud datastore indexes create datastore-indexes.xml
    

Puedes evitar los índices con alto crecimiento si evitas las consultas que requieren un índice personalizado con una propiedad de lista. Como se describió antes, esto incluye consultas con varios órdenes de clasificación o con una mezcla de filtros de igualdad y desigualdad.