Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi...

5
59 M 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. 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: ‘TONY%’ 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

Transcript of Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi...

Page 1: Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi refatorar o componente para que ele seja “puro ... Por exemplo: para o nome “Tony

59

M

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.

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:

‘TONY%’

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

Page 2: Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi refatorar o componente para que ele seja “puro ... Por exemplo: para o nome “Tony

60 www.mundoj.com.br

busca não vai retornar aquele resultado.2) A performance do LIKE não é “lá essas coisas”. Se a tabela tiver mui-

tos 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 proble-ma 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 co-mecei 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) aconte-ceu 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 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.

É 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, por-tanto, essas três palavras serão consideradas foneticamente iguais.

O fonetizador utilizado foi feito a partir de um componente de “foneti-zaçã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 biblio-teca 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 disponibili-zada 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 “Phoneti-zerP2D”, 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. É importan-te 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ê

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.

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”)); }}

TUNI KALIRITUNI KALIRITUNI KALIRI

Listagem 1. Test1.java.

Listagem 2. Saída Test1.

Page 3: Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi refatorar o componente para que ele seja “puro ... Por exemplo: para o nome “Tony

61

E o resultado:

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.

E o resultado:

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 makeAll-PhoneticCodes: o que esse método faz é gerar várias combinações das palavras que o nome contém, e gerar o PhoneticCode para cada com-binaçã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 quan-tidade de palavras de um nome (2^n – 1). Por isso o método makeAllPho-neticCodes 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).

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úscula2) 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 extenso8) Chama o algoritmo “mágico” de fonetização9) Substitui nomes parecidos (Ex: "XIRISTINA","KRISTINA")10) Substitui sinônimos (Ex: "XURASKARIA","RISTAURANTI")

Como se pode deduzir, alguns desses passos exigem que haja alguns “di-cioná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, é relati-vamente fácil customizar o algoritmo de fonetização modificando esses dicionários, mas para isso é preciso mexer no código-fonte da biblioteca.

Note que os dicionários usados para substituição pós-fonetização preci-sam 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.

Vejamos o exemplo:

import br.com.p2d.phonetizer.FonetizacaoBR;

public class Test2 {

public static void main(String[] args) {

System.out.println(FonetizacaoBR.makePhoneticCode(“Tony Calleri”));

System.out.println(FonetizacaoBR.makePhoneticCode(“Tonny Kaleri”));

}

}

a8cfe92fc4a8cfe92fc4

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”);

ArrayList<String> codes2 =

FonetizacaoBR.makeAllPhoneticCodes(“Tony Calleri”);

ArrayList<String> codes3 =

FonetizacaoBR.makeAllPhoneticCodes(“Tony Calleri França”);

System.out.println(codes1);

System.out.println(codes2);

System.out.println(codes3);

String codeTest = FonetizacaoBR.makePhoneticCode(“Tonni Kaleri”);

System.out.println(“Hora da verdade: “+codes3.contains(codeTest));

}

}

[841d4397e8]

[841d4397e8, 24b2a597db, a8cfe92fc4]

[841d4397e8, 24b2a597db, a8cfe92fc4, 5ca718e072, e1c45c785b,

8159bd784e, 0577011037]

Hora da verdade: true

Listagem 3. Test2.java.

Listagem 4. Saída Test2.

Listagem 5. Test2.java.

Listagem 6. Saída Test3.

Page 4: Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi refatorar o componente para que ele seja “puro ... Por exemplo: para o nome “Tony

62 www.mundoj.com.br

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 CO-

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

PESSOA_CODIGO

PESSOACODIGO_FONETICO

- Pessoa _id: INTEGER- Codigo_id: INTEGER

- id: INTEGER- nome: VARCHAR(255)- corFavorita: VARCHAR(20)

- id: INTEGER- codigo: VARCHAR(10)

colunas indexadas

Tony Calleri França

841d4397e824b2a597dba8cfe92fc4

5ca718e072e1c45c785b8159bd784e0577011037

string nome = "tonni Kaleri"; (digitado pelo usuário)

FonetizacaoBR.makePhoneticCode (nome); = a8cfe92fc4

select p.id, p.nomefrom PESSOA p, PESSOA_CODIGO pc, CODIGO_FONETICO Cwhere c.codigo = 'a8cfe92fc4'and pc.codigo_id = c.idand pc.pessoa_id = p.id

Comparação com '=' numa coluna indexada:muito mais rápido que o "like"!

vai encontrar!

1_ Modelo do Banco

2_ Alguns dados

3_ A Busca

Page 5: Busca fonética - Faculdades Integradas do Vale do Ivaí · CORBA pra Java. O que nós fizemos foi refatorar o componente para que ele seja “puro ... Por exemplo: para o nome “Tony

63

dessa string e procurá-lo na tabela CODIGO_FONETICO. Se encontrar, retor-na 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.

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, portan-

to, 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).

Sempre ao incluir ou alterar uma Pessoa, é necessário atualizar

esse atributo, gerado a partir do fonetizador: FonetizacaoBR.

makeAllPhoneticCodes(pessoa.nome).

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âme-

tro 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 fo-

né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 exe-

cutar! 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” te-

rã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

Este artigo mostrou como implementar uma busca fonética numa base de pessoas, mostrando também as vantagens – inclusive de perfor-mance – 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.

Nesta conclusão, aproveito para propor algumas melhorias ou continu-açõ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 ano-

tações. A ideia seria anotar, por exemplo, o método Pessoa.getNo-me() 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.

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.

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.

de registros na tabela CODIGO_FONETICO. Isso significa que, na média, de registros na tabela CODIGO_FONETICO. Isso significa que, na média,