"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