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:
- Tokens para docids
- 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:
- 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áusulaSTORING
deCREATE SEARCH INDEX
. - 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:
- 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. - 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 aoORDER BY
do índice e especifica umLIMIT
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 quandoLIMIT
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:
- Somente índices fragmentados por ordem de classificação podem ser intercalados.
- Os índices de pesquisa só podem ser intercalados em tabelas de nível superior, não em tabelas filhas.
- 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:
- 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 tipoTIMESTAMP
porque ele usa precisão de nanossegundos, que não cabe em um inteiro de 64 bits.TIMESTAMP
As colunas de ordem de classificação não podem ser
NULL
. Há duas maneiras de atender a esse requisito:- Declare a coluna de ordem de classificação como
NOT NULL
. - Configure o índice para excluir valores NULL.
- Declare a coluna de ordem de classificação como
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
- Saiba mais sobre tokenização e tokenizadores do Spanner.
- Saiba mais sobre índices numéricos.
- Saiba mais sobre índices JSON.
- Saiba mais sobre o particionamento de índice.