O PostgreSQL suporta agrupamentos “insensíveis a acentos”?


98

No Microsoft SQL Server, é possível especificar um agrupamento "insensível ao acento" (para um banco de dados, tabela ou coluna), o que significa que é possível para uma consulta como

SELECT * FROM users WHERE name LIKE 'João'

para encontrar uma linha com um Joaonome.

Eu sei que é possível remover acentos de strings no PostgreSQL usando a função contrib unaccent_string , mas estou me perguntando se o PostgreSQL suporta esses agrupamentos "insensíveis a acentos" para que o SELECTacima funcione.


Veja esta resposta para criar um dicionário FTS com unaccent: stackoverflow.com/a/50595181/124486
Evan Carroll

Você deseja pesquisas com diferenciação de maiúsculas ou minúsculas?
Evan Carroll

Respostas:


204

Use o módulo unaccent para isso - que é completamente diferente do que você está vinculando.

unaccent é um dicionário de pesquisa de texto que remove acentos (sinais diacríticos) de lexemas.

Instale uma vez por banco de dados com:

CREATE EXTENSION unaccent;

Se você receber um erro como:

ERROR: could not open extension control file
"/usr/share/postgresql/<version>/extension/unaccent.control": No such file or directory

Instale o pacote contrib em seu servidor de banco de dados conforme instruído nesta resposta relacionada:

Entre outras coisas, ele fornece a função que unaccent()você pode usar com seu exemplo (onde LIKEparece não ser necessário).

SELECT *
FROM   users
WHERE  unaccent(name) = unaccent('João');

Índice

Para usar um índice para esse tipo de consulta, crie um índice na expressão . No entanto , o Postgres só aceita IMMUTABLEfunções para índices. Se uma função pode retornar um resultado diferente para a mesma entrada, o índice pode quebrar silenciosamente.

unaccent()STABLEnãoIMMUTABLE

Infelizmente, unaccent()é apenas STABLE, não IMMUTABLE. De acordo com este tópico sobre pgsql-bugs , isso se deve a três motivos:

  1. Depende do comportamento de um dicionário.
  2. Não há conexão física com este dicionário.
  3. Portanto, também depende da corrente search_path, que pode mudar facilmente.

Alguns tutoriais na web instruem apenas a alterar a volatilidade da função para IMMUTABLE. Este método de força bruta pode quebrar sob certas condições.

Outros sugerem uma função de invólucro simplesIMMUTABLE (como eu fazia no passado).

Há um debate em andamento quanto a fazer a variante com dois parâmetros IMMUTABLE que declara explicitamente o dicionário usado. Leia aqui ou aqui .

Outra alternativa seria este módulo com uma IMUTÁVEL unaccent()função por Musicbrainz , fornecida no Github. Não testei sozinho. Acho que tive uma ideia melhor :

Melhor por agora

Essa abordagem é mais eficiente do que outras soluções flutuando e mais segura .
Crie uma IMMUTABLEfunção de wrapper SQL executando a forma de dois parâmetros com a função qualificada pelo esquema conectado e um dicionário.

Como aninhar uma função não imutável desabilitaria o inlining de função, baseie-o em uma cópia da função C, (falsa) declarada IMMUTABLEtambém. Seu único propósito é ser usado no wrapper de função SQL. Não deve ser usado sozinho.

A sofisticação é necessária, pois não há como conectar o dicionário na declaração da função C. (Seria necessário hackear o próprio código C.) A função SQL wrapper faz isso e permite o inlining de função e os índices de expressão.

CREATE OR REPLACE FUNCTION public.immutable_unaccent(regdictionary, text)
  RETURNS text LANGUAGE c IMMUTABLE PARALLEL SAFE STRICT AS
'$libdir/unaccent', 'unaccent_dict';

CREATE OR REPLACE FUNCTION public.f_unaccent(text)
  RETURNS text LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT AS
$func$
SELECT public.immutable_unaccent(regdictionary 'public.unaccent', $1)
$func$;

Remova as PARALLEL SAFEduas funções para Postgres 9.5 ou mais antigo.

publicsendo o esquema onde você instalou a extensão ( publicé o padrão).

A declaração de tipo explícita ( regdictionary) protege contra ataques hipotéticos com variantes sobrecarregadas da função por usuários mal-intencionados.

Anteriormente, defendi uma função de invólucro com base na STABLEfunção unaccent()fornecida com o módulo unaccent. Essa função desativada inlining . Esta versão executa dez vezes mais rápido do que a função de invólucro simples que tive aqui anteriormente.
E isso já foi duas vezes mais rápido do que a primeira versão adicionada SET search_path = public, pg_tempà função - até que descobri que o dicionário também pode ser qualificado pelo esquema. Ainda (Postgres 12) não muito óbvio pela documentação.

Se você não tiver os privilégios necessários para criar funções C, estará de volta à segunda melhor implementação: um IMMUTABLEwrapper de função em torno da STABLE unaccent()função fornecida pelo módulo:

CREATE OR REPLACE FUNCTION public.f_unaccent(text)
  RETURNS text AS
$func$
SELECT public.unaccent('public.unaccent', $1)  -- schema-qualify function and dictionary
$func$  LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT;

Finalmente, o índice de expressão para tornar as consultas rápidas :

CREATE INDEX users_unaccent_name_idx ON users(public.f_unaccent(name));

Lembre-se de recriar índices envolvendo esta função após qualquer alteração na função ou dicionário, como uma atualização de versão principal no local que não recriaria índices. Todos os lançamentos principais recentes tinham atualizações para o unaccentmódulo.

Adapte as consultas para corresponder ao índice (para que o planejador de consultas as use):

SELECT * FROM users
WHERE  f_unaccent(name) = f_unaccent('João');

Você não precisa da função na expressão certa. Lá você também pode fornecer strings sem ênfase 'Joao'diretamente.

A função mais rápida não se traduz em consultas muito mais rápidas usando o índice de expressão . Isso opera em valores pré-calculados e já é muito rápido. Mas a manutenção do índice e as consultas não usam o benefício do índice.

A segurança para programas cliente foi reforçada com Postgres 10.3 / 9.6.8 etc. Você precisa qualificar o esquema da função e do nome do dicionário conforme demonstrado quando usado em quaisquer índices. Vejo:

Ligaduras

No Postgres 9.5 ou anteriores, ligaduras como 'Œ' ou 'ß' devem ser expandidas manualmente (se necessário), pois unaccent()sempre substitui uma única letra:

SELECT unaccent('Œ Æ œ æ ß');

unaccent
----------
E A e a S

Você vai adorar esta atualização para unaccent no Postgres 9.6 :

Estenda contrib/unaccento unaccent.rulesarquivo padrão para lidar com todos os diacríticos conhecidos pelo Unicode e expanda as ligaduras corretamente (Thomas Munro, Léonard Benedetti)

Ênfase em negrito minha. Agora temos:

SELECT unaccent('Œ Æ œ æ ß');

unaccent
----------
OE AE oe ae ss

Correspondência de padrões

Para LIKEou ILIKEcom padrões arbitrários, combine isso com o módulo pg_trgmno PostgreSQL 9.1 ou posterior. Crie um trigrama GIN (normalmente preferível) ou índice de expressão GIST. Exemplo para GIN:

CREATE INDEX users_unaccent_name_trgm_idx ON users
USING gin (f_unaccent(name) gin_trgm_ops);

Pode ser usado para consultas como:

SELECT * FROM users
WHERE  f_unaccent(name) LIKE ('%' || f_unaccent('João') || '%');

Os índices GIN e GIST são mais caros de manter do que o btree simples:

Existem soluções mais simples para padrões ancorados apenas à esquerda. Mais sobre correspondência de padrões e desempenho:

pg_trgmtambém fornece operadores%<-> úteis para "similaridade" ( ) e "distância" ( ) .

Os índices trigramas também suportam expressões regulares simples com ~et al. e o padrão não diferencia maiúsculas de minúsculas combinando com ILIKE:


Na sua solução, são usados ​​índices ou preciso criar um índice unaccent(name)?
Daniel Serodio

@ErwinBrandstetter No psql 9.1.4, eu obtenho "funções na expressão de índice devem ser marcadas como IMUTÁVEIS", porque a função não acentuada é ESTÁVEL, em vez de INMUTÁVEL. O que você recomenda?
e3matheus

1
@ e3matheus: Sentindo-me culpado por não ter testado a solução anterior que forneci, investiguei e atualizei minha resposta com uma solução nova e melhor (IMHO) para o problema do que a que está circulando até agora.
Erwin Brandstetter

Não é o agrupamento utf8_general_ci a resposta para esse tipo de problema?
Med

5
Suas respostas são tão boas quanto a documentação do Postgres: fenomenal!
eletrótipo

6

Não, PostgreSQL não suporta agrupamentos nesse sentido

O PostgreSQL não suporta agrupamentos como esse (insensível a acentos ou não) porque nenhuma comparação pode retornar igual a menos que as coisas sejam binárias iguais. Isso ocorre porque internamente ele introduziria muitas complexidades para coisas como um índice hash. Por essa razão, os agrupamentos em seu sentido mais estrito afetam apenas a ordenação e não a igualdade.

Soluções Alternativas

Dicionário de pesquisa de texto completo que unaccents lexemas.

Para FTS, você pode definir seu próprio dicionário usando unaccent,

CREATE EXTENSION unaccent;

CREATE TEXT SEARCH CONFIGURATION mydict ( COPY = simple );
ALTER TEXT SEARCH CONFIGURATION mydict
  ALTER MAPPING FOR hword, hword_part, word
  WITH unaccent, simple;

Que você pode indexar com um índice funcional,

-- Just some sample data...
CREATE TABLE myTable ( myCol )
  AS VALUES ('fóó bar baz'),('qux quz');

-- No index required, but feel free to create one
CREATE INDEX ON myTable
  USING GIST (to_tsvector('mydict', myCol));

Agora você pode consultá-lo de forma muito simples

SELECT *
FROM myTable
WHERE to_tsvector('mydict', myCol) @@ 'foo & bar'

    mycol    
-------------
 fóó bar baz
(1 row)

Veja também

Sem acento por si só.

O unaccentmódulo também pode ser usado sozinho sem integração FTS, para isso verifique a resposta de Erwin


2

Tenho certeza de que o PostgreSQL depende do sistema operacional subjacente para o agrupamento. Ele oferece suporte à criação de novos agrupamentos e à personalização de agrupamentos . Não tenho certeza de quanto trabalho isso pode ser para você, no entanto. (Pode ser bastante.)


1
O novo suporte a agrupamento atualmente é basicamente limitado a wrappers e aliases para localidades do sistema operacional. É muito básico. Não há suporte para funções de filtro, comparadores personalizados ou qualquer um dos que você precisa para agrupamentos personalizados verdadeiros.
Craig Ringer
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.