Índices de búsqueda

En esta página, se describe cómo agregar y usar índices de búsqueda. La búsqueda en el texto completo se ejecuta en las entradas del índice de búsqueda.

Cómo usar los índices de búsqueda

Puedes crear un índice de búsqueda en cualquier columna que desees que esté disponible para las búsquedas de texto completo. Para crear un índice de búsqueda, usa la declaración DDL CREATE SEARCH INDEX. Para actualizar un índice, usa la declaración DDL ALTER SEARCH INDEX. Spanner compila y mantiene automáticamente el índice de búsqueda, lo que incluye agregar y actualizar datos en el índice de búsqueda tan pronto como cambian en la base de datos.

Particiones del índice de búsqueda

Un índice de búsqueda puede ser particionado o sin particiones, según el tipo de consultas que desees acelerar.

  • Un ejemplo de cuándo un índice particionado es la mejor opción es cuando la aplicación consulta un buzón de correo electrónico. Cada búsqueda se limita a un buzón específico.

  • Un ejemplo de cuándo una consulta sin particiones es la mejor opción es cuando hay una consulta en todas las categorías de productos de un catálogo de productos.

Casos de uso del índice de búsqueda

Además de la búsqueda en el texto completo, los índices de búsqueda de Spanner admiten lo siguiente:

  • Búsquedas en JSON, que es una forma eficiente de indexar y consultar documentos JSON y JSONB
  • Búsquedas de subcadenas, que son un tipo de búsqueda que busca una cadena más corta (la subcadena) dentro de un cuerpo de texto más grande.
  • Combinar condiciones en cualquier subconjunto de datos indexados, incluidas las coincidencias exactas y las numéricas, en un solo análisis de índice

Para obtener más información sobre los casos de uso, consulta Búsqueda en comparación con los índices secundarios.

Ejemplo de índice de búsqueda

Para mostrar las capacidades de los índices de búsqueda, supongamos que hay una tabla que almacena información sobre álbumes de música:

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX)
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
PRIMARY KEY(albumid));

Spanner tiene varias funciones de tokenización que crean tokens. Para modificar la tabla anterior y permitir que los usuarios ejecuten una búsqueda de texto completo para encontrar títulos de álbumes, usa la función TOKENIZE_FULLTEXT para crear tokens a partir de los títulos de álbumes. Luego, crea una columna que use el tipo de datos TOKENLIST para contener el resultado de la tokenización de TOKENIZE_FULLTEXT. Para este ejemplo, creamos la columna AlbumTitle_Tokens.

GoogleSQL

ALTER TABLE Albums
  ADD COLUMN AlbumTitle_Tokens TOKENLIST
  AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN;

PostgreSQL

ALTER TABLE albums
  ADD COLUMN albumtitle_tokens spanner.tokenlist
    GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN;

En el siguiente ejemplo, se usa el DDL CREATE SEARCH INDEX para crear un índice de búsqueda (AlbumsIndex) en los tokens AlbumTitle (AlbumTitle_Tokens):

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

PostgreSQL

En este ejemplo, se usa CREATE SEARCH INDEX.

CREATE SEARCH INDEX albumsindex ON albums(albumtitle_tokens);

Después de agregar el índice de búsqueda, usa consultas SQL para encontrar los álbumes que coincidan con los criterios de búsqueda. Por ejemplo:

GoogleSQL

SELECT AlbumId
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")

PostgreSQL

SELECT albumid
FROM albums
WHERE spanner.search(albumtitle_tokens, 'fifth symphony')

Coherencia de los datos

Cuando se crea un índice, Spanner usa procesos automatizados para completar los datos de forma retroactiva y garantizar la coherencia. Cuando se confirman las escrituras, los índices se actualizan en la misma transacción. Spanner realiza automáticamente verificaciones de coherencia de datos.

Definiciones de esquema del índice de búsqueda

Los índices de búsqueda se definen en una o más columnas TOKENLIST de una tabla. Los índices de búsqueda tienen los siguientes componentes:

  • Tabla base: Es la tabla de Spanner que necesita indexación.
  • Columna TOKENLIST: Es una colección de columnas que definen los tokens que se deben indexar. El orden de estas columnas no es importante.

Por ejemplo, en la siguiente instrucción, la tabla base es Albums. Las columnas de TOKENLIST se crean en AlbumTitle (AlbumTitle_Tokens) y Rating (Rating_Tokens).

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  SingerId INT64 NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumTitle STRING(MAX),
  Rating FLOAT64,
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN,
  Rating_Tokens TOKENLIST AS (TOKENIZE_NUMBER(Rating)) HIDDEN
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  singerid bigint NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumtitle character varying,
  rating double precision,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN,
  rating_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(rating)) VIRTUAL HIDDEN,
PRIMARY KEY(AlbumId));

Usa la siguiente instrucción CREATE SEARCH INDEX para crear un índice de búsqueda con los tokens de AlbumTitle y Rating:

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC

Los índices de búsqueda tienen las siguientes opciones:

  • Particiones: Es un grupo opcional de columnas que dividen el índice de búsqueda. Consultar un índice particionado suele ser mucho más eficiente que consultar un índice sin particionar. Para obtener más información, consulta Cómo particionar índices de búsqueda.
  • Columna de ordenamiento: Es una columna INT64 opcional que establece el orden de recuperación del índice de búsqueda. Para obtener más información, consulta Orden de clasificación del índice de búsqueda.
  • Intercalación: Al igual que con los índices secundarios, puedes intercalar índices de búsqueda. Los índices de búsqueda intercalados usan menos recursos para escribir y unir con la tabla base. Para obtener más información, consulta Índices de búsqueda intercalados.
  • Cláusula de opciones: Es una lista de pares clave-valor que anula la configuración predeterminada del índice de búsqueda.

Diseño interno de los índices de búsqueda

Un elemento importante de la representación interna de los índices de búsqueda es un docid, que sirve como una representación eficiente en el almacenamiento de la clave primaria de la tabla base, que puede ser arbitrariamente larga. También es lo que crea el orden para el diseño de datos interno según las columnas ORDER BY proporcionadas por el usuario de la instrucción CREATE SEARCH INDEX. Se representa como uno o dos números enteros de 64 bits.

Internamente, los índices de búsqueda se implementan como una asignación de dos niveles:

  1. Tokens para docids
  2. Asigna IDs de documentos a claves primarias de tablas base

Este esquema genera un ahorro significativo de almacenamiento, ya que Spanner no necesita almacenar la clave primaria completa de la tabla base para cada par de <token, document>.

Existen dos tipos de índices físicos que implementan los dos niveles de asignación:

  1. Un índice secundario que asigna claves de partición y un docid a la clave primaria de la tabla base. En el ejemplo de la sección anterior, esto asigna {SingerId, ReleaseTimestamp, uid} a {AlbumId}. El índice secundario también almacena todas las columnas especificadas en la cláusula STORING de CREATE SEARCH INDEX.
  2. Un índice de tokens que asigna tokens a docids, similar a los índices invertidos en la literatura de recuperación de información. Spanner mantiene un índice de tokens independiente para cada TOKENLIST del índice de búsqueda. Lógicamente, los índices de tokens mantienen listas de docids para cada token dentro de cada partición (conocidas en la recuperación de información como listas de publicaciones). Las listas se ordenan por tokens para una recuperación rápida y, dentro de las listas, se usa el docid para el ordenamiento. Los índices de tokens individuales son un detalle de implementación que no se expone a través de las APIs de Spanner.

Spanner admite las siguientes cuatro opciones para docid.

Índice de la Búsqueda Docid Comportamiento
Se omitió la cláusula ORDER BY para el índice de búsqueda {uid} Spanner agrega un valor único (UID) oculto para identificar cada fila.
ORDER BY column {column, uid} Spanner agrega la columna UID como desempate entre las filas con los mismos valores de column dentro de una partición.

Notas de uso:

  • La columna del UID interno no se expone a través de la API de Spanner.
  • En los índices en los que no se agrega el UID, fallan las transacciones que agregan una fila con un par (partición,orden de clasificación) ya existente.

Por ejemplo, considera los siguientes datos:

AlbumId SingerId ReleaseTimestamp SongTitle
a1 1 997 Días hermosos
a2 1 743 Ojos hermosos

Si se supone que la columna de clasificación previa está en orden ascendente, el contenido del índice de tokens particionado por SingerId particiona el contenido del índice de tokens de la siguiente manera:

SingerId _token ReleaseTimestamp uid
1 hermosa 743 uid1
1 hermosa 997 uid2
1 días 743 uid1
1 ojos 997 uid2

Fragmentación del índice de búsqueda

Cuando Spanner divide una tabla, distribuye los datos del índice de búsqueda de modo que todos los tokens de una fila de tabla base en particular se encuentren en la misma división. En otras palabras, el índice de búsqueda se fragmenta por documentos. Esta estrategia de fragmentación tiene implicaciones significativas en el rendimiento:

  1. La cantidad de servidores con los que se comunica cada transacción permanece constante, independientemente de la cantidad de tokens o de columnas TOKENLIST indexadas.
  2. Las búsquedas que involucran varias expresiones condicionales se ejecutan de forma independiente en cada división, lo que evita la sobrecarga de rendimiento asociada con una unión distribuida.

Los índices de búsqueda tienen dos modos de distribución:

  • Fragmentación uniforme (predeterminada). En el sharding uniforme, los datos indexados de cada fila de la tabla base se asignan de forma aleatoria a una división del índice de una partición.
  • Fragmentación por orden de clasificación. En el sharding por orden de clasificación, los datos de cada fila de la tabla base se asignan a una división de índice de una partición según las columnas ORDER BY (es decir, las columnas de clasificación previa). Por ejemplo, en el caso de un orden de clasificación descendente, todas las filas con los valores de orden de clasificación más grandes aparecen en la primera división del índice de una partición, y el siguiente grupo más grande de valores de orden de clasificación en la siguiente división.

Estos modos de fragmentación tienen una compensación entre los riesgos de puntos de acceso y el costo de la consulta:

  • Se recomiendan los índices de búsqueda fragmentados uniformes cuando los patrones de lectura o escritura en el índice de búsqueda podrían generar puntos calientes. El sharding uniforme mitiga los puntos calientes distribuyendo la carga de lectura y escritura de manera uniforme entre las divisiones, pero esto podría aumentar el uso de recursos durante las ejecuciones de consultas como una compensación. En los índices de búsqueda fragmentados uniformes, las consultas deben leer todas las divisiones dentro de una partición debido a la distribución aleatoria de los datos. Cuando se accede a índices fragmentados de manera uniforme, Spanner lee todas las divisiones en paralelo para reducir la latencia general de la consulta.
  • Los índices de búsqueda fragmentados por orden de clasificación son preferibles cuando es poco probable que los patrones de lectura o escritura causen hotspots. Este enfoque puede reducir el costo de las consultas cuyo ORDER BY coincide con el ORDER BY del índice y especifica un LIMIT relativamente bajo. Cuando se ejecutan estas consultas, Spanner lee de forma incremental a partir de las primeras divisiones de una partición, y la consulta puede completarse sin leer todas las divisiones cuando LIMIT se puede satisfacer de forma anticipada.
  • El modo de fragmentación de un índice de búsqueda se configura con la cláusula OPTIONS.

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC
OPTIONS (sort_order_sharding = true);

PostgreSQL

El modo de fragmentación de un índice de búsqueda se configura con la cláusula WITH.

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC
WITH (sort_order_sharding = true);

Cuando se establece sort_order_sharding=false o no se especifica, el índice de búsqueda se crea con el sharding uniforme.

Índices de búsqueda intercalados

Al igual que los índices secundarios, puedes intercalar índices de búsqueda en una tabla principal de la tabla base. El motivo principal para usar índices de búsqueda intercalados es ubicar los datos de la tabla base junto con los datos del índice para particiones pequeñas. Esta colocación oportunista tiene las siguientes ventajas:

  • Las escrituras no necesitan realizar una confirmación de dos fases.
  • Las uniones inversas del índice de búsqueda con la tabla base no se distribuyen.

Los índices de búsqueda intercalados tienen las siguientes restricciones:

  1. Solo se pueden intercalar los índices fragmentados por orden de clasificación.
  2. Los índices de búsqueda solo se pueden intercalar en tablas de nivel superior (no en tablas secundarias).
  3. Al igual que con las tablas intercaladas y los índices secundarios, haz que la clave de la tabla principal sea un prefijo de las columnas PARTITION BY en el índice de búsqueda intercalado.

Define un índice de búsqueda intercalado

En el siguiente ejemplo, se muestra cómo definir un índice de búsqueda intercalado:

GoogleSQL

CREATE TABLE Singers (
  SingerId INT64 NOT NULL
) PRIMARY KEY(SingerId);

CREATE TABLE Albums (
  SingerId INT64 NOT NULL,
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN
) PRIMARY KEY(SingerId, AlbumId),
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
PARTITION BY SingerId,
INTERLEAVE IN Singers
OPTIONS (sort_order_sharding = true);

PostgreSQL

CREATE TABLE singers(
  singerid bigint NOT NULL
PRIMARY KEY(singerid));

CREATE TABLE albums(
  singerid bigint NOT NULL,
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist
  GENERATED ALWAYS
AS (
  spanner.tokenize_fulltext(albumtitle)
) VIRTUAL HIDDEN,
  PRIMARY KEY(singerid, albumid)),
INTERLEAVE IN PARENT singers ON DELETE CASCADE;

CREATE
  SEARCH INDEX albumsindex
ON
  albums(albumtitle_tokens)
  PARTITION BY singerid INTERLEAVE IN singers WITH(sort_order_sharding = true);

Orden de clasificación del índice de búsqueda

Los requisitos para la definición del orden de clasificación del índice de búsqueda son diferentes de los de los índices secundarios.

Por ejemplo, considera la siguiente tabla:

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumName STRING(MAX),
  AlbumName_Token TOKENLIST AS (TOKEN(AlbumName)) HIDDEN
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumname character varying,
  albumname_token spanner.tokenlist
      GENERATED ALWAYS AS(spanner.token(albumname)) VIRTUAL HIDDEN,
PRIMARY KEY(albumid));

La aplicación podría definir un índice secundario para buscar información con AlbumName ordenado por ReleaseTimestamp:

CREATE INDEX AlbumsSecondaryIndex ON Albums(AlbumName, ReleaseTimestamp DESC);

El índice de búsqueda equivalente se ve de la siguiente manera (usa la tokenización de concordancia exacta, ya que los índices secundarios no admiten búsquedas de texto completo):

CREATE SEARCH INDEX AlbumsSearchIndex
ON Albums(AlbumName_Token)
ORDER BY ReleaseTimestamp DESC;

El orden de clasificación del índice de búsqueda debe cumplir con los siguientes requisitos:

  1. Solo usa columnas de INT64 para el orden de clasificación de un índice de búsqueda. Las columnas que tienen tamaños arbitrarios usan demasiados recursos en el índice de búsqueda porque Spanner necesita almacenar un docid junto a cada token. Específicamente, la columna de ordenamiento no puede usar el tipo TIMESTAMP porque este usa una precisión de nanosegundos que no cabe en un número entero de 64 bits.TIMESTAMP
  2. Las columnas de ordenamiento no deben ser NULL. Existen dos formas de cumplir con este requisito:

    1. Declara la columna de orden de clasificación como NOT NULL.
    2. Configura el índice para excluir los valores NULL.

A menudo, se usa una marca de tiempo para determinar el orden de clasificación. Una práctica habitual es usar microsegundos desde la época de Unix para estas marcas de tiempo.

Por lo general, las aplicaciones recuperan primero los datos más recientes con un índice de búsqueda ordenado de forma descendente.

Índices de búsqueda filtrados por NULL

Los índices de búsqueda pueden usar la sintaxis WHERE column_name IS NOT NULL para excluir filas de la tabla base. El filtrado de valores NULL se puede aplicar a las claves de partición, las columnas de ordenamiento y las columnas almacenadas. No se permite el filtrado de valores NULL en columnas de arrays almacenados.

Ejemplo

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
STORING (Genre)
WHERE Genre IS NOT NULL

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens)
INCLUDE (genre)
WHERE genre IS NOT NULL

La consulta debe especificar la condición de filtrado NULL (Genre IS NOT NULL para este ejemplo) en la cláusula WHERE. De lo contrario, el optimizador de consultas no podrá usar el índice de búsqueda. Para obtener más información, consulta Requisitos de las consultas SQL.

Usa el filtrado de NULL en una columna generada para excluir filas según cualquier criterio arbitrario. Para obtener más información, consulta Crea un índice parcial con una columna generada.

¿Qué sigue?