Busca fonética – um jeito mais inteligente e eficiente de procurar

Propaganda
"SUJHPt#VTDBGPOÏUJDBoVNKFJUPNBJTJOUFMJHFOUFFFmDJFOUFEFQSPDVSBSOPNFT
Busca fonética
um jeito mais inteligente e eficiente de procurar nomes
Tony Calleri França
([email protected]) é Engenheiro de
Computação formado pelo ITA, e atua há 10 anos com
desenvolvimento de sistemas, quase sempre usando
Java. Hoje trabalha como arquiteto responsável pelas
soluções tecnológicas da P2D Prontuário Universal.
uitos sistemas com banco de dados têm uma tabela de
pessoas. Uma das colunas dessa tabela é obviamente o
nome do Fulano. Normalmente, sistemas assim também
têm uma telinha (ou mais) onde é possível fazer uma busca
de pessoas por nome – ou parte do nome.
M
Esse é um problema bem resolvido, de solução conhecida. O que se faz é
implementar uma lógica de negócio que, no fim das contas, executa uma
“busca like” no banco:
Exemplo:
t 6TVÈSJPEJHJUBi5POZw
t #BODP FYFDVUB TFMFDU *% /0.& GSPN 1&440"4 XIFSF OPNF -*,&
‘TONY%’
t $POTVMUBSFUPSOB<A50/:$"--&3*'3"/±">
Essa solução é boa para maioria dos casos, mas tem dois “probleminhas”.
1) O match deve ser exato. Se o usuário digita “TONI” ou “TONNY”, a
59
"SUJHPt#VTDBGPOÏUJDBoVNKFJUPNBJTJOUFMJHFOUFFFmDJFOUFEFQSPDVSBSOPNFT
busca não vai retornar aquele resultado.
2) A performance do LIKE não é “lá essas coisas”. Se a tabela tiver muitos registros, a busca fica lenta. Eu fiz um teste com 7 milhões de
registros num PostgreSQL: a busca demorou uns 3 minutos.
Em 9 anos trabalhando com sistemas, isso nunca tinha sido um problema para mim. Ou seja, eu nunca tinha mexido num sistema que tivesse
tantos registros numa tabela de PESSOAS (ou de qualquer outra coisa
que precisasse fazer “busca like”), nem tinha tido como requisito do
sistema que ele fizesse “mágica” (o usuário buscar “Tonny”, e aparecer
um registro “Tony” nos resultados de busca). Isso mudou quando eu comecei a trabalhar com informática em saúde. Especialmente no projeto
que estou atualmente – um prontuário eletrônico universal baseado
na web.
Um sistema dessa natureza tem características, complexidades, com as
quais eu ainda não havia lidado. Para começar, o problema (2) aconteceu já na tabela de PACIENTES do sistema (com mais de 7 milhões de
registros).
Além disso, uma das muitas preocupações com o módulo de cadastro
são os registros duplicados.
Imagine que quando eu faço uma visita ao dr. Fulano, a recepcionista
me cadastra como “Toni Calleri França”. Depois eu vou no dr. Beltrano
e a moça busca por “Tony”, não encontra, e cadastra um novo “Tony
Calleri França”.
Resultado: paciente duplicado no sistema (e, portanto, prontuário,
prescrições etc.). É bem provável que o dr. Beltrano não veja que o
dr. Fulano – que me atendeu antes – inseriu no sistema que o Toni é
alérgico a Paracetamol. E aí nada impede que o dr. Beltrano receite um
Tylenol para o Tony.
E aí você poderia argumentar – mas esse paciente deve saber que é
alérgico e não vai tomar o remédio. E eu rebateria – e se o paciente
estiver inconsciente? Como é que fica?
Por isso é tão importante a unicidade do paciente nesse sistema. E
se houver alguma “mágica” que resolva o problema (1), certamente é
desejável que isso seja incorporado no sistema para reduzir o risco de
duplicidade.
É aqui que entra a busca fonética.
Existe um algoritmo que literalmente permite que se faça busca por
palavras que tenham o mesmo som. Ou seja, Toni, Tony e Tonny, seriam
considerados iguais. E o melhor – a implementação disso num banco
de dados pode deixar a pesquisa bem mais rápida que a “busca like”.
Sabe aquele texto de 3 minutos que eu mencionei? Com a busca fonética ficaria em dois segundos! Interessou-se? Continue lendo...
O fonetizador
O fonetizador é a base de tudo. Ele é um componente de software que
é capaz de “identificar o som das palavras”. Basicamente, o fonetizador
é uma biblioteca que possui um método fonetizar(String) : String. Esse
método recebe como argumento uma palavra, e retorna o fonema dessa
palavra, ou seja, uma String que representa o som produzido por essa
palavra. Veja o trecho da Listagem 1.
60 www.mundoj.com.br
O nosso fonetizador é essa classe FonetizacaoBR. Essa classe é a que
faz a “mágica” necessária para resolver o problema (1). O resultado da
execução desse programa está na Listagem 2.
Listagem 1. Test1.java.
import br.com.p2d.phonetizer.FonetizacaoBR;
public class Test1 {
public static void main(String[] args) {
System.out.println(FonetizacaoBR.fonetizar(“Tony Calleri”));
System.out.println(FonetizacaoBR.fonetizar(“Tonny Kaleri”));
System.out.println(FonetizacaoBR.fonetizar(“Toni Kalery”));
}
}
Listagem 2. Saída Test1.
TUNI KALIRI
TUNI KALIRI
TUNI KALIRI
É verdade que “Tony” e “TUNI”, na realidade, não têm exatamente o
mesmo som. Felizmente isso não é importante. O importante é que os
fonemas gerados a partir de Tony, Tonny e Toni são os mesmos e, portanto, essas três palavras serão consideradas foneticamente iguais.
O fonetizador utilizado foi feito a partir de um componente de “fonetização” desenvolvido pelo INCOR (Instituto do Coração), utilizando um
algoritmo fornecido pela PROCEMPA (Companhia de Processamento de
Dados do Município de Porto Alegre).
O componente original foi disponibilizado pelo INCOR como uma biblioteca livre, através da licença GPL. O código do componente original está
disponível para download no site da revista. O endereço original: http://
www.incor.usp.br/spdweb/ccssis/fonetica/ (ultimamente esse link parece
estar quebrado).
A versão original desse componente foi concebida pra ser disponibilizada através de uma interface CORBA, portanto, o código-fonte original
está “poluído” com alguns imports e chamadas a uma API específica de
CORBA pra Java.
O que nós fizemos foi refatorar o componente para que ele seja “puro
java”, ou seja, independente de bibliotecas CORBA, contendo só mesmo
a parte de fonetização. Esse novo componente foi batizado de “PhonetizerP2D”, e também é livre para uso e modificação, segundo a GPL.
É importante deixar claro o crédito: a “mágica”, o “pulo do gato” é da
equipe do INCOR e do PROCEMPA. Nós somente refatoramos. É importante deixar claro também: só funciona (bem) para a nossa língua pátria – o
português!
O PhonetizerP2D está disponível para download no site da P2D: http://
www.p2d.com.br/downloads/phonetizer
O processo de fonetização passa por algumas etapas pré e pós-fonetização. Essas etapas são bem mais simples do que o processo de fonetização
em si, e ajudam o resultado final a ficar mais “inteligente”. Se você
"SUJHPt#VTDBGPOÏUJDBoVNKFJUPNBJTJOUFMJHFOUFFFmDJFOUFEFQSPDVSBSOPNFT
baixar o código do PhonetizerP2D e observar o método FonetizacaoBR.
fonetizar(String), vai ver que o processo é mais ou menos assim:
1) Converte tudo para maiúscula
2) Remove preposições (da, de, do, del...)
3) Remove títulos (Sr, Sra, Dr...)
4) Substitui acentos (Á->A, É->E,...)
5) Remove caracteres estranhos (não-alfanuméricos)
6) Substitui letras por extenso (AGA->H, EFE->F, ...)
7) Substitui números por extenso
8) Chama o algoritmo “mágico” de fonetização
9) Substitui nomes parecidos (Ex: "XIRISTINA","KRISTINA")
10) Substitui sinônimos (Ex: "XURASKARIA","RISTAURANTI")
Listagem 5. Test2.java.
import java.util.ArrayList;
import br.com.p2d.phonetizer.FonetizacaoBR;
public class Test3 {
public static void main(String[] args) {
ArrayList<String> codes1 =
FonetizacaoBR.makeAllPhoneticCodes(“Tony”);
Como se pode deduzir, alguns desses passos exigem que haja alguns “dicionários” (preposições, títulos, letras e números por extenso etc.). Estes
dicionários estão codificados direto no fonte da classe FonetizacaoBR,
na forma de variáveis estáticas (das classes Set e Map). Ou seja, é relativamente fácil customizar o algoritmo de fonetização modificando esses
dicionários, mas para isso é preciso mexer no código-fonte da biblioteca.
ArrayList<String> codes2 =
FonetizacaoBR.makeAllPhoneticCodes(“Tony Calleri”);
ArrayList<String> codes3 =
FonetizacaoBR.makeAllPhoneticCodes(“Tony Calleri França”);
System.out.println(codes1);
Note que os dicionários usados para substituição pós-fonetização precisam ter entradas já fonetizadas. Por isso os dicionários usados nos passos
9 e 10 têm aqueles nomes engraçados: se tiver cadastrado “Churrascaria
Christina”, e o usuário procurar por “Restaurante da Cristina”, essa busca
vai encontrar esse resultado. É isso o que eu quis dizer com o resultado
final ficar mais “inteligente”!
A próxima funcionalidade não é muito empolgante, é um gerador de código hash para Strings. Só que antes de “hashear”, ele fonetiza a palavra.
System.out.println(codes2);
System.out.println(codes3);
String codeTest = FonetizacaoBR.makePhoneticCode(“Tonni Kaleri”);
System.out.println(“Hora da verdade: “+codes3.contains(codeTest));
}
}
Vejamos o exemplo:
E o resultado:
Listagem 3. Test2.java.
Listagem 6. Saída Test3.
import br.com.p2d.phonetizer.FonetizacaoBR;
[841d4397e8]
public class Test2 {
public static void main(String[] args) {
System.out.println(FonetizacaoBR.makePhoneticCode(“Tony Calleri”));
[841d4397e8, 24b2a597db, a8cfe92fc4]
[841d4397e8, 24b2a597db, a8cfe92fc4, 5ca718e072, e1c45c785b,
8159bd784e, 0577011037]
Hora da verdade: true
System.out.println(FonetizacaoBR.makePhoneticCode(“Tonny Kaleri”));
}
}
E o resultado:
Listagem 4. Saída Test2.
a8cfe92fc4
a8cfe92fc4
Já entendeu a ideia? Palavras foneticamente iguais têm o mesmo código
hash. Esse código hash é uma String com 10 dígitos hexadecimais. E aí
vem a próxima funcionalidade da biblioteca: dado um nome composto
de várias palavras, gerar códigos hash para várias combinações dessas
palavras. Exemplo na Listagem 5.
Pronto. É exatamente isso que faz a busca ser fonética, e mais rápida. Mas
vamos com calma. Primeiro vamos explicar o que faz o método makeAllPhoneticCodes: o que esse método faz é gerar várias combinações das
palavras que o nome contém, e gerar o PhoneticCode para cada combinação. Por exemplo: para o nome “Tony Calleri”, as combinações são:
“Tony”, “Calleri” e “Tony Calleri”. Por isso, três códigos hash foram gerados.
Para o nome “Tony Calleri França”, as combinações são as três anteriores
e mais quatro: “França”, “Tony França”, “Calleri França”, e “Tony Calleri França”; portanto temos sete códigos hash.
A quantidade de códigos hash cresceria exponencialmente com a quantidade de palavras de um nome (2^n – 1). Por isso o método makeAllPhoneticCodes coloca um limitante nesse crescimento removendo algumas
palavras “do meio” quando o nome é composto de mais de cinco palavras
(ou seja, consideramos que os primeiros nomes e últimos sobrenomes
são mais importantes – ou prováveis de serem usados numa busca).
61
"SUJHPt#VTDBGPOÏUJDBoVNKFJUPNBJTJOUFMJHFOUFFFmDJFOUFEFQSPDVSBSOPNFT
1_ Modelo do Banco
PESSOA_CODIGO
colunas indexadas
- Pessoa _id: INTEGER
- Codigo_id: INTEGER
PESSOA
CODIGO_FONETICO
- id: INTEGER
- nome: VARCHAR(255)
- corFavorita: VARCHAR(20)
- id: INTEGER
- codigo: VARCHAR(10)
2_ Alguns dados
841d4397e8
24b2a597db
a8cfe92fc4
5ca718e072
e1c45c785b
8159bd784e
0577011037
Tony Calleri França
3_ A Busca
string nome = "tonni Kaleri"; (digitado pelo usuário)
FonetizacaoBR.makePhoneticCode (nome); = a8cfe92fc4
select p.id, p.nome
from
PESSOA p,
PESSOA_CODIGO pc,
CODIGO_FONETICO C
where c.codigo = 'a8cfe92fc4'
and pc.codigo_id = c.id
and pc.pessoa_id = p.id
vai encontrar!
Comparação com '=' numa coluna indexada:
muito mais rápido que o "like"!
'JHVSB'VODJPOBNFOUPEBCVTDBGPOÏUJDB
Como incluir busca fonética na sua
aplicação
A ideia é que no banco, cada pessoa cadastrada esteja associada com os
códigos gerados pelo método makeAllPhoneticCodes. Esses códigos ficam
numa tabela separada, digamos CODIGO_FONETICO. O relacionamento
entre PESSOA e CODIGO_FONETICO é do tipo “n pra n”. Tanto a tabela CO62 www.mundoj.com.br
DIGO_FONETICO quanto a tabela de relacionamento precisa de índices: na
coluna do código fonético, e nas duas colunas da tabela de relacionamento.
Na hora de fazer a busca a partir de uma String, é só gerar o PhoneticCode
"SUJHPt#VTDBGPOÏUJDBoVNKFJUPNBJTJOUFMJHFOUFFFmDJFOUFEFQSPDVSBSOPNFT
dessa string e procurá-lo na tabela CODIGO_FONETICO. Se encontrar, retorna todas as pessoas que tiverem associados com esse código. É exatamente
essa busca que está sendo simulada pelo teste codes3.contains(codeTest)
na Listagem 5. A figura 1 mostra um “desenho” do que foi explicado.
são usados 10 fonemas por pessoa. Mas existem tantos fonemas
repetidos que reutilizando essa proporção cai para menos que 2,6
fonemas por pessoa: uma economia de 74% em volume na tabela
CODIGO_FONETICO.
Incorporar a funcionalidade de busca fonética numa aplicação Java que
use Hibernate não é difícil. Essa aplicação provavelmente já tem uma
classe Pessoa mapeada com a tabela PESSOA. É necessário criar, portanto, uma nova entidade CodigoFonetico associada com a tabela CODIGO_
FONETICO. A classe Pessoa ganharia um atributo Set<CodigoFonetico>
codigosFoneticos, anotado com @ManyToMany (ou configurado de
forma análoga no .hbm.xml).
Um ponto de atenção que se deve ter é: depois de fonetizar a base
não se deve customizar o algoritmo de fonetização (alterando os
dicionários, por exemplo), pois isso invalidaria a fonetização que foi
feita. Se essa customização for absolutamente necessária, o preço a
pagar é a refonetização da base.
Sempre ao incluir ou alterar uma Pessoa, é necessário atualizar
esse atributo, gerado a partir do fonetizador: FonetizacaoBR.
makeAllPhoneticCodes(pessoa.nome).
Este artigo mostrou como implementar uma busca fonética numa base
de pessoas, mostrando também as vantagens – inclusive de performance – da busca fonética em comparação com uma busca usando o
operador LIKE do SQL. Está disponibilizado para a comunidade, com
licença GPL, o PhonetizerP2D – uma biblioteca que executa as funções
de fonetização, na qual se baseiam os exemplos deste artigo.
Na hora de fazer uma busca por nome, a query em JPA seria assim: “from
Pessoa p inner join p. codigosFoneticos c where c.codigo = ?”. E o parâmetro para query também é obtido a partir do fonetizador: FonetizacaoBR.
makePhoneticCodes(nomeDigitadoPeloUsuario).
Incorporar essa funcionalidade numa aplicação já existente exige um
esforço adicional: a fonetização da base, ou seja, inserir os códigos fonéticos das pessoas que já estavam cadastradas antes. Este processo é
custoso! A fonetização daqueles 7 milhões de pacientes (rodando num
servidor “parrudo” da empresa) demorou umas duas semanas para executar! Esse é o preço que se paga.
Uma dúvida que pode surgir é: por que o relacionamento entre PESSOA
e CODIGO_FONETICO é de n pra n, e não de 1 pra n? A resposta é: para
economizar registros na tabela CODIGO_FONETICO.
Por exemplo, as pessoas de nomes “Tony Lâmpada” e “Tony Calleri” terão um código comum: 841d4397e9 – que corresponde ao fonema da
palavra “Tony”. Como o relacionamento é de n pra n, não é necessário
inserir o código 841d4397e9 duas vezes. Ele pode estar associado às duas
pessoas. Se o relacionamento fosse de 1 pra n isso não seria possível. E,
de fato, a economia é considerável.
Aquela base de 7 milhões de pacientes, depois de fonetizada, gerou 70
milhões de registros na tabela PESSOA_CODIGO, mas apenas 18 milhões
de registros na tabela CODIGO_FONETICO. Isso significa que, na média,
Conclusão
Nesta conclusão, aproveito para propor algumas melhorias ou continuações desse trabalho:
1) Externalizar os dicionários usados pelo PhonetizerP2D para que
não fique “hard-coded”.
2) A criação de um “framework para busca fonética” baseado em anotações. A ideia seria anotar, por exemplo, o método Pessoa.getNome() com um @Phonetic, habilitando automaticamente a busca
fonética para esse campo – o framework se encarregaria do resto.
3) Estudar um possível ganho de performance na fonetização de
bases existentes usando procedures em Java (já aproveitando o
“gancho” do artigo “Construindo Stored Procedures com Java no
PostgreSQL” da edição 39).
Se alguém fizer essa sugestão (2) acima, por favor, disponibilize para
gente na revista!
Por fim, quero agradecer alguns amigos que ajudaram indiretamente
na produção deste artigo: ao meu chefe, Alexandre Marcelo – sem as
exigências dele a solução não teria saído, e também por permitir a
liberação de conhecimento da empresa na forma de software livre.
Aos colegas-arquitetos Luiz Rolim e Rafael Adson, da P2D Electronic
Health Record, e Ricardo Lazaro, da Touch Tecnologia – pelas ajudas
na concepção dessa solução.
63
Download