Índices del almacén

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 índices llamado datastore-indexes.xml, que se genera en el directorio /war/WEB-INF/appengine-generated de la aplicación. El servidor de desarrollo añade automáticamente sugerencias a este archivo cuando detecta consultas que no se pueden ejecutar con los índices actuales. Puedes ajustar estos índices manualmente modificando el archivo antes de subir la aplicación.

Nota: El mecanismo de consulta basado en índices admite una amplia gama de consultas y es adecuado para la mayoría de las aplicaciones. Sin embargo, no admite algunos tipos de consultas habituales en otras tecnologías de bases de datos. En concreto, las combinaciones y las consultas de agregación no se admiten en el motor de consultas de Datastore. Consulta la página Consultas de Datastore para ver las limitaciones de las consultas de Datastore.

Definición y estructura de índices

Un índice se define en una lista de propiedades de un tipo de entidad determinado, con un orden correspondiente (ascendente o descendente) para cada propiedad. Para usarlo con consultas de ancestros, el índice también puede incluir opcionalmente los ancestros de una entidad.

Una tabla de índice contiene una columna por cada propiedad cuyo nombre se indica en la definición del índice. Cada fila de la tabla representa una entidad de 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 definido para cada propiedad utilizada en el índice. Si la definición del índice hace referencia a una propiedad para la que la entidad no tiene ningún valor, esa entidad no aparecerá en el índice y, por lo tanto, nunca se devolverá como resultado de ninguna consulta basada en el índice.

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

Nota: Los índices compuestos por varias propiedades requieren que cada propiedad individual no se defina como sin indexar.

Las filas de una tabla de índice se ordenan primero por ancestro y, después, por valores de propiedad, en el orden especificado en la definición del índice. El índice perfecto de una consulta, que permite que la consulta se ejecute de la forma más eficiente posible, se define en las siguientes propiedades, por orden:

  1. Propiedades usadas en filtros de igualdad
  2. Propiedad usada en un filtro de desigualdad (solo puede haber una)
  3. Propiedades usadas en los criterios de ordenación

De esta forma, todos los resultados de cada posible ejecución de la consulta aparecen en filas consecutivas de la tabla. Datastore ejecuta una consulta con un índice perfecto siguiendo estos pasos:

  1. Identifica el índice correspondiente al tipo de consulta, las propiedades de filtro, los operadores de filtro y los criterios de ordenación.
  2. Analiza desde el principio del índice hasta la primera entidad que cumple todas las condiciones de filtro de la consulta.
  3. Sigue analizando el índice y devuelve cada entidad por turno hasta que
    • encuentra una entidad que no cumple las condiciones del filtro, o
    • llega al final del índice o
    • ha recogido el número máximo de resultados solicitado por la consulta.

Por ejemplo, echa un vistazo a esta 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 para esta consulta es una tabla de claves de entidades de tipo Person, con columnas para los valores de las propiedades lastName y height. El índice se ordena primero en orden ascendente por lastName y, después, en orden descendente por height.

Para generar estos índices, configura los índices 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 con la misma forma, pero con valores de filtro diferentes, usan el mismo índice. Por ejemplo, la siguiente consulta usa el mismo índice que la 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 automáticamente un índice para cada propiedad de cada tipo de entidad. Estos índices predefinidos son suficientes para realizar muchas consultas sencillas, como las que solo incluyen igualdades y las que incluyen desigualdades sencillas. En el resto de las consultas, la aplicación debe definir los índices que necesita en un archivo de configuración de índice llamado datastore-indexes.xml. Si la aplicación intenta realizar una consulta que no se puede ejecutar con los índices disponibles (ya sean predefinidos o especificados en el archivo de configuración de índice), la consulta fallará y se devolverá un error DatastoreNeedIndexException.

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

  • Consultas independientes del tipo que solo usan filtros de ancestro y de clave
  • Consultas que solo usan filtros de ancestro y de igualdad
  • Consultas que solo usan filtros de desigualdad (que se limitan a una sola propiedad)
  • Consultas que solo usan filtros de ancestro, filtros de igualdad en propiedades y filtros de desigualdad en claves
  • Consultas sin filtros y con un solo orden de clasificación en una propiedad, ya sea ascendente o descendente

Para otros tipos de consultas, es necesario especificar los índices en el archivo de configuración de índices, como las siguientes:

  • Consultas con filtros de ancestro y de desigualdad
  • Consultas con uno o varios filtros de desigualdad en una propiedad y uno o varios filtros de igualdad en otras propiedades
  • Consultas con un orden de clasificación de las claves en orden descendente
  • Consultas con varios criterios de ordenación

Índices y propiedades

A continuación, se indican algunas consideraciones especiales que debes tener en cuenta sobre los índices y cómo se relacionan con las propiedades de las entidades de Datastore:

Propiedades con tipos de valor mixtos

Cuando dos entidades tienen propiedades con el mismo nombre, pero con tipos de valor diferentes, un índice de la propiedad ordena las entidades primero por tipo de valor y, después, por un orden secundario adecuado para cada tipo. Por ejemplo, si dos entidades tienen una propiedad llamada age, una con un valor entero y otra con un valor de cadena, la entidad con el valor entero siempre precede a la que tiene el valor de cadena cuando se ordena por la propiedad age, independientemente de los valores de las propiedades.

Esto es especialmente importante en el caso de los números enteros y de coma flotante, que Datastore trata como tipos independientes. Como todos los números enteros se ordenan antes que todos los números de coma flotante, una propiedad con el valor entero 38 se ordena antes que una con el valor de coma flotante 37.5.

Propiedades sin indexar

Si sabes que nunca tendrás que filtrar ni ordenar por una propiedad concreta, puedes indicar a Datastore que no mantenga entradas de índice para esa propiedad declarándola como sin indexar. De esta forma, se reduce el coste de ejecutar tu aplicación, ya que disminuye el número de operaciones de escritura de Datastore que tiene que realizar. Una entidad con una propiedad sin indexar se comporta como si la propiedad no estuviera definida: las consultas con un filtro o un orden de clasificación en la propiedad sin indexar nunca coincidirán con esa entidad.

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

Por ejemplo, supongamos que una entidad tiene las propiedades a y b y que quiere crear un índice que pueda responder a consultas como WHERE a ="bike" and b="red". Supongamos que no te interesan las consultas WHERE a="bike" y WHERE b="red". Si asignas el valor "unindexed" a a y creas un índice para a y b, Datastore no creará entradas de índice para el índice de a y b, por lo que la consulta WHERE a="bike" and b="red" no funcionará. Para que Datastore cree entradas para los índices a y b, ambos deben estar indexados.

En la API de Datastore de Java de bajo nivel, las propiedades se definen como indexadas o no indexadas por entidad, en función del método que utilices para definirlas (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());

Puede cambiar una propiedad indexada a no indexada restableciendo su valor con setUnindexedProperty() o de no indexada a indexada restableciéndola con setProperty().

Sin embargo, tenga en cuenta que, si cambia una propiedad de sin indexar a indexada, no se verán afectadas las entidades que se hayan creado antes del cambio. Las consultas que filtren por la propiedad no devolverán esas entidades, ya que no se escribieron en el índice de la consulta cuando se crearon. Para que las entidades sean accesibles mediante consultas futuras, debes reescribirlas en Datastore para que se introduzcan en los índices correspondientes. Es decir, debes hacer lo siguiente con cada entidad de este tipo:

  1. Recupera (obtiene) la entidad de Datastore.
  2. Escribe (coloca) la entidad de nuevo en Datastore.

Del mismo modo, si cambia una propiedad de indexada a no indexada, solo se verán afectadas las entidades que se escriban posteriormente en Datastore. Las entradas de índice de las entidades que ya tengan esa propiedad seguirán existiendo hasta que se actualicen o se eliminen. Para evitar resultados no deseados, debes eliminar de tu código todas las consultas que filtren u ordenen por la propiedad (que ahora no está indexada).

Límites de los índices

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

Como se ha descrito más arriba, Datastore crea una entrada en un índice predefinido para cada propiedad de cada entidad, excepto para las cadenas de texto largas (Text), las cadenas de bytes largas (Blob) y las entidades insertadas (EmbeddedEntity), así como para las propiedades que hayas declarado explícitamente como sin indexar. 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 (en el caso de los índices que no son de ancestros) o una por cada uno de los ancestros de la entidad (en el caso de los índices de ancestros). 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 tenga un solo valor por entidad, cada valor posible debe almacenarse solo una vez por entidad en el índice predefinido de la propiedad. Aun así, es posible que una entidad con un gran número de propiedades de un solo valor supere el límite de tamaño o de entrada del índice. Del mismo modo, una entidad que puede tener varios valores para la misma propiedad requiere una entrada de índice independiente para cada valor. De nuevo, si el número de valores posibles es grande, dicha 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 adoptar varios valores. Para dar cabida a una entidad de este 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 varios valores, pueden "explotar" combinatoriamente, lo que requiere un gran número de entradas para una entidad con un número relativamente pequeño de valores de propiedad posibles. Estos índices explosivos pueden aumentar drásticamente el coste de escribir una entidad en Datastore debido al gran número de entradas de índice que se deben actualizar. Además, pueden provocar fácilmente que la entidad supere el límite de tamaño o de entradas de índice.

Tener en cuenta 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);

lo que provoca 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 por entidad (donde |x| denota el número de valores asociados a la entidad de la propiedad x). Por ejemplo, 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);

crea una entidad con cuatro valores para la propiedad x, tres valores para la propiedad y y date definida como la fecha actual. Esto requerirá 12 entradas de índice, una por 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 explosivos y sugerir un índice alternativo. Sin embargo, en el resto de los casos (como la consulta definida en este ejemplo), Datastore generará un índice explosivo. En este caso, puedes evitar que el índice se expanda configurando manualmente un índice en el 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>
De esta forma, solo se necesitan (|x| * |date| + |y| * |date|) entradas, es decir, 7 en lugar de 12:

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

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

Cualquier operación de inserción que provoque que un índice supere el límite de entrada o de tamaño del índice fallará con un error IllegalArgumentException . El texto de la excepción describe qué límite se ha superado ("Too many indexed properties" o "Index entries too large") y qué índice personalizado ha sido la causa. Si crea un índice que supere los límites de cualquier entidad cuando se compile, las consultas en el índice fallarán y el índice aparecerá en el estado Error en la consola Google Cloud . Para resolver los índices en el estado Error, haz lo siguiente:

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

  2. Ejecuta el siguiente comando desde el directorio en el que se encuentra tu 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.
    • Elimina las entidades que estén provocando que el índice se desborde.
  4. Vuelve a añadir el índice al archivo datastore-indexes.xml.

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

    gcloud datastore indexes create datastore-indexes.xml
    

Para evitar que los índices se disparen, no hagas consultas que requieran un índice personalizado con una propiedad de lista. Como se ha descrito anteriormente, esto incluye las consultas con varios criterios de ordenación o con una combinación de filtros de igualdad y desigualdad.