Encontrar correspondências aproximadas com a pesquisa aproximada

Nesta página, descrevemos como usar uma pesquisa difusa como parte de uma pesquisa de texto completo.

Além de realizar pesquisas exatas de tokens usando as funções SEARCH e SEARCH_SUBSTRING, o Spanner também oferece suporte a pesquisas aproximadas (ou difusas). As pesquisas aproximadas encontram documentos correspondentes apesar de pequenas diferenças entre a consulta e o documento.

O Spanner é compatível com os seguintes tipos de pesquisa difusa:

  • Pesquisa aproximada baseada em n-gramas
  • Pesquisa fonética usando Soundex

A pesquisa difusa baseada em n-gramas depende da mesma tokenização de substring que uma pesquisa de substring exige. A configuração do tokenizador é importante porque afeta a qualidade e o desempenho da pesquisa. O exemplo a seguir mostra como criar uma consulta com palavras escritas incorretamente ou de forma diferente para encontrar correspondências aproximadas no índice de pesquisa.

Esquema

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (
    TOKENIZE_SUBSTRING(AlbumTitle, ngram_size_min=>2, ngram_size_max=>3,
                      relative_search_types=>["word_prefix", "word_suffix"])) HIDDEN
) PRIMARY KEY(AlbumId);

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
STORING (AlbumTitle);

PostgreSQL

Este exemplo usa spanner.tokenize_substring.

CREATE TABLE albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (
    spanner.tokenize_substring(albumtitle, ngram_size_min=>2, ngram_size_max=>3,
                      relative_search_types=>'{word_prefix, word_suffix}'::text[])) VIRTUAL HIDDEN,
PRIMARY KEY(albumid));

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens)
INCLUDE (albumtitle);

Consulta

A consulta a seguir encontra os álbuns com títulos mais próximos de "Hatel Kaliphorn", como "Hotel California".

GoogleSQL

SELECT AlbumId
FROM Albums
WHERE SEARCH_NGRAMS(AlbumTitle_Tokens, "Hatel Kaliphorn")
ORDER BY SCORE_NGRAMS(AlbumTitle_Tokens, "Hatel Kaliphorn") DESC
LIMIT 10

PostgreSQL

Este exemplo usa spanner.score_ngrams e spanner.search_ngrams.

SELECT albumid
FROM albums
WHERE spanner.search_ngrams(albumtitle_tokens, 'Hatel Kaliphorn')
ORDER BY spanner.score_ngrams(albumtitle_tokens, 'Hatel Kaliphorn') DESC
LIMIT 10

Otimizar a performance e o recall para uma pesquisa aproximada baseada em n-gramas

A consulta de exemplo na seção anterior pesquisa em duas fases, usando duas funções diferentes:

  1. SEARCH_NGRAMS encontra todos os álbuns candidatos que têm n-gramas compartilhados com a consulta de pesquisa. Por exemplo, os n-gramas de três caracteres para "Califórnia" incluem [cal, ali, lif, ifo, for, orn, rni, nia] e para "Kaliphorn" incluem [kal, ali, lip, iph, pho, hor, orn]. Os n-gramas compartilhados nesses conjuntos de dados são [ali, orn]. Por padrão, SEARCH_NGRAMS corresponde a todos os documentos com pelo menos dois n-gramas compartilhados. Portanto, "Kaliphorn" corresponde a "California".
  2. O SCORE_NGRAMS classifica as correspondências por semelhança. A similaridade de duas strings é definida como uma proporção de n-gramas compartilhados distintos para n-gramas não compartilhados distintos:
$$ \frac{shared\_ngrams}{total\_ngrams_{index} + total\_ngrams_{query} - shared\_ngrams} $$

Normalmente, a consulta de pesquisa é a mesma nas funções SEARCH_NGRAMS e SCORE_NGRAMS. A maneira recomendada de fazer isso é usar o argumento com parâmetros de consulta em vez de literais de string e especificar o mesmo parâmetro de consulta nas funções SEARCH_NGRAMS e SCORE_NGRAMS.

O Spanner tem três argumentos de configuração que podem ser usados com SEARCH_NGRAMS:

  • Os tamanhos mínimo e máximo para n-gramas são especificados com as funções TOKENIZE_SUBSTRING ou TOKENIZE_NGRAMS. Não recomendamos n-gramas de um caractere porque eles podem corresponder a um número muito grande de documentos. Por outro lado, n-gramas longos fazem com que o SEARCH_NGRAMS não detecte palavras curtas com erros de ortografia.
  • O número mínimo de n-gramas que SEARCH_NGRAMS precisa corresponder (definido com os argumentos min_ngrams e min_ngrams_percent em SEARCH_NGRAMS). Números mais altos geralmente tornam a consulta mais rápida, mas reduzem o recall.

Para alcançar um bom equilíbrio entre desempenho e recall, configure esses argumentos para se adequar à consulta e à carga de trabalho específicas.

Também recomendamos incluir um LIMIT interno para evitar a criação de consultas muito caras quando uma combinação de n-gramas populares é encontrada.

GoogleSQL

SELECT AlbumId
FROM (
  SELECT AlbumId,
        SCORE_NGRAMS(AlbumTitle_Tokens, @p) AS score
  FROM Albums
  WHERE SEARCH_NGRAMS(AlbumTitle_Tokens, @p)
  LIMIT 10000  # inner limit
)
ORDER BY score DESC
LIMIT 10  # outer limit

PostgreSQL

Este exemplo usa spanner.score_ngrams e spanner.search_ngrams. O parâmetro de consulta $1 está vinculado a "Hatel Kaliphorn".

SELECT albumid
FROM
  (
    SELECT albumid, spanner.score_ngrams(albumtitle_tokens, $1) AS score
    FROM albums
    WHERE spanner.search_ngrams(albumtitle_tokens, $1)
    LIMIT 10000
  ) AS inner_query
ORDER BY inner_query.score DESC
LIMIT 10

Pesquisa difusa baseada em n-gramas x modo de consulta avançado

Além da pesquisa aproximada baseada em n-gramas, o modo de consulta avançada também lida com algumas palavras escritas incorretamente. Portanto, há alguma sobreposição entre os dois recursos. A tabela a seguir resume as diferenças:

Pesquisa imprecisa baseada em n-gramas Modo de consulta avançado
Custo Requer uma tokenização de substring mais cara com base em n-grams Requer uma tokenização de texto completo menos cara
Tipos de consultas de pesquisa Funciona bem com documentos curtos com poucas palavras, como o nome de uma pessoa, cidade ou produto. Funciona igualmente bem com documentos e consultas de pesquisa de qualquer tamanho
Pesquisa de palavras parciais Realiza uma pesquisa de substring que permite erros ortográficos Só é possível pesquisar palavras inteiras (SEARCH_SUBSTRING não é compatível com o argumento enhance_query).
Palavras com erros de ortografia Aceita palavras com erros ortográficos no índice ou na consulta Só aceita palavras com erros de ortografia na consulta
Correções Encontra correspondências com erros ortográficos, mesmo que não sejam palavras reais Corrige erros ortográficos em palavras comuns e conhecidas

Fazer uma pesquisa fonética com Soundex

O Spanner oferece a função SOUNDEX para encontrar palavras que são escritas de maneira diferente, mas soam da mesma forma. Por exemplo, SOUNDEX("steven"), SOUNDEX("stephen") e SOUNDEX("stefan") são "s315", enquanto SOUNDEX("stella") é "s340". SOUNDEX diferencia maiúsculas de minúsculas e só funciona para alfabetos latinos.

A pesquisa fonética com SOUNDEX pode ser implementada com uma coluna gerada e um índice de pesquisa, conforme mostrado no exemplo a seguir:

GoogleSQL

CREATE TABLE Singers (
  SingerId INT64,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN,
  Name STRING(MAX),
  NameSoundex STRING(MAX) AS (LOWER(SOUNDEX(Name))),
  NameSoundex_Tokens TOKENLIST AS (TOKEN(NameSoundex)) HIDDEN
) PRIMARY KEY(SingerId);

CREATE SEARCH INDEX SingersPhoneticIndex ON Singers(AlbumTitle_Tokens, NameSoundex_Tokens);

PostgreSQL

Este exemplo usa spanner.soundex.

CREATE TABLE singers (
  singerid bigint,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN,
  name character varying,
  namesoundex character varying GENERATED ALWAYS AS (lower(spanner.soundex(name))) VIRTUAL,
  namesoundex_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.token(lower(spanner.soundex(name))) VIRTUAL HIDDEN,
PRIMARY KEY(singerid));

CREATE SEARCH INDEX singersphoneticindex ON singers(albumtitle_tokens, namesoundex_tokens);

A consulta a seguir corresponde a "stefan" e "Steven" em SOUNDEX, além de AlbumTitle que contém "cat":

GoogleSQL

SELECT SingerId
FROM Singers
WHERE NameSoundex = LOWER(SOUNDEX("stefan")) AND SEARCH(AlbumTitle_Tokens, "cat")

PostgreSQL

SELECT singerid
FROM singers
WHERE namesoundex = lower(spanner.soundex('stefan')) AND spanner.search(albumtitle_tokens, 'cat')

A seguir