From a8160f0ce5056df45779639f9e848e471ee0b365 Mon Sep 17 00:00:00 2001 From: "Rodrigo E. S. Borges" Date: Fri, 26 Dec 2025 18:45:40 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fun=C3=A7=C3=B5es=20consulta=20,=20similari?= =?UTF-8?q?dade=20e=20sugest=C3=A3o=20de=20grafia=20de=20nomes=20a=20parti?= =?UTF-8?q?r=20de=20cadastro=20central?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DESCRIPTION | 2 + NAMESPACE | 7 + R/utils_com_central.R | 315 +++++++++++++++++++++++ man/buscar_similares_otimizado.Rd | 35 +++ man/calcular_similaridade_nomes.Rd | 34 +++ man/processamento_lote.Rd | 34 +++ man/sugerir_correcao_nomes.Rd | 36 +++ tests/testthat/test_similaridade_nomes.R | 54 ++++ 8 files changed, 517 insertions(+) create mode 100644 man/buscar_similares_otimizado.Rd create mode 100644 man/calcular_similaridade_nomes.Rd create mode 100644 man/processamento_lote.Rd create mode 100644 man/sugerir_correcao_nomes.Rd create mode 100644 tests/testthat/test_similaridade_nomes.R diff --git a/DESCRIPTION b/DESCRIPTION index 6d653dc..3669983 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,12 +36,14 @@ Imports: dplyr, httr2, stringr, + stringdist, tictoc Suggests: testthat (>= 3.0.0), DBI, duckdb, digest, + metaphonebr, mockery, knitr, rmarkdown diff --git a/NAMESPACE b/NAMESPACE index 03c59f1..42fd5c7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,16 +2,23 @@ export(add_nome_proprio_to_word1_and_word2p) export(add_string_w1_w2_w3_and_w2p) +export(buscar_similares_otimizado) +export(calcular_similaridade_nomes) export(consulta_nome_em_central) export(find_and_clean_NAnames_and_extra_spaces) export(identificar_adicionar_nome_proprio) export(limpar_nomes) +export(processamento_lote) export(remove_PARTICULAS_AGNOMES) export(segmentar_nomes) export(simplifica_PARTICULAS_AGNOMES_PATENTES) +export(sugerir_correcao_nomes) export(tabular_problemas_em_nomes) export(tabulate_name_poblems) import(data.table) +import(stringdist) +importFrom(dplyr,arrange) +importFrom(dplyr,desc) importFrom(dplyr,na_if) importFrom(httr2,req_error) importFrom(httr2,req_perform) diff --git a/R/utils_com_central.R b/R/utils_com_central.R index fa9c0bb..146d6b5 100644 --- a/R/utils_com_central.R +++ b/R/utils_com_central.R @@ -124,3 +124,318 @@ consulta_nome_em_central <- return(encontrado) } + + + + +#' Calcular similaridade entre nomes +#' +#' Esta função calcula a similaridade entre dois nomes utilizando uma combinação +#' ponderada de algoritmos de distância de strings (Jaro-Winkler e Damerau-Levenshtein) +#' após pré-processamento fonético. +#' +#' @param nome1 Primeiro nome para comparação (character) +#' @param nome2 Segundo nome para comparação (character) +#' +#' @return Um valor numérico entre 0 e 1 representando a similaridade entre os nomes +#' +#' @details +#' A função realiza os seguintes passos: +#' \itemize{ +#' \item Limpeza dos nomes usando nomesbr::limpar_nomes +#' \item Codificação fonética usando metaphonebr::metaphonebr +#' \item Cálculo da similaridade usando Jaro-Winkler (peso 0.7) e Damerau-Levenshtein (peso 0.3) +#' } +#' +#' @examples +#' calcular_similaridade_nomes("Maria", "Mary") +#' calcular_similaridade_nomes("José", "Jose") +#' +#' @import stringdist +#' @import nomesbr +#' @import data.table +#' @importFrom dplyr arrange desc +#' +#' @export +#' + + +calcular_similaridade_nomes <- \(nome1, nome2) { + + nome1_clean <- nomesbr::limpar_nomes(data.table(nome=nome1),'nome')$nome_clean + + nome2_clean <-nomesbr::limpar_nomes(data.table(nome=nome2),'nome')$nome_clean + # Verifica se o pacote metaphonebr está disponível + if (requireNamespace("metaphonebr", quietly = TRUE)) { + nome1_clean <- metaphonebr::metaphonebr(nome1_clean,verbose = F) + nome2_clean <- metaphonebr::metaphonebr(nome2_clean,verbose = F) + # Pré-processamento com seu pacote + + + } else { + print("Pacote metaphonebr inexistente localmente, aplicando apenas limpeza de nomesbr") + } + + print(paste('nomes limpos',nome1_clean,'comparado com',nome2_clean)) + + # Combinação ponderada de distâncias + jarowinkler <- stringdist::stringsim(nome1_clean, nome2_clean, method = "jw") + + damerau_levenshtein <- stringdist::stringsim(nome1_clean, nome2_clean, method = "dl") + + + + # Peso maior para Jaro-Winkler (melhor para nomes) + similaridade <- 0.7 * jarowinkler + 0.3 * damerau_levenshtein + + + + return(similaridade) + + +} + + +#' Sugerir correções para um nome alvo +#' +#' Esta função sugere correções para um nome alvo com base em uma lista de nomes candidatos, +#' utilizando um limiar adaptativo baseado no comprimento do nome. +#' +#' @param nome_alvo Nome para o qual se buscam correções (character) +#' @param lista_nomes Vetor de nomes candidatos (character vector) +#' @param threshold_adaptativo Lógico indicando se deve usar limiar adaptativo (default = TRUE) +#' +#' @return Um data.frame com colunas 'sugestao' e 'similaridade' contendo as sugestões +#' que superaram o limiar mínimo +#' +#' @details +#' O limiar adaptativo funciona da seguinte forma: +#' \itemize{ +#' \item Nomes com até 5 caracteres: limiar de 0.85 +#' \item Nomes entre 6 e 10 caracteres: limiar de 0.80 +#' \item Nomes com mais de 10 caracteres: limiar de 0.75 +#' } +#' +#' @examples +#' sugerir_correcao_nomes("Jão", c("João", "Jonas", "Juan", "Joaquim")) +#' sugerir_correcao_nomes("Ana", c("Anna", "Hana", "Ana Paula"), threshold_adaptativo = FALSE) +#' +#' @export +sugerir_correcao_nomes <- \(nome_alvo, lista_nomes, threshold_adaptativo = TRUE) { + + + + + similaridades <- sapply(lista_nomes, \(x) { + + calcular_similaridade_nomes(nome_alvo, x) + + }) + + + + if (threshold_adaptativo) { + + # Threshold baseado no comprimento do nome + nome_alvo <- data.table("nome"=nome_alvo) + comprimento <- nchar(limpar_nomes(nome_alvo,"nome")) + + threshold <- ifelse(comprimento <= 5, 0.85, + + ifelse(comprimento <= 10, 0.80, 0.75)) + + } else { + + threshold <- 0.80 # Default conservador + + } + + + + sugestoes <- lista_nomes[similaridades >= threshold] + + scores <- similaridades[similaridades >= threshold] + + + + return(data.frame(sugestao = sugestoes, similaridade = scores)) + +} + +#' Busca otimizada de nomes similares +#' +#' Realiza busca eficiente de nomes similares em grandes volumes de dados, +#' utilizando pré-filtragem fonética e limitando o número de candidatos. +#' +#' @param nome_alvo Nome alvo para busca (character) +#' @param vetor_nomes Vetor de nomes onde buscar (character vector) +#' @param max_candidates Número máximo de candidatos a considerar (default = 1000) +#' +#' @return Um data.frame com colunas 'nome_original', 'sugestao' e 'similaridade' +#' ordenado por similaridade decrescente, filtrado por similaridade >= 0.75 +#' +#' @details +#' A função utiliza uma estratégia de dois estágios: +#' \itemize{ +#' \item Primeiro filtro fonético usando códigos Metaphone +#' \item Cálculo detalhado de similaridade apenas para candidatos pré-selecionados +#' } +#' +#' @examples +#' buscar_similares_otimizado("Francisco", c("Francisco", "Fran", "Franscisco", "Francisko")) +#' buscar_similares_otimizado("Maria", c("Ana", "João", "Pedro"), max_candidates = 50) +#' +#' @export + +#3. Otimização para Grandes Volumes + + +# Usando seu metaphonebr para pré-filtragem +buscar_similares_otimizado <- \(nome_alvo, vetor_nomes, max_candidates = 1000) { + + + # Verifica se o pacote metaphonebr está disponível + if (requireNamespace("metaphonebr", quietly = TRUE)) { + + + # Primeiro filtro fonético + metaphone_alvo <- metaphonebr::metaphonebr(nome_alvo) + + metaphones_db <- metaphonebr::metaphonebr(vetor_nomes) + + + + # Candidatos com mesmo código fonético + candidatos_foneticos <- which(metaphones_db == metaphone_alvo) + + + + if (length(candidatos_foneticos) == 0) { + + # Fallback: busca por similaridade fonética + + similaridade_fonetica <- stringdist::stringsim(metaphone_alvo, metaphones_db, method = "jw") + + candidatos_foneticos <- which(similaridade_fonetica >= 0.7) + + } + } else { + # Fallback: busca por similaridade fonética + + similaridade_fonetica <- stringdist::stringsim(nome_alvo, vetor_nomes, method = "jw") + + candidatos_foneticos <- which(similaridade_fonetica >= 0.7) + } + + + + # Limita candidatos para eficiência + candidatos <- utils::head(candidatos_foneticos, max_candidates) + + + + # Calcula similaridade detalhada apenas para candidatos + if (length(candidatos) > 0) { + + similaridades <- sapply(vetor_nomes[candidatos], \(x) { + + calcular_similaridade_nomes(nome_alvo, x) + + }) + + + + return(data.frame( + + "nome_original" = nome_alvo, + + "sugestao" = vetor_nomes[candidatos], + + "similaridade" = similaridades + + ) |> dplyr::filter("similaridade" >= 0.75) |> dplyr::arrange(dplyr::desc("similaridade"))) + + } + + + + return(data.frame()) + +} + + + +#' Processamento em lote de nomes +#' +#' Processa um vetor de nomes em lotes para encontrar nomes similares, +#' otimizado para grandes volumes de dados. +#' +#' @param vetor_nomes Vetor de nomes para processar (character vector) +#' @param chunk_size Tamanho de cada lote para processamento (default = 10000) +#' +#' @return Um data.frame combinado com todos os resultados dos lotes +#' +#' @details +#' A função: +#' \itemize{ +#' \item Remove duplicatas exatas primeiro +#' \item Processa os dados em blocos (chunks) para otimizar memória +#' \item Pode ser facilmente paralelizada modificando o loop interno +#' } +#' +#' @examples +#' processamento_lote(c("Maria", "João", "Ana", "Pedro", "Francisco")) +#' nomes_grande_vetor <- sample(c("Maria", "João", "Ana", "Pedro", "Francisco"),100,replace = TRUE) +#' processamento_lote(nomes_grande_vetor, chunk_size = 50) +#' +#' @export + + + +#4. Implementação Escalável + +processamento_lote <- \(vetor_nomes, chunk_size = 10000) { + + # Remove duplicatas exatas primeiro + nomes_unicos <- unique(vetor_nomes) + + + + # Processa em chunks + resultados <- list() + + + + for (i in seq(1, length(nomes_unicos), chunk_size)) { + + chunk <- nomes_unicos[i:min(i + chunk_size - 1, length(nomes_unicos))] + + + + # Aqui você pode paralelizar + + chunk_result <- lapply(chunk, \(nome) { + + buscar_similares_otimizado(nome, nomes_unicos) + + }) + + + + resultados <- c(resultados, chunk_result) + + } + + + + return(dplyr::bind_rows(resultados)) + +} + + + + + + + + diff --git a/man/buscar_similares_otimizado.Rd b/man/buscar_similares_otimizado.Rd new file mode 100644 index 0000000..f23d045 --- /dev/null +++ b/man/buscar_similares_otimizado.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_com_central.R +\name{buscar_similares_otimizado} +\alias{buscar_similares_otimizado} +\title{Busca otimizada de nomes similares} +\usage{ +buscar_similares_otimizado(nome_alvo, vetor_nomes, max_candidates = 1000) +} +\arguments{ +\item{nome_alvo}{Nome alvo para busca (character)} + +\item{vetor_nomes}{Vetor de nomes onde buscar (character vector)} + +\item{max_candidates}{Número máximo de candidatos a considerar (default = 1000)} +} +\value{ +Um data.frame com colunas 'nome_original', 'sugestao' e 'similaridade' + ordenado por similaridade decrescente, filtrado por similaridade >= 0.75 +} +\description{ +Realiza busca eficiente de nomes similares em grandes volumes de dados, +utilizando pré-filtragem fonética e limitando o número de candidatos. +} +\details{ +A função utiliza uma estratégia de dois estágios: +\itemize{ + \item Primeiro filtro fonético usando códigos Metaphone + \item Cálculo detalhado de similaridade apenas para candidatos pré-selecionados +} +} +\examples{ +buscar_similares_otimizado("Francisco", c("Francisco", "Fran", "Franscisco", "Francisko")) +buscar_similares_otimizado("Maria", c("Ana", "João", "Pedro"), max_candidates = 50) + +} diff --git a/man/calcular_similaridade_nomes.Rd b/man/calcular_similaridade_nomes.Rd new file mode 100644 index 0000000..54c7e23 --- /dev/null +++ b/man/calcular_similaridade_nomes.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_com_central.R +\name{calcular_similaridade_nomes} +\alias{calcular_similaridade_nomes} +\title{Calcular similaridade entre nomes} +\usage{ +calcular_similaridade_nomes(nome1, nome2) +} +\arguments{ +\item{nome1}{Primeiro nome para comparação (character)} + +\item{nome2}{Segundo nome para comparação (character)} +} +\value{ +Um valor numérico entre 0 e 1 representando a similaridade entre os nomes +} +\description{ +Esta função calcula a similaridade entre dois nomes utilizando uma combinação +ponderada de algoritmos de distância de strings (Jaro-Winkler e Damerau-Levenshtein) +após pré-processamento fonético. +} +\details{ +A função realiza os seguintes passos: +\itemize{ + \item Limpeza dos nomes usando nomesbr::limpar_nomes + \item Codificação fonética usando metaphonebr::metaphonebr + \item Cálculo da similaridade usando Jaro-Winkler (peso 0.7) e Damerau-Levenshtein (peso 0.3) +} +} +\examples{ +calcular_similaridade_nomes("Maria", "Mary") +calcular_similaridade_nomes("José", "Jose") + +} diff --git a/man/processamento_lote.Rd b/man/processamento_lote.Rd new file mode 100644 index 0000000..d20426c --- /dev/null +++ b/man/processamento_lote.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_com_central.R +\name{processamento_lote} +\alias{processamento_lote} +\title{Processamento em lote de nomes} +\usage{ +processamento_lote(vetor_nomes, chunk_size = 10000) +} +\arguments{ +\item{vetor_nomes}{Vetor de nomes para processar (character vector)} + +\item{chunk_size}{Tamanho de cada lote para processamento (default = 10000)} +} +\value{ +Um data.frame combinado com todos os resultados dos lotes +} +\description{ +Processa um vetor de nomes em lotes para encontrar nomes similares, +otimizado para grandes volumes de dados. +} +\details{ +A função: +\itemize{ + \item Remove duplicatas exatas primeiro + \item Processa os dados em blocos (chunks) para otimizar memória + \item Pode ser facilmente paralelizada modificando o loop interno +} +} +\examples{ +processamento_lote(c("Maria", "João", "Ana", "Pedro", "Francisco")) +nomes_grande_vetor <- sample(c("Maria", "João", "Ana", "Pedro", "Francisco"),100,replace = TRUE) +processamento_lote(nomes_grande_vetor, chunk_size = 50) + +} diff --git a/man/sugerir_correcao_nomes.Rd b/man/sugerir_correcao_nomes.Rd new file mode 100644 index 0000000..f74882f --- /dev/null +++ b/man/sugerir_correcao_nomes.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_com_central.R +\name{sugerir_correcao_nomes} +\alias{sugerir_correcao_nomes} +\title{Sugerir correções para um nome alvo} +\usage{ +sugerir_correcao_nomes(nome_alvo, lista_nomes, threshold_adaptativo = TRUE) +} +\arguments{ +\item{nome_alvo}{Nome para o qual se buscam correções (character)} + +\item{lista_nomes}{Vetor de nomes candidatos (character vector)} + +\item{threshold_adaptativo}{Lógico indicando se deve usar limiar adaptativo (default = TRUE)} +} +\value{ +Um data.frame com colunas 'sugestao' e 'similaridade' contendo as sugestões + que superaram o limiar mínimo +} +\description{ +Esta função sugere correções para um nome alvo com base em uma lista de nomes candidatos, +utilizando um limiar adaptativo baseado no comprimento do nome. +} +\details{ +O limiar adaptativo funciona da seguinte forma: +\itemize{ + \item Nomes com até 5 caracteres: limiar de 0.85 + \item Nomes entre 6 e 10 caracteres: limiar de 0.80 + \item Nomes com mais de 10 caracteres: limiar de 0.75 +} +} +\examples{ +sugerir_correcao_nomes("Jão", c("João", "Jonas", "Juan", "Joaquim")) +sugerir_correcao_nomes("Ana", c("Anna", "Hana", "Ana Paula"), threshold_adaptativo = FALSE) + +} diff --git a/tests/testthat/test_similaridade_nomes.R b/tests/testthat/test_similaridade_nomes.R new file mode 100644 index 0000000..dfc6a4c --- /dev/null +++ b/tests/testthat/test_similaridade_nomes.R @@ -0,0 +1,54 @@ +#' Validar abordagem de similaridade +#' +#' Função para validar a abordagem de cálculo de similaridade com casos de teste conhecidos. +#' Thresholds Recomendados + +#' Alta confiança: 0.85-0.90 (poucos falsos positivos) + +#' Balanceado: 0.75-0.85 (uso geral) + +#' Sensível: 0.65-0.75 (captura mais variações) + +#' +#' @return Imprime no console os resultados dos testes de similaridade +#' +#' @details +#' Testa a função calcular_similaridade_nomes com pares de nomes conhecidos, +#' exibindo os scores de similaridade para avaliação visual. +#' +#' @examples +#' validar_abordagem() +#' + + + +validar_abordagem <- \() { + + testes <- data.frame( + + original = c("maria", "jose", "francisco", "ana","josue"), + + variantes = c("maria", "jos\u00e9", "francisca", "anna","jose"), + + stringsAsFactors = FALSE + + ) + + + + resultado <- sapply(1:nrow(testes), \(i){ + + sim <- round(calcular_similaridade_nomes(testes$original[i], testes$variantes[i]),3) + + cat(sprintf("%s vs %s: %.3f\n", testes$original[i], testes$variantes[i], sim)) + + return(sim) + + }) + return(resultado) +} + + +testthat::test_that('teste_abordagem_similaridade_nomes', + {testthat::expect_equal(validar_abordagem(),c(1,1,0.915,1,0.893))}) + From 9ae524138a4e914efe7f5617c0a730fdf0c1ef08 Mon Sep 17 00:00:00 2001 From: Rodrigo Emmanuel Santana Borges Date: Tue, 27 Jan 2026 10:48:00 -0300 Subject: [PATCH 2/3] =?UTF-8?q?ajustes=20fun=C3=A7=C3=B5es=20similaridade?= =?UTF-8?q?=20com=20=C3=ADndice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- R/utils_com_central.R | 189 +++++++++++++++++++++++------------------- 1 file changed, 106 insertions(+), 83 deletions(-) diff --git a/R/utils_com_central.R b/R/utils_com_central.R index 146d6b5..85d5654 100644 --- a/R/utils_com_central.R +++ b/R/utils_com_central.R @@ -162,9 +162,9 @@ consulta_nome_em_central <- calcular_similaridade_nomes <- \(nome1, nome2) { - nome1_clean <- nomesbr::limpar_nomes(data.table(nome=nome1),'nome')$nome_clean + nome1_clean <- nomesbr::limpar_nomes(data.table::data.table(nome=nome1),'nome')$nome_clean - nome2_clean <-nomesbr::limpar_nomes(data.table(nome=nome2),'nome')$nome_clean + nome2_clean <-nomesbr::limpar_nomes(data.table::data.table(nome=nome2),'nome')$nome_clean # Verifica se o pacote metaphonebr está disponível if (requireNamespace("metaphonebr", quietly = TRUE)) { nome1_clean <- metaphonebr::metaphonebr(nome1_clean,verbose = F) @@ -176,7 +176,7 @@ calcular_similaridade_nomes <- \(nome1, nome2) { print("Pacote metaphonebr inexistente localmente, aplicando apenas limpeza de nomesbr") } - print(paste('nomes limpos',nome1_clean,'comparado com',nome2_clean)) + #print(paste('nomes limpos',nome1_clean,'comparado com',nome2_clean)) # Combinação ponderada de distâncias jarowinkler <- stringdist::stringsim(nome1_clean, nome2_clean, method = "jw") @@ -237,8 +237,8 @@ sugerir_correcao_nomes <- \(nome_alvo, lista_nomes, threshold_adaptativo = TRUE) if (threshold_adaptativo) { # Threshold baseado no comprimento do nome - nome_alvo <- data.table("nome"=nome_alvo) - comprimento <- nchar(limpar_nomes(nome_alvo,"nome")) + nome_alvo <- data.table::data.table("nome"=nome_alvo) + comprimento <- nchar(nomesbr::limpar_nomes(nome_alvo,"nome")) threshold <- ifelse(comprimento <= 5, 0.85, @@ -246,7 +246,7 @@ sugerir_correcao_nomes <- \(nome_alvo, lista_nomes, threshold_adaptativo = TRUE) } else { - threshold <- 0.80 # Default conservador + threshold <- 0.85 # Default conservador } @@ -258,113 +258,136 @@ sugerir_correcao_nomes <- \(nome_alvo, lista_nomes, threshold_adaptativo = TRUE) - return(data.frame(sugestao = sugestoes, similaridade = scores)) + return( + data.table::setorder( + data.table::data.table(sugestao = sugestoes, similaridade = scores), + -similaridade) + ) } -#' Busca otimizada de nomes similares -#' -#' Realiza busca eficiente de nomes similares em grandes volumes de dados, -#' utilizando pré-filtragem fonética e limitando o número de candidatos. -#' -#' @param nome_alvo Nome alvo para busca (character) -#' @param vetor_nomes Vetor de nomes onde buscar (character vector) -#' @param max_candidates Número máximo de candidatos a considerar (default = 1000) -#' -#' @return Um data.frame com colunas 'nome_original', 'sugestao' e 'similaridade' -#' ordenado por similaridade decrescente, filtrado por similaridade >= 0.75 -#' -#' @details -#' A função utiliza uma estratégia de dois estágios: -#' \itemize{ -#' \item Primeiro filtro fonético usando códigos Metaphone -#' \item Cálculo detalhado de similaridade apenas para candidatos pré-selecionados -#' } -#' -#' @examples -#' buscar_similares_otimizado("Francisco", c("Francisco", "Fran", "Franscisco", "Francisko")) -#' buscar_similares_otimizado("Maria", c("Ana", "João", "Pedro"), max_candidates = 50) -#' -#' @export -#3. Otimização para Grandes Volumes +#' Busca otimizada de nomes similares contando com índice reverso +#' +#' +#' @import data.table +#' @import duckdb +#' @import DBI +#' @import stringdist +#' @import stringi -# Usando seu metaphonebr para pré-filtragem -buscar_similares_otimizado <- \(nome_alvo, vetor_nomes, max_candidates = 1000) { +buscar_similares_indice <- \(nome,n_candidatos = 2000, + limite_similaridade=0.85, + indice='dic_palavras_metaphone.duckdb', + central='nomes_limpos_master.duckdb') { + #A) CONFIGURAÇÃO INICIAL - # Verifica se o pacote metaphonebr está disponível - if (requireNamespace("metaphonebr", quietly = TRUE)) { + #1. Conexão ao índice de 'palavras' metaphone + con <- DBI::dbConnect(duckdb::duckdb(),indice) - # Primeiro filtro fonético - metaphone_alvo <- metaphonebr::metaphonebr(nome_alvo) - metaphones_db <- metaphonebr::metaphonebr(vetor_nomes) + #2. Anexa Dados de central de nomes (somente leitura por segurança) + DBI::dbExecute(con,paste0("ATTACH '",central,"' AS central_de_nomes_db (READ_ONLY)")) + on.exit({ + DBI::dbExecute(con,'DETACH central_de_nomes_db') + DBI::dbDisconnect(con, shutdown = TRUE) + }) + #3. Validação (opcional) + #print(DBI::dbGetQuery(con,"SELECT table_name FROM information_schema.tables")) + #B) limpeza de nome - # Candidatos com mesmo código fonético - candidatos_foneticos <- which(metaphones_db == metaphone_alvo) + nome_limpo <- metaphonebr::metaphonebr(nome) + nome_palavras <- unlist(stringi::stri_extract_all_words(nome_limpo)) - if (length(candidatos_foneticos) == 0) { - - # Fallback: busca por similaridade fonética - - similaridade_fonetica <- stringdist::stringsim(metaphone_alvo, metaphones_db, method = "jw") - - candidatos_foneticos <- which(similaridade_fonetica >= 0.7) - - } - } else { - # Fallback: busca por similaridade fonética - - similaridade_fonetica <- stringdist::stringsim(nome_alvo, vetor_nomes, method = "jw") - - candidatos_foneticos <- which(similaridade_fonetica >= 0.7) - } + ##Se vazio, parar a retornar nada + if(length(nome_palavras) == 0) return(data.table::data.table()) + #C) Obtenção de candidatos + #Consulta ao índice reverso para qualquer ID com os tokens/palavras + sql_palavras <- paste(paste0("'",nome_palavras,"'"), collapse = ",") - # Limita candidatos para eficiência - candidatos <- utils::head(candidatos_foneticos, max_candidates) + ## Consulta para: + # 1. Filtrar palavras + # 2. UNNEST -> para formato long , permitindo contagem de ocorrências de id + # 3. GROUP BY id -> agrupa pelo hash do nome + # 4. COUNT(*) + # 5. ORDER DESC -> prioridade para aqueles com maior número de matches + ##IMPORTANTE - transformar ints de 128bit para string antes de chegar ao R + ## com x::VARCHAR + consulta_candidatos <- sprintf(" + WITH ocorrencias_brutas AS ( + SELECT unnest(ids) as id + FROM indice_palavras_metaphone + WHERE palavra IN (%s) + ) + SELECT id::VARCHAR as id_str, + COUNT(*) palavras_encontradas + FROM ocorrencias_brutas + GROUP BY id + ORDER BY palavras_encontradas DESC + LIMIT %d + ", sql_palavras,n_candidatos) + candidatos_classificados <- data.table::setDT(DBI::dbGetQuery(con,consulta_candidatos)) - # Calcula similaridade detalhada apenas para candidatos - if (length(candidatos) > 0) { - - similaridades <- sapply(vetor_nomes[candidatos], \(x) { - - calcular_similaridade_nomes(nome_alvo, x) - - }) - - - - return(data.frame( - - "nome_original" = nome_alvo, - - "sugestao" = vetor_nomes[candidatos], - - "similaridade" = similaridades - - ) |> dplyr::filter("similaridade" >= 0.75) |> dplyr::arrange(dplyr::desc("similaridade"))) - - } + ##Se vazio, parar a retornar nada + if(nrow(candidatos_classificados) == 0) return(data.table::data.table()) + + + #D) recuperar nomes completos dos candidatos + ids_candidatos <- candidatos_classificados$id_str + consulta_ids <- paste(paste0("'",ids_candidatos,"'"),collapse=",") + consulta_nomes <- sprintf(" + SELECT nome_original_hash::VARCHAR as id, nome_original, nome_metaphonebr + FROM central_de_nomes_db.nomes_limpos + WHERE nome_original_hash IN (%s) + ", consulta_ids) + + #Resultado é um data.table com id, nome completo e nome metaphone + dt_candidatos <- data.table::setDT(DBI::dbGetQuery(con,consulta_nomes)) + + ##Para debug juntar com score de tokens + dt_candidatos <- merge( + dt_candidatos, + candidatos_classificados, + by.x="id", + by.y="id_str" + ) + + #E) Re-rankeamento por similaridade + + + #encontrados <- sugerir_correcao_nomes(nome_limpo,dt_candidatos$nome_metaphonebr) + dt_candidatos[,similaridade:= calcular_similaridade_nomes(nome_limpo,nome_metaphonebr)] + + ##Filtrar pelo threshold + encontrados <- dt_candidatos[similaridade>=limite_similaridade] + + data.table::setorder(encontrados,-similaridade) + + return(encontrados) - return(data.frame()) } + + + + + #' Processamento em lote de nomes #' #' Processa um vetor de nomes em lotes para encontrar nomes similares, @@ -416,7 +439,7 @@ processamento_lote <- \(vetor_nomes, chunk_size = 10000) { chunk_result <- lapply(chunk, \(nome) { - buscar_similares_otimizado(nome, nomes_unicos) + buscar_similares_indice(nome, nomes_unicos) }) From a3b3101401d35d6c88c6a7ade06df87616927c5a Mon Sep 17 00:00:00 2001 From: "Rodrigo E. S. Borges" Date: Wed, 28 Jan 2026 11:44:37 -0300 Subject: [PATCH 3/3] =?UTF-8?q?adicionada=20docmuneta=C3=A7=C3=A3o=20a=20f?= =?UTF-8?q?unc=C3=A7=C3=A3o=20busca=5Fsimilares=5Findice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- R/utils_com_central.R | 59 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/R/utils_com_central.R b/R/utils_com_central.R index 85d5654..bcc8427 100644 --- a/R/utils_com_central.R +++ b/R/utils_com_central.R @@ -268,15 +268,66 @@ sugerir_correcao_nomes <- \(nome_alvo, lista_nomes, threshold_adaptativo = TRUE) -#' Busca otimizada de nomes similares contando com índice reverso -#' -#' +#' Busca otimizada de nomes similares com uso de índice reverso +#' Realiza uma busca eficiente por nomes similares em uma grande base de dados, +#' utilizando uma abordagem de duas etapas: primeiro seleciona candidatos via +#' índice invertido no DuckDB e, em seguida, refina os resultados usando +#' cálculos de distância de strings fonéticas. +#' +#' @param nome Character. O nome (ou parte de nome) para o qual se deseja encontrar +#' similares. +#' @param n_candidatos Integer. O número máximo de candidatos a serem recuperados +#' do índice invertido na primeira etapa. Padrão é 2000. Aumentar este valor +#' pode melhorar a precisão (encontrando nomes mais raros), mas diminui a performance. +#' @param limite_similaridade Numeric (0.0 a 1.0). O limiar mínimo de similaridade +#' para que um nome seja incluído no resultado final. Padrão é 0.85. +#' @param indice Character. Caminho para o arquivo do banco de dados DuckDB +#' contendo o índice invertido de palavras fonéticas (ex: 'dic_palavras_metaphone.duckdb'). +#' @param central Character. Caminho para o arquivo do banco de dados DuckDB +#' contendo a tabela central de nomes limpos (ex: 'nomes_limpos_master.duckdb'). +#' +#' @return Um \code{data.table} ordenado por similaridade decrescente, contendo as colunas: +#' \itemize{ +#' \item{\code{id}: O hash identificador único do nome encontrado.} +#' \item{\code{nome_original}: O nome completo original encontrado na base central.} +#' \item{\code{nome_metaphonebr}: A representação fonética pré-calculada do nome encontrado.} +#' \item{\code{id_str}: O ID original convertido para string (para junção).} +#' \item{\code{palavras_encontradas}: Inteiro indicando quantos tokens fonéticos do nome de entrada coincidiram com este candidato.} +#' \item{\code{similaridade}: Score numérico (0-1) indicando o grau de similaridade final.} +#' } +#' Retorna um \code{data.table} vazio se nenhum candidato for encontrado ou se o nome de entrada for inválido. +#' +#' @details +#' Esta função é projetada para performar em bases com milhões de nomes, evitando +#' varreduras completas (full table scans) e cálculos de distância de string em toda a base. +#' +#' O processo ocorre nas seguintes etapas: +#' \enumerate{ +#' \item \strong{Configuração:} Conecta ao banco de índice e anexa o banco central em modo somente leitura. +#' \item \strong{Pré-processamento da Entrada:} O \code{nome} de entrada é convertido para sua forma fonética (usando \code{metaphonebr}) e dividido em tokens (palavras). +#' \item \strong{Seleção de Candidatos (Índice Invertido):} Uma consulta SQL busca no banco de \code{indice} quaisquer IDs de nomes que contenham pelo menos um dos tokens de entrada. Os resultados são agrupados por ID, e os \code{n_candidatos} com maior número de tokens coincidentes são selecionados. +#' \item \strong{Recuperação de Dados Brutos:} Uma segunda consulta SQL busca os dados completos (nome original, metaphone pré-calculado) no banco \code{central} apenas para os IDs candidatos selecionados. +#' \item \strong{Re-rankeamento Fino:} Para o conjunto reduzido de candidatos, a função calcula a similaridade exata entre a forma fonética da entrada e a forma fonética do candidato usando a função auxiliar \code{\link{calcular_similaridade_nomes}}. +#' \item \strong{Filtragem:} Os resultados abaixo do \code{limite_similaridade} são descartados e o restante é ordenado. +#' } +#' +#' \strong{Pré-requisitos:} +#' A função assume a existência de dois arquivos DuckDB estruturados especificamente: +#' \itemize{ +#' \item \code{indice}: Deve conter a tabela \code{indice_palavras_metaphone} com mapeamento de palavras fonéticas para listas de IDs. +#' \item \code{central}: Deve conter a tabela \code{nomes_limpos} com as colunas \code{nome_original_hash}, \code{nome_original} e \code{nome_metaphonebr}. +#' } +#' +#' @seealso \code{\link{calcular_similaridade_nomes}} para detalhes sobre o cálculo do score final. +#' #' @import data.table #' @import duckdb #' @import DBI #' @import stringdist #' @import stringi - +#' @importFrom metaphonebr metaphonebr +#' +#' @export buscar_similares_indice <- \(nome,n_candidatos = 2000, limite_similaridade=0.85, indice='dic_palavras_metaphone.duckdb',