Índices de pesquisa

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

Como usar os índices de pesquisa

Pode criar um índice de pesquisa em quaisquer colunas que queira disponibilizar para pesquisas de texto integral. Para criar um índice de pesquisa, use a declaração DDL CREATE SEARCH INDEX. Para atualizar um índice, use a declaraçã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 são alterados na base de dados.

Partições do índice de pesquisa

Um índice de pesquisa pode ser particionado ou não particionado, consoante o tipo de consultas que quer acelerar.

  • Um exemplo de quando um índice particionado é a melhor escolha é quando a aplicação consulta uma caixa de correio eletrónico. Cada consulta está restrita a uma caixa de correio específica.

  • Um exemplo de quando uma consulta não particionada é a melhor escolha é quando existe uma consulta em todas as categorias de produtos num catálogo de produtos.

Exemplos de utilização do índice da Pesquisa

Além da pesquisa de texto completo, os índices de pesquisa do Spanner suportam o seguinte:

  • Pesquisas JSON, que são uma forma eficiente de indexar e consultar documentos JSON e JSONB.
  • Pesquisas de substrings, que são um tipo de consulta que procura uma string mais curta (a substring) num corpo de texto maior.
  • Combinar condições em qualquer subconjunto de dados indexados, incluindo correspondência exata e numérica, numa única análise de índice.

Para mais informações sobre exemplos de utilização, consulte o artigo Pesquisa versus índices secundários.

Exemplo de índice de pesquisa

Para mostrar as capacidades dos índices de pesquisa, suponhamos que existe 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 de modo a permitir que os utilizadores executem uma pesquisa de texto completo para encontrar títulos de álbuns, use a função TOKENIZE_FULLTEXT para criar tokens a partir dos títulos dos álbuns. Em seguida, crie uma coluna que use o tipo de dados TOKENLIST para conter o resultado da tokenização de TOKENIZE_FULLTEXT. Para este 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 seguinte usa o 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. Por 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 dos dados

Quando é criado um índice, o Spanner usa processos automáticos para preencher os dados de forma retroativa 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 do esquema do índice de pesquisa

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

  • Tabela 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 destas colunas é irrelevante.

Por exemplo, na seguinte declaração, a tabela 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 declaração CREATE SEARCH INDEX seguinte para criar um índice de pesquisa com os tokens para 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. A consulta de um índice particionado é, muitas vezes, significativamente mais eficiente do que a consulta de um índice não particionado. Para mais informações, consulte o artigo Particione os índices de pesquisa.
  • Coluna de ordem de ordenação: uma coluna INT64 opcional que estabelece a ordem de obtenção do índice de pesquisa. Para mais informações, consulte Ordem de ordenação do índice de pesquisa.
  • Intercalação: tal como os índices secundários, pode intercalar índices de pesquisa. Os índices de pesquisa intercalados usam menos recursos para escrever e juntar-se à tabela base. Para mais informações, consulte o artigo Índices de pesquisa intercalados.
  • Cláusula options: uma lista de pares de chave-valor que substitui as predefinições do índice de pesquisa.

Esquema 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 principal da tabela base, que pode ser arbitrariamente longa. Também é o que cria a ordem do esquema de dados interno de acordo com as colunas ORDER BY fornecidas pelo utilizador da declaração CREATE SEARCH INDEX. É 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 IDs de documentos
  2. Docids para chaves principais da tabela base

Este esquema resulta numa poupança significativa de armazenamento, uma vez que o Spanner não precisa de armazenar a chave primária da tabela base completa para cada par <token, document>.

Existem 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 base. No exemplo da secção anterior, isto 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 tokens que mapeia tokens para docids, semelhante a índices invertidos na literatura de obtenção de informações. O Spanner mantém um índice de tokens separado para cada TOKENLIST do índice de pesquisa. Logicamente, os índices de tokens mantêm listas de docids para cada token em cada partição (conhecidas na obtenção de informações como listas de publicações). As listas são ordenadas por tokens para uma obtenção rápida e, nas listas, o docid é usado para a ordenação. Os índices de tokens individuais são um detalhe de implementação não exposto através das APIs Spanner.

O Spanner suporta as seguintes quatro opções para docid.

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

Notas de utilização:

  • A coluna UID interna não é exposta através da API Spanner.
  • Nos índices onde o UID não é adicionado, as transações que adicionam uma linha com uma ordem de partição e ordenação já existente falham.

Por exemplo, considere os seguintes dados:

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

Partindo do princípio de que a coluna de pré-ordenação está por ordem ascendente, o conteúdo do índice do token particionado por SingerId particiona o conteúdo do índice do token da seguinte forma:

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

Fragmentação do índice de pesquisa

Quando o Spanner divide uma tabela, distribui os dados do índice de pesquisa para que todos os tokens numa linha de tabela de base específica estejam na mesma divisão. Por outras palavras, o índice de pesquisa é fragmentado por documentos. Esta estratégia de divisão tem implicações significativas no desempenho:

  1. O número de servidores com os quais cada transação comunica permanece constante, independentemente do número de tokens ou do número 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 desempenho associada a uma junção distribuída.

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

  • Divisão uniforme (predefinição). Na divisão uniforme, os dados indexados de cada linha da tabela base são atribuídos aleatoriamente a uma divisão do índice de uma partição.
  • Partição de ordem de classificação. Na divisão em partições por ordem de ordenação, os dados de cada linha da tabela base são atribuídos a uma divisão do índice de uma partição com base nas colunas ORDER BY (ou seja, colunas de pré-ordenação). Por exemplo, no caso de uma ordem de ordenação descendente, todas as linhas com os valores de ordem de ordenação mais elevados aparecem na primeira divisão do índice de uma partição e o grupo de valores de ordem de ordenação seguinte aparece na divisão seguinte.

Estes modos de divisão vêm com uma compensação entre os riscos de pontos críticos e o custo da consulta:

  • Os índices de pesquisa fragmentados uniformes são recomendados quando os padrões de leitura ou escrita no índice de pesquisa podem originar hotspots. A divisão uniforme mitiga os pontos críticos distribuindo a carga de leitura e escrita uniformemente pelas divisões, mas isto pode aumentar a utilização de recursos durante as execuções de consultas como contrapartida. Nos índices de pesquisa divididos uniformemente, as consultas têm de ler todas as divisões numa partição devido aos dados distribuídos aleatoriamente. Quando acede a índices divididos uniformemente, o Spanner lê todas as divisões em paralelo para reduzir a latência geral das consultas.
  • Os índices de pesquisa fragmentados com ordem de ordenação são preferíveis quando os padrões de leitura ou escrita têm pouca probabilidade de causar pontos críticos. Esta abordagem pode reduzir o custo das consultas cujo ORDER BY corresponde ao ORDER BY do índice e especifica um LIMIT relativamente baixo. Quando executa essas consultas, o Spanner lê a partir das primeiras divisões de uma partição de forma incremental e a consulta pode ser concluída sem ler todas as divisões quando a condição LIMIT pode ser satisfeita antecipadamente.
  • O modo de divisão de um índice de pesquisa é configurado através da 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 divisão de um índice de pesquisa é configurado através da 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 está definido ou não especificado, o índice de pesquisa é criado com a divisão uniforme.

Índices de pesquisa intercalados

Tal como os índices secundários, pode intercalar índices de pesquisa numa tabela principal da tabela base. O motivo principal para usar índices de pesquisa intercalados é a colocação conjunta de dados da tabela base com dados de índice para partições pequenas. Esta colocação em comum oportunista tem as seguintes vantagens:

  • As gravações não precisam de fazer uma confirmação de duas fases.
  • As junções inversas 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. Só é possível intercalar índices fragmentados por ordem de classificação.
  2. Os índices de pesquisa só podem ser intercalados em tabelas de nível superior (não em tabelas secundárias).
  3. Tal como nas tabelas intercaladas e nos índices secundários, torne a chave da tabela principal um prefixo das colunas no índice de pesquisa intercalado.PARTITION BY

Defina um índice de pesquisa intercalado

O exemplo seguinte 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 ordenação do índice de pesquisa

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

Por exemplo, considere a seguinte tabela:

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

A aplicação pode definir um índice secundário para procurar informações através do AlbumName ordenado por ReleaseTimestamp:

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

O índice de pesquisa equivalente tem o seguinte aspeto (usa a tokenização de correspondência exata, uma vez que os índices secundários não suportam pesquisas de texto completo):

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

A ordem de ordenação do índice de pesquisa tem de estar em conformidade com os seguintes requisitos:

  1. Use apenas colunas INT64 para a ordem de ordenação de um índice de pesquisa. As colunas que têm tamanhos arbitrários usam demasiados recursos no índice de pesquisa porque o Spanner tem de armazenar um docid junto a cada token. Especificamente, a coluna de ordem de classificação não pode usar o tipo TIMESTAMP porque TIMESTAMP usa uma precisão de nanosegundos que não se enquadra num número inteiro de 64 bits.
  2. As colunas de ordem de ordenação não podem ser NULL. Existem duas formas de cumprir este requisito:

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

Normalmente, é usada uma data/hora para determinar a ordem de ordenação. Uma prática comum é usar microssegundos desde a época Unix para essas indicações de tempo.

Normalmente, as aplicações obtêm primeiro os dados mais recentes através de um índice de pesquisa ordenado por ordem descendente.

Í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 base. A filtragem de NULL pode aplicar-se a chaves de partição, colunas de ordem de ordenação e colunas armazenadas. A filtragem de NULL em colunas de matriz armazenadas não é permitida.

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 tem de especificar a condição de filtragem NULL (Genre IS NOT NULL para este exemplo) na cláusula WHERE. Caso contrário, o otimizador de consultas não consegue usar o índice de pesquisa. Para mais informações, consulte os requisitos de consultas SQL.

Use a filtragem NULL numa coluna gerada para excluir linhas com base em quaisquer critérios arbitrários. Para mais informações, consulte o artigo Crie um índice parcial com uma coluna gerada.

O que se segue?