Índices da Pesquisa

Nesta página, descrevemos como adicionar e usar índices de pesquisa. A pesquisa de texto completo é executada em entradas no índice de pesquisa.

Como usar índices de pesquisa

É possível criar um índice de pesquisa em qualquer coluna que você queira disponibilizar para pesquisas de texto completo. Para criar um índice de pesquisa, use a instrução DDL CREATE SEARCH INDEX. Para atualizar um índice, use a instrução DDL ALTER SEARCH INDEX. O Spanner cria e mantém automaticamente o índice de pesquisa, incluindo a adição e a atualização de dados no índice de pesquisa assim que eles mudam no banco de dados.

Partições do índice de pesquisa

Um índice de pesquisa pode ser particionado ou não particionado, dependendo do tipo de consultas que você quer acelerar.

  • Um exemplo de quando um índice particionado é a melhor escolha é quando o aplicativo consulta uma caixa de e-mail. Cada consulta é restrita a uma caixa de correio específica.

  • Um exemplo de quando uma consulta sem particionamento é a melhor opção é quando há uma consulta em todas as categorias de produtos em um catálogo.

Casos de uso do índice de pesquisa

Além da pesquisa de texto completo, os índices de pesquisa do Spanner oferecem suporte ao seguinte:

  • Pesquisas JSON, que são uma maneira eficiente de indexar e consultar documentos JSON e JSONB.
  • Pesquisas de substring, que é um tipo de consulta que procura uma string menor (a substring) dentro de um texto maior.
  • Combinar condições em qualquer subconjunto de dados indexados, incluindo correspondência exata e numérica, em uma única verificação de índice.

Para mais informações sobre casos de uso, consulte Pesquisa x índices secundários.

Exemplo de índice de pesquisa

Para mostrar as funcionalidades dos índices de pesquisa, suponha que haja uma tabela que armazena informações sobre álbuns 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));

O Spanner tem várias funções de tokenização que criam tokens. Para modificar a tabela anterior e permitir que os usuários façam uma pesquisa de texto completo para encontrar títulos de álbuns, use a função TOKENIZE_FULLTEXT para criar tokens com base nos títulos. Em seguida, crie uma coluna que use o tipo de dados TOKENLIST para armazenar a saída de tokenização de TOKENIZE_FULLTEXT. Neste exemplo, criamos a coluna 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;

O exemplo a seguir usa a DDL CREATE SEARCH INDEX para criar um índice de pesquisa (AlbumsIndex) nos tokens AlbumTitle (AlbumTitle_Tokens):

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

PostgreSQL

Este exemplo usa CREATE SEARCH INDEX.

CREATE SEARCH INDEX albumsindex ON albums(albumtitle_tokens);

Depois de adicionar o índice de pesquisa, use consultas SQL para encontrar álbuns que correspondam aos critérios de pesquisa. Exemplo:

GoogleSQL

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

PostgreSQL

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

Consistência de dados

Quando um índice é criado, o Spanner usa processos automatizados para preencher os dados e garantir a consistência. Quando as gravações são confirmadas, os índices são atualizados na mesma transação. O Spanner realiza automaticamente verificações de consistência de dados.

Definições de esquema do índice de pesquisa

Os índices de pesquisa são definidos em uma ou mais colunas TOKENLIST de uma tabela. Os índices de pesquisa têm os seguintes componentes:

  • Tabela de base: a tabela do Spanner que precisa de indexação.
  • Coluna TOKENLIST: uma coleção de colunas que definem os tokens que precisam de indexação. A ordem dessas colunas não é importante.

Por exemplo, na instrução a seguir, a tabela de base é "Albums". As colunas TOKENLIST são criadas em AlbumTitle (AlbumTitle_Tokens) e 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));

Use a seguinte instrução CREATE SEARCH INDEX para criar um índice de pesquisa usando os tokens de AlbumTitle e 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

Os índices de pesquisa têm as seguintes opções:

  • Partições: um grupo opcional de colunas que dividem o índice de pesquisa. Consultar um índice particionado costuma ser muito mais eficiente do que consultar um índice não particionado. Para mais informações, consulte Particionar índices de pesquisa.
  • Coluna de ordem de classificação: uma coluna INT64 opcional que estabelece a ordem de recuperação do índice de pesquisa. Para mais informações, consulte Ordem de classificação do índice de pesquisa.
  • Intercalação: assim como os índices secundários, é possível intercalar índices de pesquisa. Os índices de pesquisa intercalados usam menos recursos para gravar e fazer junção com a tabela de base. Para mais informações, consulte Índices de pesquisa intercalada.
  • Cláusula "options": uma lista de pares de chave-valor que substitui as configurações padrão do índice de pesquisa.

Layout interno dos índices de pesquisa

Um elemento importante da representação interna dos índices de pesquisa é um docid, que serve como uma representação eficiente em termos de armazenamento da chave primária da tabela de base, que pode ser arbitrariamente longa. É também o que cria a ordem do layout de dados interno de acordo com as colunas ORDER BY fornecidas pelo usuário da instrução CREATE SEARCH INDEX. Ele é representado como um ou dois números inteiros de 64 bits.

Os índices de pesquisa são implementados internamente como um mapeamento de dois níveis:

  1. Tokens para docids
  2. Docids para chaves primárias da tabela de base

Esse esquema resulta em uma economia significativa de armazenamento, já que o Spanner não precisa armazenar a chave primária completa da tabela de base para cada par <token, document>.

Há dois tipos de índices físicos que implementam os dois níveis de mapeamento:

  1. Um índice secundário que mapeia chaves de partição e um docid para a chave primária da tabela de base. No exemplo da seção anterior, isso mapeia {SingerId, ReleaseTimestamp, uid} para {AlbumId}. O índice secundário também armazena todas as colunas especificadas na cláusula STORING de CREATE SEARCH INDEX.
  2. Um índice de token que mapeia tokens para docids, semelhante aos índices invertidos na literatura de recuperação de informações. O Spanner mantém um índice de token separado para cada TOKENLIST do índice de pesquisa. Logicamente, os índices de token mantêm listas de docids para cada token em cada partição (conhecidas na recuperação de informações como listas de postagens). As listas são ordenadas por tokens para recuperação rápida, e dentro das listas, o docid é usado para ordenação. Os índices de token individuais são um detalhe de implementação que não é exposto pelas APIs do Spanner.

O Spanner é compatível com as quatro opções a seguir para docid.

Índice da Pesquisa Docid Comportamento
A cláusula ORDER BY é omitida para o índice de pesquisa {uid} O Spanner adiciona um valor exclusivo oculto (UID) para identificar cada linha.
ORDER BY column {column, uid} O Spanner adiciona a coluna UID como um critério de desempate entre linhas com os mesmos valores de column em uma partição.

Observações sobre o uso:

  • A coluna de UID interno não é exposta pela API do Spanner.
  • Em índices em que o UID não é adicionado, as transações que adicionam uma linha com uma ordem de partição/classificação já existente falham.

Por exemplo, considere os seguintes dados:

AlbumId SingerId ReleaseTimestamp SongTitle
a1 1 997 Dias lindos
a2 1 743 Olhos lindos

Supondo que a coluna de pré-ordenação esteja em ordem crescente, o conteúdo do índice de token particionado por SingerId particiona o conteúdo do índice de token da seguinte maneira:

SingerId _token ReleaseTimestamp uid
1 linda 743 uid1
1 linda 997 uid2
1 dias 743 uid1
1 olhos 997 uid2

Fragmentação do índice de pesquisa

Quando o Spanner divide uma tabela, ele distribui os dados do índice de pesquisa para que todos os tokens em uma determinada linha da tabela base estejam na mesma divisão. Em outras palavras, o índice de pesquisa é fragmentado por documento. Essa estratégia de fragmentação tem implicações significativas na performance:

  1. O número de servidores com que cada transação se comunica permanece constante, independente do número de tokens ou de colunas TOKENLIST indexadas.
  2. As consultas de pesquisa que envolvem várias expressões condicionais são executadas de forma independente em cada divisão, evitando a sobrecarga de performance associada a uma junção distribuída.

Os índices de pesquisa têm dois modos de distribuição:

  • Fragmentação uniforme (padrão). No sharding uniforme, os dados indexados de cada linha da tabela de base são atribuídos aleatoriamente a uma divisão de índice de uma partição.
  • Fragmentação por ordem de classificação. No sharding por ordem de classificação, os dados de cada linha da tabela de base são atribuídos a uma divisão de índice de uma partição com base nas colunas ORDER BY (ou seja, colunas de pré-classificação). Por exemplo, no caso de uma ordem de classificação decrescente, todas as linhas com os maiores valores de ordem de classificação aparecem na primeira divisão de índice de uma partição, e o próximo maior grupo de valores de ordem de classificação na próxima divisão.

Esses modos de fragmentação têm uma compensação entre os riscos de hotspot e o custo da consulta:

  • Os índices de pesquisa fragmentados uniformes são recomendados quando padrões de leitura ou gravação no índice de pesquisa podem levar a pontos de acesso. O sharding uniforme reduz os pontos de acesso distribuindo a carga de leitura e gravação de maneira uniforme entre as divisões, mas isso pode aumentar o uso de recursos durante as execuções de consultas como compensação. Em índices de pesquisa fragmentados uniformes, as consultas precisam ler todas as divisões em uma partição devido aos dados distribuídos aleatoriamente. Ao acessar índices fragmentados de maneira uniforme, o Spanner lê todas as divisões em paralelo para reduzir a latência geral da consulta.
  • Os índices de pesquisa fragmentados por ordem de classificação são preferíveis quando é improvável que padrões de leitura ou gravação causem pontos de acesso. Essa abordagem pode reduzir o custo de consultas cujo ORDER BY corresponde ao ORDER BY do índice e especifica um LIMIT relativamente baixo. Ao executar essas consultas, o Spanner lê incrementalmente a partir das primeiras divisões de uma partição, e a consulta pode ser concluída sem ler todas as divisões quando LIMIT pode ser satisfeita antecipadamente.
  • O modo de fragmentação de um índice de pesquisa é configurado usando a 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

O modo de fragmentação de um índice de pesquisa é configurado usando a 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);

Quando sort_order_sharding=false é definido ou deixado sem especificação, o índice de pesquisa é criado usando sharding uniforme.

Índices de pesquisa intercalados

Assim como os índices secundários, é possível intercalar índices de pesquisa em uma tabela pai da tabela de base. O principal motivo para usar índices de pesquisa intercalados é alocar dados da tabela de base com dados de índice para partições pequenas. Essa colocation oportunista tem as seguintes vantagens:

  • As gravações não precisam fazer um commit de duas fases.
  • As junções de retorno do índice de pesquisa com a tabela base não são distribuídas.

Os índices de pesquisa intercalados têm as seguintes restrições:

  1. Somente índices fragmentados por ordem de classificação podem ser intercalados.
  2. Os índices de pesquisa só podem ser intercalados em tabelas de nível superior, não em tabelas filhas.
  3. Assim como nas tabelas intercaladas e nos índices secundários, faça da chave da tabela pai um prefixo das colunas PARTITION BY no índice de pesquisa intercalado.

Definir um índice de pesquisa intercalado

O exemplo a seguir demonstra como definir um índice de pesquisa 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);

Ordem de classificação do índice de pesquisa

Os requisitos para a definição da ordem de classificação do índice de pesquisa são diferentes dos índices secundários.

Por exemplo, considere a tabela a seguir:

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

O aplicativo pode definir um índice secundário para pesquisar informações usando o AlbumName classificado por ReleaseTimestamp:

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

O índice de pesquisa equivalente é assim (ele usa a tokenização de correspondência exata, já que os índices secundários não oferecem suporte a pesquisas de texto completo):

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

A ordem de classificação do índice de pesquisa precisa obedecer aos seguintes requisitos:

  1. Use apenas colunas INT64 para a ordem de classificação de um índice de pesquisa. Colunas com tamanhos arbitrários usam muitos recursos no índice de pesquisa porque o Spanner precisa armazenar um docid ao lado de cada token. Especificamente, a coluna de ordem de classificação não pode usar o tipo TIMESTAMP porque ele usa precisão de nanossegundos, que não cabe em um inteiro de 64 bits.TIMESTAMP
  2. As colunas de ordem de classificação não podem ser NULL. Há duas maneiras de atender a esse requisito:

    1. Declare a coluna de ordem de classificação como NOT NULL.
    2. Configure o índice para excluir valores NULL.

Um carimbo de data/hora costuma ser usado para determinar a ordem de classificação. Uma prática comum é usar microssegundos desde a época do Unix para esses carimbos de data/hora.

Os aplicativos geralmente recuperam os dados mais recentes primeiro usando um índice de pesquisa classificado em ordem decrescente.

Índices de pesquisa filtrados por NULL

Os índices de pesquisa podem usar a sintaxe WHERE column_name IS NOT NULL para excluir linhas da tabela de base. A filtragem de NULL pode ser aplicada a chaves de particionamento, colunas de ordem de classificação e colunas armazenadas. Não é permitido filtrar NULL em colunas de array armazenadas.

Exemplo

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

A consulta precisa especificar a condição de filtragem NULL (Genre IS NOT NULL neste exemplo) na cláusula WHERE. Caso contrário, o otimizador de consultas não poderá usar o índice de pesquisa. Para mais informações, consulte Requisitos de consulta SQL.

Use a filtragem NULL em uma coluna gerada para excluir linhas com base em qualquer critério arbitrário. Para mais informações, consulte Criar um índice parcial usando uma coluna gerada.

A seguir