Encontrar vizinhos mais próximos aproximados, criar índices de vetor e consultar embeddings de vetor

Esta página descreve como encontrar vizinhos mais próximos aproximados (ANN, na sigla em inglês), criar índices de vetor e consultar embeddings de vetor usando as seguintes funções de distância ANN no Spanner:

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

Quando um conjunto de dados é pequeno, é possível usar vizinhos mais próximos (KNN) para encontrar os vetores exatos mais próximos. No entanto, à medida que o conjunto de dados cresce, a latência e o custo de uma pesquisa KNN também aumentam. É possível usar ANN para encontrar os vizinhos mais próximos k aproximados com latência e custo significativamente reduzidos.

Vizinhos k-mais próximos aproximados

Em uma pesquisa ANN, os vetores retornados não são os vizinhos mais próximos mais importantes porque a pesquisa ANN calcula distâncias aproximadas e pode não analisar todos os vetores no conjunto de dados. Ocasionalmente, alguns vetores que não estão entre os vizinhos mais próximos são retornados. Isso é conhecido como perda de recordação. A quantidade de perda de recuperação aceitável depende do caso de uso, mas, na maioria dos casos, perder um pouco de recuperação em troca de um melhor desempenho do banco de dados é uma compensação aceitável.

Para mais detalhes sobre as funções de distância aproximada do Spanner, consulte:

Índice vetorial

O Spanner acelera as pesquisas de vetor ANN usando um índice de vetor especializado. Esse índice aproveita o Vizinho mais próximo escalonável (ScaNN, na sigla em inglês) da Google Research, um algoritmo de vizinho mais próximo altamente eficiente.

O índice de vetor usa uma estrutura baseada em árvore para particionar dados e facilitar pesquisas mais rápidas. O Spanner oferece configurações de árvore de dois e três níveis:

  • Configuração de árvore de dois níveis: os nós de folha (num_leaves) contêm grupos de vetores intimamente relacionados e o centroide correspondente. O nível raiz é composto pelos centroides de todos os nós de folha.
  • Configuração de árvore de três níveis: semelhante ao conceito de uma árvore de dois níveis, mas com a introdução de uma camada de ramificação adicional (num_branches), a partir da qual os centroides de nós de folha são particionados para formar o nível raiz (num_leaves).

O Spanner escolhe um índice para você. No entanto, se você souber que um índice específico funciona melhor, use a dica FORCE_INDEX para escolher o índice vetorial mais adequado para seu caso de uso.

Para mais informações, consulte as instruções VECTOR INDEX.

Criar índice vetorial

Para otimizar a recuperação e o desempenho de um índice de vetor, recomendamos que você:

  • Crie o índice de vetor depois que a maioria das linhas com embeddings for gravada no banco de dados. Talvez também seja necessário reconstruir periodicamente o índice de vetores depois de inserir novos dados. Para mais informações, consulte Recriar o índice vetorial.

  • Use a cláusula STORING para armazenar uma cópia de uma coluna no índice de vetor. Se um valor de coluna for armazenado no índice de vetor, o Spanner vai filtrar no nível da folha do índice para melhorar a performance da consulta. Recomendamos armazenar uma coluna se ela for usada em uma condição de filtragem. Para mais informações sobre como usar STORING em um índice, consulte Criar um índice para verificações somente de índice.

Ao criar a tabela, a coluna de incorporação precisa ser uma matriz do tipo de dados FLOAT32 (recomendado) ou FLOAT64 e ter uma anotação vector_length, indicando a dimensão dos vetores.

A instrução DDL a seguir cria uma tabela Documents com uma coluna de embeddings DocEmbedding com um comprimento de vetor:

CREATE TABLE Documents {
  ...
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128);
};

Depois de preencher a tabela Documents, é possível criar um índice vetorial com uma árvore de dois níveis e 1.000 nós de folha em uma tabela Documents com uma coluna de embedding DocEmbedding usando a distância do cosseno:

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING (WordCount)
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

Para criar um índice de vetores com uma árvore de três níveis e 1.000.000 nós de folhas:

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 3, num_branches=1000, num_leaves = 1000000);

Se a coluna de incorporação não estiver marcada como NOT NULL na definição da tabela, ela precisa ser declarada com uma cláusula WHERE column_name IS NOT NULL:

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

Consultar embeddings de vetor

Para consultar um índice de vetor, use uma das três funções de distância aproximada:

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

As restrições ao usar as funções de distância aproximada incluem:

  • A função de distância aproximada precisa calcular a distância entre uma coluna de incorporação e uma expressão constante (por exemplo, um parâmetro ou um literal).
  • A saída da função de distância aproximada precisa ser usada em uma cláusula ORDER BY como a única chave de classificação, e um LIMIT precisa ser especificado após o ORDER BY.
  • A consulta precisa filtrar explicitamente as linhas que não estão indexadas. Na maioria dos casos, isso significa que a consulta precisa incluir uma cláusula WHERE <column_name> IS NOT NULL que corresponda à definição do índice do vetor, a menos que a coluna já esteja marcada como NOT NULL na definição da tabela.

Para conferir uma lista detalhada de limitações, consulte a página de referência da função de distância aproximada.

Exemplo

Para pesquisar os 100 vetores mais próximos de [1.0, 2.0, 3.0]:

SELECT DocId
FROM Documents
WHERE WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], DocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

Se a coluna de inserção for anulável:

SELECT DocId
FROM Documents
WHERE NullableDocEmbedding IS NOT NULL AND WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], NullableDocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

Práticas recomendadas

Siga estas práticas recomendadas para otimizar os índices de vetor e melhorar os resultados da consulta.

Ajustar as opções de pesquisa vetorial

O valor de pesquisa de vetor mais ideal depende do caso de uso, do conjunto de dados de vetor e dos vetores de consulta. Talvez seja necessário realizar um ajuste iterativo para encontrar os melhores valores para sua carga de trabalho específica.

Confira algumas diretrizes úteis para escolher os valores adequados:

  • tree_depth (nível da árvore): se a tabela indexada tiver menos de 10 milhões de linhas, use um tree_depth de 2. Caso contrário, um tree_depth de 3 oferece suporte a tabelas de até 10 bilhões de linhas.

  • num_leaves: use a raiz quadrada do número de linhas no conjunto de dados. Um valor maior pode aumentar o tempo de criação do índice vetorial. Evite definir num_leaves maior que table_row_count/1000, porque isso resulta em folhas muito pequenas e em uma performance ruim.

  • num_leaves_to_search: essa opção especifica quantos nós de folha do índice são pesquisados. Aumentar num_leaves_to_search melhora o recall, mas também aumenta a latência e o custo. Recomendamos usar um número que seja 1% do número total de folhas definidas na instrução CREATE VECTOR INDEX como o valor de num_leaves_to_search. Se você estiver usando uma cláusula de filtro, aumente esse valor para ampliar a pesquisa.

Se o recall aceitável for alcançado, mas o custo da consulta for muito alto, resultando em QPS máxima baixa, tente aumentar num_leaves seguindo estas etapas:

  1. Defina num_leaves como um múltiplo k do valor original (por exemplo, 2 * sqrt(table_row_count)).
  2. Defina num_leaves_to_search como o mesmo múltiplo k do valor original.
  3. Experimente reduzir num_leaves_to_search para melhorar o custo e o QPS mantendo a recuperação.

Melhorar a lembrança

Há várias possibilidades de piora da revogação, incluindo as seguintes:

  • num_leaves_to_search é muito pequeno: pode ser mais difícil encontrar os vizinhos mais próximos de alguns vetores de consulta. Portanto, aumentar num_leaves_to_search para pesquisar mais folhas pode ajudar a melhorar o recall. As consultas recentes podem ter mudado para conter mais desses vetores desafiadores.

  • O índice de vetor precisa ser recriado: a estrutura de árvore do índice de vetor é otimizada para o conjunto de dados no momento da criação e fica estática depois disso. Portanto, se vetores significativamente diferentes forem adicionados após a criação do índice de vetor inicial, a estrutura de árvore poderá ser subótima, levando a uma recuperação mais fraca.

Recriar o índice vetorial

Para recriar o índice de vetores sem inatividade:

  1. Crie um novo índice vetorial na mesma coluna de inserção do índice vetorial atual, atualizando os parâmetros (por exemplo, OPTIONS) conforme apropriado.
  2. Depois que a criação do índice for concluída, use a dica FORCE_INDEX para apontar para o novo índice e atualizar a consulta de pesquisa vetorial. Isso garante que a consulta use o novo índice de vetor. Talvez também seja necessário retornar num_leaves_to_search na nova consulta.
  3. Descarte o índice vetorial desatualizado.

A seguir