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 "California" 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 distintos compartilhados e não compartilhados:
$$ \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 com erros ortográficos. 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 Suporta palavras com erros de ortografia no índice ou na consulta Só oferece suporte a palavras com erros ortográficos 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 funciona apenas 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