Essa é uma proposta de pesquisa textual implementada com recursos em python puro com o uso de dicionário de sinônimos e distância entre termos pesquisados.
- É uma pesquisa que tenta ir além do que pesquisas comuns fazem, pois não tem o objetivo de trazer grandes volumes de resultados, mas resultados precisos.
- Implementada em python para uso em pesquisa textual avançada com foco no Português, permitindo busca em campos textuais e critérios de proximidade textual.
- O objetivo é refinar pesquisas textuais com frameworks comuns de mercado (MemSQL/SingleStore, ElasticSearch) em volume muito grande de dados, ou pode ser usada para pesquisa completa em textos. Ou análise em tempo real se textos correspondem a critérios pré estabelecidos (regras de texto para mudança de fluxo de trabalho).
- Essa ideia não é nova, conheci ao longo dos últimos 20 anos vários sistemas que faziam algo parecido. Não há pretensão em competir com qualquer um desses produtos, mas ter algo simples e operacional para quem tiver interesse em personalizar uma busca textual da forma que precisar.
- Uma aplicação muito útil dos critérios de pesquisa, alé de encontrar textos, é identificar rótulos que são aplicáveis a um texto ao testar um conjunto de regras pré-definidas com seus rótulos correspondentes, simulando um classificador multilabel só que no lugar do modelo, tem-se um conjunto de regras textuais. Daí pode-se identificar fluxos automáticos para sistemas, definir alertas, etc.
- O componente já pode ser usado com o serviço de regras, basta baixar a pasta componente e baixar o exemplo do serviço na pasta servico_regras
-
Classe python PesquisaBR que recebe um documento e um critério de pesquisa e retorna a avaliação.
-
Classe python RegrasPesquisaBR que recebe um conjunto de regras e seus rótulos e aplica as regras em um documento, identificando que rótulos são aplicáveis a ele. Simula um modelo multilabel mas com regras no lugar de um modelo de IA.
-
Serviço avaliador de regras: Um exemplo simples de serviço que simula um classificador multilabel que funciona por regras no lugar de um modelo treinado.
-
Testes da classe que permitem validar todos os critérios e funcionalidades implementadas
-
Conversor de pesquisas com critérios avançados para critérios simples AND OR NOT aceitos pelo MemSQL
-
Classe experimental PesquisaBRMemSQL : classe que permite combinar a análise de pesquisa da classe PesquisaBR com o poder de pesquisa textual nativo do MemSQL. Agora o MemSQL chama-se SingleStore
-
Manual com os operadores de pesquisa
pb = PesquisaBR(texto = 'A casa de papel é um seriado muito legal', criterios='casa adj2 papel adj5 seriado')
print('Retorno: ', pb.retorno())
print(pb.print_resumo())
Console
Retorno: True
RESUMO DA PESQUISA: retorno = True
- texto: a casa de papel e um seriado muito legal
- tokens: ['a', 'casa', 'de', 'papel', 'e', 'um', 'seriado', 'muito', 'legal']
- tokens_unicos: {'papel', 'a', 'um', 'muito', 'de', 'e', 'seriado', 'legal', 'casa'}
- criterios: ['casa', 'adj2', 'papel', 'adj5', 'seriado']
- mapa: {'a': {'t': [0], 'p': [0], 'c': ['']}, 'casa': {'t': [1], 'p': [0], 'c': ['']}, 'de': {'t': [2], 'p': [0], 'c': ['']}, 'papel': {'t': [3], 'p': [0], 'c': ['']}, 'e': {'t': [4], 'p': [0], 'c': ['']}, 'um': {'t': [5], 'p': [0], 'c': ['']}, 'seriado': {'t': [6], 'p': [0], 'c': ['']}, 'muito': {'t': [7], 'p': [0], 'c': ['']}, 'legal': {'t': [8], 'p': [0], 'c': ['']}}
regras = [{'grupo' : 'receitas_bolo', 'rotulo': 'Receita de Bolo', 'regra': 'receita ADJ10 bolo'},
{'grupo' : 'receitas_bolo', 'rotulo': 'Receita de Bolo', 'regra': 'aprenda ADJ5 fazer ADJ10 bolo'},
{'grupo' : 'receitas_pao', 'rotulo': 'Receita de Pão', 'regra': 'receita PROX15 pao'},
{'grupo' : 'grupo teste', 'rotulo': 'teste', 'regra': 'teste'}]
# receita de bolo
texto = 'nessa receita você vai aprender a fazer bolos incríveis'
pbr = RegrasPesquisaBR(regras = regras, print_debug=False)
rotulos = pbr.aplicar_regras(texto = texto)
print(f'Rótulos encontrados para o texto: "{texto}" >> ', rotulos)
Console
Rótulos encontrados para o texto: "nessa receita você vai aprender a fazer bolos incríveis" >> ['Receita de Bolo']
Estão disponíveis diversos textos e pesquisas que são testados para garantir o funcionamento da classe durante o desenvolvimento.
python pesquisabr_testes.py
Implementei aqui um conjunto de operadores de pesquisa por proximidade dos termos e outros operadores para refinamento de pesquisa. Esses tipos de operadores tornam-se importantes para refinar pesquisas em grande volume de dados, onde não é importante trazer muito resultado, mas um resultado o mais próximo possível do que é procurado. Ferramentas comuns de busca como ElasticSearch e o próprio MemSQL não trazem nativamente esses tipos de operadores.
Essa ideia não é nova, conheci ao longo dos últimos 20 anos vários sistemas que faziam algo parecido. Não há pretensão em competir com qualquer um desses produtos, mas ter algo simples e operacional para quem tiver interesse em personalizar uma busca textual da forma que precisar.
Esse tipo de pesquisa permite o uso de dicionário de sinônimos em qualquer língua, inclusive o uso de recursos fonéticos. O texto de entrada é pré-processado para evitar não encontrar termos com grafia incorreta de acentos ou termos no singular/plural, bem como números com pontuação ou sem. Por padrão o texto é pesquisado no singular, removendo pronomes oblíquos, mas é possível localizar o termo real usando aspas (exceto acentos que sempre são desconsiderados).
A pesquisa também permite localizar termos pelo dicionário de sinônimos. Ao pesquisar a palavra "genitor", o sistema pesquisa também "pai". A tabela de sinônimos é flexível e facilmente atualizável, permitindo incluir termos em outras línguas se desejado. O uso de sinônimos pode ser ativado ou desativado a cada pesquisa. Ao pesquisar termos entre aspas, o sinônimo é desativado para o termo ou conjunto de termos entre aspas enquanto os outros termos podem ser pesquisados com o uso dos sinônimos.
O pré-processamento envolve:
- retirada de acentos
- redução a um pseudosingular ou singular estimado: não é um português perfeito, mas uma singularização para a máquina localizar termos com maior flexibilidade
Conectores ou operadores de pesquisa são termos especiais utilizados em sistemas de pesquisa para indicar a relação desejada entre os termos pesquisados. Por exemplo, se é desejado encontrar documentos com a palavra casa e a palavra papel, pode-se escrever o critério de pesquisa como casa papel ou pode-se escrever casa E papel. O operador E está subentendido quando nenhum operador é informado. Para usar termos que são iguais aos operadores, é necessário colocar o termo entre aspas. Ex.: amor e ódio deveria ser escrito como amor "e" ódio para garantir que os três termos existem no texto. Ou também "amor e ódio" para que os termos sejam exigidos nessa sequência, um seguido do outro.
- E: conector padrão, exige a existência do termo no documento
- NÃO: nega a existência de um termo no documento
- OU entre termos: indica que um ou outro termo podem ser encontrados para satisfazer a pesquisa ex.: | "fazer" OU "feito" E casa | realiza uma pesquisa que encontre (fazer ou feito literalmente) e também (casa ou termos que no singular sejam escritos como casa)
- OU com parênteses: permite realizar pesquisas mais complexas. Ex.: | (casa ADJ5 papel) ou (casa ADJ5 moeda) |. Nesse caso a pesquisa poderia ser simplificada como | casa ADJ5 papel ou moeda |
- ADJn: permite localizar termos que estejam até n termos a frente do primeiro termo. Ex.: | casa ADJ3 papel | vai localizar textos que contenham "casa de papel", "casa papel", "casa feita de papel", mas não localizaria "casa feita de muito papel".
- ADJCn: equivalente ao ADJ padrão, mas obriga que os dois termos estejam presentes no mesmo parágrafo. Não necessariamente a mesma sentença, mas o mesmo parágrafo considerando a quebra /n no texto
- PROXn: semelhante ao ADJ, mas localiza termos posteriores ou anteriores ao primeiro termo pesquisado. Ex.: | casa PROX3 papel | vai localizar textos que contenham "casa de papel", "papel na casa", "papel colado na casa", "casa feita de papel", mas não localizaria "casa feita de muito papel" ou "papel desenhado e colado na casa".
- PROXCn: equivalente ao PROX padrão, mas obriga que os dois termos estejam presentes no mesmo parágrafo. Não necessariamente a mesma sentença, mas o mesmo parágrafo considerando a quebra /n no texto
- COMn: obriga que os dois termos pesquisados estejam presentes em um mesmo parágrafo, independente da distância e da ordem. Ex.: | casa COM papel | avalia se o texto contém "casa" e papel em um mesmo parágrafo, em qualquer ordem e distância. Opcionalmente pode-se informar o número de parágrafos. COM1 avalia se os termos estão no mesmo parágrafo, COM2 avalia no parágrafo e o seguinte, e assim por diante.
- MESMO: os documentos podem ser indexados com um tipo único, ou com tipos independentes como, por exemplo: resumo, dados textuais complementares e o texto original. O operador MESMO permite que o documento seja encontrado apenas se os termos pesquisados estiverem em um mesmo tipo do documento. Sem o operador MESMO, o texto será localizado se tiver um termo em um tipo (resumo por exemplo) e outro termo em outro tipo (índice remissivo, por exemplo). O operador MESMO funciona apenas como substituição do operador E, pois os operdores ADJ, ADJC, PROX, PROXC e COM subentendem o uso do operador MESMO por usarem recrusos de distância entre termos. Ex.: | casa MESMO papel | vai localizar textos que contenham "casa" E "papel" no mesmo tipo de documento, caso o termo "casa" esteja no resumo e "papel" esteja no índice, o texto não será localizado.
- $: permite o uso de partes do termo no critério de pesquisa. Por exemplo: cas$ vai encontrar casa, casinha, casamento...
- ?: permite a existência ou não te um caracter no lugar do símbolo "?". Por exemplo: cas? vai encontrar cas, casa, caso, case... Pode estar no meio do termo tamém: ca?a vai encontrar caa, casa, cata, cala ...
Esse operador foi criado para remover trechos do texto antes da análise da regra, para o caso de existirem trechos conhecidos que podem resultar em faso positivos para a regra, como cabeçalhos, citações, dentre outros. Pode-se usar quantos remover(...)
forem necessários
Como usar o operador remover(texto)
:
$
ou * - de 0 a 100 caracteres quaisquer?
- um caractere de letra ou número opcional&
- um caractere de letra ou número obrigatório#
- um a 10 caracteres que não sejam letra nem número (pontuação, início ou final de texto, espaço, etc)*#
- caracteres até um símbolo (pontuação, início ou final de texto, espaço, etc)*##
- caracteres até uma quebra de linha%
- aspas, parênteses, chaves ou colchetes (citações/explicações em geral)"
- aspas normal
Exemplos de uso do remover:
- remover(aspas): remove todo o conteúdo do texto entre aspas ou parênteses, com o objetivo de remoção de citações
- remover(termo1 termo2 termo3): remove o trecho do texto
termo1 termo2 termo3
conforme está escrito dentro dos parênteses do remover(...) - remover(termo&): remove qualquer trecho que contenha termo seguido de um número ou letra obrigatória
- remover(termo?): remove qualquer trecho que contenha termo podendo ou não estar seguido de um número ou letra
- remover(contab*#): remove todo o texto iniciado por
contab
até encontrar o final da palavra - remover(conforme exemplos*##): remove todo o texto iniciado por
conforme exemplos
até encontrar uma quebra de linha
Exemplos de uso dentro dos critérios de pesquisa:
- `casa adj2 papel remover(termo1) remover(teste)'
- Ao ser aplicado o critério no texto
o seriado casa termo1 de teste papel', a avaliação será verdadeira já que os termos
termo1e
teste` serão removidos antes da análise.
- ao encontrar um termo no texto analisado, os sinônimos são mapeados como se fossem esse termo ** sinônimos compostos são analisados apenas para termos entre aspas nos critérios de pesquisa
- Sinônimos: {'alegre': ['feliz','sorridente'], 'feliz':['alegre','sorridente'], 'sorridente':['alegre','feliz'], 'casa':['apartamento'] }
- Sinônimos compostos: {'casa_de_papel':['la casa de papel','a casa de papel'], "inss" : ['instituto nacional de seguridade social'], 'instituto_nacional_de_seguridade_social':['inss']}
Com esse mapeamento, se o critério de pesquisa estiver escrito "alegre" é o mesmo que pesquisar (alegre ou feliz ou sorridente). Se estiver escrito "alegre" entre aspas, os sinônimos não serão pesquisados. Os sinônimos compostos possuem um comportamento peculiar, permitem o mapeamento de expressões, siglas, etc. Se o critério de pesquisa estiver escrito "inss" é o mesmo que pesquisar (inss ou "instituto nacional de seguridade social"). Mas se no critério de pesquisa estiver escrito inss sem aspas, somente será pesquisada a palavra inss.
- esses textos serão usados mais abaixo
- Texto único: A casa de papel é um seriado muito interessante
- Texto composto: {'texto' : 'A casa de papel é um seriado muito interessante', 'tipo' : 'seriado', 'ano': '2017', 'comentario': 'seriado muito bom'}
- o operador E é padrão para pesquisas sem operadores entre termos
- ao pesquisar "papeis", a pesquisa vai localizar no texto o termo "papel", pois o texto estará singularizado e o critério de pesquisa também
- Termos simples: casa papel
- Termos simples com curingas: casa? E papeis
- Termos simples com operadores: casa E papel E seriado
- Termos simples com operadores e parênteses: (casa E papel) ou (papel E seriado)
- Termos literias: "casa de papel" E seriado
- Termos próximos: casa ADJ2 papel ADJ5 seriado
- Termos próximos em qualquer ordem: papel PROX2 casa ADJ10 seriado
- Termos no mesmo parágrafo: papel PROX2 casa COM seriado
- operadores especiais alteram o comportamento da pesquisa. Ao colocar um termo no critério de pesquisa seguido de .nomo_campo., o critério será analisado apenas no campo informado. ** um conjunto de critérios pode ser analisado no campo colocando (termo1 E termo2).nome_campo. ** combinações mais complexas podem ser feitas em conjuntos de critérios (termo1.campo1. E termo2 E termo3).campo2. - operadores de campos internos serão avaliados no lugar dos externos quando existirem.
- critérios por campo: (papel PROX2 casa).texto. E 2017.ano=.
- campo ANO>=2017: papel PROX2 casa E 2017.ano>=.
- critérios por campo: (papel PROX2 casa).texto. E 2017.ano=.
- critérios por campo (escrita alternativa): (papel PROX2 casa).texto. E @ano=2017
- critérios por campos diferentes: (papel PROX2 casa).texto. E 2017.ano=. E "muito bom".comentario.
- palavras simples são analisadas como se fossem seus sinônimos. Os sinônimos simples são desativados em termos entre aspas.
- os sinônimos compostos são analisados apenas em palavras entre aspas no critério de pesquisa
- apartamento = casa: papel PROX2 apartamento ADJ10 seriado
- Sinônimos: papel PROX2 apartamento ADJ10 seriado
Exemplos disponíveis no arquivo testes_exemplos.py e testes_exemplos_sem_db.py Para uso da classe PesquisaBRMemSQL é necessário ter instalado o MemSQL (pode ser o container de exemplo). E criar as tabelas e funções do database pesquisabr. Scripts disponívels db_funcoes.sql e db_tabelas.sql
Esse é um serviço simples de exemplo do uso da classe de avaliação de regras para gerar um classificador multilabel por regras.
O arquivo regras.json contém uma lista de regras de exemplo. As regras podem estar em um banco de dados que o serviço carrega ao subir, ou em um arquivo texto mesmo. Depois basta chamar o serviço passando o texto que ele retorna os rótulos aplicáveis com base nas regras carregadas.
A responsabilidade do serviço é rotular o texto recebido, comportando-se como um classificador multilabel por regras.
Opcionalmente pode-se informar ao serviço que regras devem ser testadas, passando uma tag ou conjunto de tags ou o nome do grupo da regra.
É possível usar regex no lugar dos critérios textuais para regras mais refinadas. Para isso, basta registrar a regra com r: no início da regra. Ex.: r:oficio \d+
O serviço de exemplo está na subpasta: servico_regras da pasta do projeto (https://github.com/luizanisio/PesquisaTextualBR/tree/master/projeto_e_exemplos/servico_regras).
Também é possível incluir critérios especiais nas regras que servem para ignorar trechos de textos, sendo eles:
- remover(aspas) --> remover trechos entre aspas do texto antes de verificar os critérios definidos na regra
- remover(um texto qualquer) --> remove o texto entre parênteses do texto antes de verificar os critérios definidos na regra
O trecho incluído entre os parênteses será processado e comparado com o texto recebido após ser processado também. Ou seja, se for usado o critério remover(esse texto), ele vai remover do texto recebido os conjuntos: esse texto, esses textos, esse textos, esses texto. Os operadores especiais de remoção de texto funcionam apenas nas regras, no componente RegrasPesquisaBR, não são analisados ao avaliar critérios diretamente no componente PesquisaBR. E a análise de regras foi otimizada para reprocessar o mínimo possível os textos ao serem submetidos a várias regras com ou sem remoção de texto, cabeçalho ou rodapé. Exemplos de regras:
- casa adj2 papel remover(aspas) remover(casa do papel)
- oficio adj5 remetido remover(de oficio)
{"texto": "esse é um texto legal", "criterio": " texto PROX10 legal", "detalhar": 1, "grifar":1}
Retorno
{ "criterios": "texto PROX10 legal",
"criterios_aon": "texto AND legal", "retorno": true,
"texto": "esse e um texto legal",
"texto_grifado": "esse e um <mark>texto</mark> <mark>legal</mark>" }
{"texto": "esse texto tem umas receitas de pão e de bolos legais 123 456 um dois três com o oficio número 5.174", "detalhar": 0 }
Retorno
{ "extracoes": [["oficio numero 5174"],[],["receita de pao"]],
"rotulos": ["oficio","Receita de Bolo","Receita de Pão"] }
- POST: http://localhost:8000/analisar_regras
- o detalhar=1 nesse exeplo retorna a regra identificada por cada rótulo e o texto processado*
- a chave opcional tags pode ser usada para filtras e avaliar apenas regras que contenham uma das tags
- a chave opcional grupo pode ser usada para filtrar e avaliar apenas regras de um determinado grupo
{"texto": "esse ofício 12 texto tem umas receitas de pão e de bolos legais 123 456 um dois são vários testes três com o oficio número 5.174",
"detalhar":0, "tags":"oficio"}
Retorno
{ "extracoes": [ [ "12", "numero 5174" ] ],
"rotulos": [ "oficio" ] }
Regras desse exemplo (arquivo regras.json):
- as chaves tags, qtd_cabecalho, qtd_rodape e extracao são opcionais
- regra: é a regra usando os operadores de pesquisa textual, ou um regex. No caso de regex, a regra deve começar com r: regex desejado
- rotulo: é o rótulo do grupo que será retornado se a regra retornar true
- qtd_cabecalho: a regra é aplicada no início do texto até o caracter da posição informada
- qtd_rodape: a regra é aplicada no final do texto, do caracter da posição informada até o fim
- qtd_cabecalho e qtd_rodape: a regra é aplicada removento o miolo do texto de qtd_cabecalho até qtd_rodape
- extracao: é um regex usado para extrair informações do texto se a regra retornar true
{"regras": [
{"grupo" : "receitas_bolo", "rotulo": "Receita de Bolo", "regra": "receita ADJ10 bolo", "tags": "receita bolo", "qtd_cabecalho":0, "qtd_rodape":0},
{"grupo" : "receitas_bolo", "rotulo": "Receita de Bolo", "regra": "aprenda ADJ5 fazer ADJ10 bolo", "tags": "receita bolo", "qtd_cabecalho":0, "qtd_rodape":0},
{"grupo" : "receitas_pao", "rotulo": "Receita de Pão", "regra": "receita PROX15 pao", "extracao": "(receita.*pao)|(pao.*receita)", "tags": "receita pao", "qtd_cabecalho":0, "qtd_rodape":0},
{"grupo" : "grupo_teste", "rotulo": "teste", "regra": "teste", "extracao": "(\\d+)(\\Wum\\W|\\Wdois\\W|\\Wtres\\W)", "tags": "teste", "qtd_cabecalho":0, "qtd_rodape":0},
{"grupo" : "grupo_regex", "rotulo": "teste regex", "regra": "r:teste|testar?", "extracao": "", "tags": "teste", "qtd_cabecalho":0, "qtd_rodape":0},
{"grupo" : "grupo_oficio", "rotulo": "oficio", "regra": "r:oficio (n.{1,10})?\\d+", "extracao": "(?<=oficio\\W)(?:n|numero|num|nro)?(?:\\s*\\d+)(?=$|\\W)" , "tags": "teste oficio", "qtd_cabecalho":20, "qtd_rodape":20}
]
}