Você já se perguntou: "O que é fuzzy matching?" A correspondência difusa permite que você identifique correspondências não exatas do seu item de destino. É a pedra fundamental de muitas estruturas de mecanismos de pesquisa e um dos principais motivos pelos quais você pode obter resultados de pesquisa relevantes mesmo que tenha um erro de digitação na consulta ou um tempo verbal diferente.
Como era de se esperar, há muitos algoritmos de busca difusa que podem ser usados para texto, mas praticamente todas as estruturas de mecanismos de busca (incluindo sangrar) usam principalmente a distância Levenshtein para correspondência de strings difusas:
Distância de Levenshtein
Também conhecido como Editar distânciaé o número de transformações (exclusões, inserções ou substituições) necessárias para transformar uma cadeia de caracteres de origem na cadeia de caracteres de destino. Para um exemplo de pesquisa difusa, se o termo de destino for "book" e a fonte for "back", você precisará alterar o primeiro "o" para "a" e o segundo "o" para "c", o que nos dará uma distância de Levenshtein de 2.Você pode encontrar implementações de Levenshtein em JavaScript, Kotlin, Java e muitos outros aqui).
Além disso, algumas estruturas também suportam a distância Damerau-Levenshtein:
Distância Damerau-Levenshtein
É uma extensão da distância de Levenshtein, permitindo uma operação extra: Transposição de dois caracteres adjacentes:
Ex: TSAR para STAR
Distância Damerau-Levenshtein = 1 (A troca das posições S e T custa apenas uma operação)
Distância de Levenshtein = 2 (Substituir S por T e T por S)
Correspondência e relevância difusa
A correspondência difusa tem um grande efeito colateral: ela prejudica a relevância. Embora Damerau-Levenshtein seja um algoritmo de correspondência difusa que considera a maioria dos erros ortográficos comuns do usuário, ele também pode incluir um número significativo de falsos positivosespecialmente quando estamos usando uma linguagem com um média de apenas 5 letras por palavracomo o inglês. É por isso que a maioria das estruturas dos mecanismos de busca prefere usar a distância de Levenshtein. Vamos ver um exemplo real de correspondência fuzzy:
Primeiro, vamos usar esse conjunto de dados do catálogo de filmes. Eu o recomendo muito se você quiser brincar com a pesquisa de texto completo. Em seguida, vamos pesquisar filmes com "livro" no título. Um código simples seria parecido com o seguinte:
1 2 3 4 5 6 |
Cordas indexName = "movies_index"; MatchQuery consulta = Pesquisa.partida("livro").campo("título"); Resultado da pesquisa resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( novo Pesquisa(indexName, consulta).destaque().limite(6)); printResults(filmes); |
O código acima produzirá os seguintes resultados:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
1) O Livro Ladrão pontuação: 4.826942606027416 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> Ladrão] ----------------------------- 2) O Livro de Eli pontuação: 4.826942606027416 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Eli] ----------------------------- 3) O Livro de Vida pontuação: 4.826942606027416 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Vida] ----------------------------- 4) Preto Livro pontuação: 4.826942606027416 Acertar Locais: campo:título prazo:livro fragmento:[Preto <marca>Livro</marca>] ----------------------------- 5) O Selva Livro pontuação: 4.826942606027416 Acertar Locais: campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca>] ----------------------------- 6) O Selva Livro 2 pontuação: 3.9411821308689627 Acertar Locais: campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca> 2] ----------------------------- |
Por padrão, os resultados não diferenciam maiúsculas de minúsculas, mas você pode alterar facilmente esse comportamento criando novos índices com analisadores diferentes.
Agora, vamos adicionar um recurso de correspondência difusa à nossa consulta, definindo a imprecisão como 1 (distância de Levenshtein 1), o que significa que "livro" e "olhar" terá a mesma relevância.
1 2 3 4 5 6 |
Cordas indexName = "movies_index"; MatchQuery consulta = Pesquisa.partida("livro").campo("título").imprecisão(1); Resultado da pesquisa resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( novo Pesquisa(indexName, consulta).destaque().limite(6)); printResults(filmes); |
E aqui está o resultado da pesquisa difusa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
1) Gancho pontuação: 1.1012248063242538 Acertar Locais: campo:título prazo:gancho fragmento:[<marca>Gancho</marca>] ----------------------------- 2) Aqui Vem o Boom pontuação: 0.7786835148361213 Acertar Locais: campo:título prazo:estrondo fragmento:[Aqui Vem o <marca>Boom</marca>] ----------------------------- 3) Veja Quemtambém está falando Pontuação: 0.7047266634351538 Locais de acerto: campo:título termo:look fragmento:[Look Who's Conversação Também] ----------------------------- 4) Veja QuemConversando Pontuação: 0.7047266634351538 Locais de acerto: campo:título termo:look fragmento:[Look Who's Conversação] ----------------------------- 5) O Livro Ladrão pontuação: 0.5228811753737184 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> Ladrão] ----------------------------- 6) O Livro de Eli pontuação: 0.5228811753737184 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Eli] ----------------------------- |
Agora, o filme chamado "Gancho" é o primeiro resultado da pesquisa, que pode não ser exatamente o que o usuário está esperando em uma pesquisa por "Livro".
Como minimizar os falsos positivos durante as pesquisas difusas
Em um mundo ideal, os usuários nunca cometeriam erros de digitação ao pesquisar algo. No entanto, esse não é o mundo em que vivemos e, se você quiser que seus usuários tenham uma experiência agradável, terá de lidar com pelo menos uma distância de edição de 1. Portanto, a verdadeira questão é: como podemos fazer a correspondência de strings fuzzy minimizando a perda de relevância?
Podemos tirar proveito de uma característica da maioria das estruturas de mecanismos de pesquisa: Uma correspondência com uma distância de edição menor geralmente terá uma pontuação mais alta. Essa característica nos permite combinar essas duas consultas com diferentes níveis de imprecisão em uma só:
1 2 3 4 5 6 7 8 |
Cordas indexName = "movies_index"; Cordas palavra = "Livro"; MatchQuery titleFuzzy = Pesquisa.partida(palavra).imprecisão(1).campo("título"); MatchQuery titleSimple = Pesquisa.partida(palavra).campo("título"); DisjunctionQuery ftsQuery = Pesquisa.disjuntos(titleSimple, titleFuzzy); Resultado da pesquisa resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( novo Pesquisa(indexName, ftsQuery).destaque().limite(20)); printResults(resultado); |
Aqui está o resultado da consulta fuzzy acima:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
1) O Livro Ladrão pontuação: 2.398890092610849 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> Ladrão] ----------------------------- campo:título prazo:livro fragmento:[O <marca>Livro</marca> Ladrão] ----------------------------- 2) O Livro de Eli pontuação: 2.398890092610849 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Eli] ----------------------------- campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Eli] ----------------------------- 3) O Livro de Vida pontuação: 2.398890092610849 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Vida] ----------------------------- campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Vida] ----------------------------- 4) Preto Livro pontuação: 2.398890092610849 Acertar Locais: campo:título prazo:livro fragmento:[Preto <marca>Livro</marca>] ----------------------------- campo:título prazo:livro fragmento:[Preto <marca>Livro</marca>] ----------------------------- 5) O Selva Livro pontuação: 2.398890092610849 Acertar Locais: campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca>] ----------------------------- campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca>] ----------------------------- 6) O Selva Livro 2 pontuação: 1.958685557004688 Acertar Locais: campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca> 2] ----------------------------- campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca> 2] ----------------------------- 7) Nacional Tesouro: Livro de Segredos pontuação: 1.6962714808368062 Acertar Locais: campo:título prazo:livro fragmento:[Nacional Tesouro: <marca>Livro</marca> de Segredos] ----------------------------- campo:título prazo:livro fragmento:[Nacional Tesouro: <marca>Livro</marca> de Segredos] ----------------------------- 8) Americano Torta Presentes: O Livro de Amor pontuação: 1.517191317611584 Acertar Locais: campo:título prazo:livro fragmento:[Americano Torta Presentes: O <marca>Livro</marca> de Amor] ----------------------------- campo:título prazo:livro fragmento:[Americano Torta Presentes: O <marca>Livro</marca> de Amor] ----------------------------- 9) Gancho pontuação: 0.5052232518679681 Acertar Locais: campo:título prazo:gancho fragmento:[<marca>Gancho</marca>] ----------------------------- 10) Aqui Vem o Boom pontuação: 0.357246781294941 Acertar Locais: campo:título prazo:estrondo fragmento:[Aqui Vem o <marca>Boom</marca>] ----------------------------- 11) Veja Quemtambém está falando Pontuação: 0.32331663301992025 Locais de acerto: campo:título termo:look fragmento:[Look Who's Conversação Também] ----------------------------- 12) Veja QuemConversando Pontuação: 0.32331663301992025 Locais de acerto: campo:título termo:look fragmento:[Look Who's Conversação] ----------------------------- |
Como você pode ver, esse resultado está muito mais próximo do que o usuário poderia esperar. Observe que estamos usando uma classe chamada DisjunctionQuery agora, pois as disjunções são equivalentes a "OU" no SQL.
O que mais poderíamos melhorar para reduzir o efeito colateral negativo de um algoritmo de correspondência difusa? Vamos reanalisar nosso problema para entender se ele precisa de mais melhorias:
Já sabemos que as pesquisas difusas podem produzir alguns resultados inesperados (por exemplo, Book -> Look, Hook). No entanto, uma pesquisa de termo único geralmente é uma consulta terrível, pois mal nos dá uma dica do que exatamente o usuário está tentando realizar.
Mesmo o Google, que tem um dos algoritmos de pesquisa difusa mais desenvolvidos em uso, não sabe exatamente o que estou procurando quando pesquiso "tabela":
Então, qual é o tamanho médio das palavras-chave em uma consulta de pesquisa? Para responder a essa pergunta, mostrarei um gráfico de Apresentação de Rand Fishkin em 2016. (Ele é um dos gurus mais famosos do mundo de SEO)
De acordo com o gráfico acima, ~80% das consultas de pesquisa têm 2 ou mais palavras-chave, portanto, vamos tentar pesquisar o filme "Black Book" usando a fuzziness 1:
1 2 3 4 5 6 |
Cordas indexName = "movies_index"; MatchQuery consulta = Pesquisa.partida("Livro Negro").campo("título").imprecisão(1); Resultado da pesquisa resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( novo Pesquisa(indexName, consulta).destaque().limite(12)); printResults(filmes); |
Resultado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
1) Preto Livro pontuação: 0.6946442424065404 Acertar Locais: campo:título prazo:livro fragmento:[<marca>Preto</marca> <marca>Livro</marca>] ----------------------------- campo:título prazo:preto fragmento:[<marca>Preto</marca> <marca>Livro</marca>] ----------------------------- 2) Gancho pontuação: 0.40139742528039857 Acertar Locais: campo:título prazo:gancho fragmento:[<marca>Gancho</marca>] ----------------------------- 3) Ataque o Bloqueio pontuação: 0.2838308365090324 Acertar Locais: campo:título prazo:bloco fragmento:[Ataque o <marca>Bloqueio</marca>] ----------------------------- 4) Aqui Vem o Boom pontuação: 0.2838308365090324 Acertar Locais: campo:título prazo:estrondo fragmento:[Aqui Vem o <marca>Boom</marca>] ----------------------------- 5) Veja Quemtambém está falando Pontuação: 0.25687349813115684 Locais de acerto: campo:título termo:look fragmento:[Look Who's Conversação Também] ----------------------------- 6) Veja QuemConversando Pontuação: 0.25687349813115684 Locais de acerto: campo:título termo:look fragmento:[Look Who's Conversação] ----------------------------- 7) Grosseiro Pointe Em branco pontuação: 0.2317469073782136 Acertar Locais: campo:título prazo:em branco fragmento:[Grosseiro Pointe <marca>Em branco</marca>] ----------------------------- 8) O Livro Ladrão pontuação: 0.19059065534780495 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> Ladrão] ----------------------------- 9) O Livro de Eli pontuação: 0.19059065534780495 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Eli] ----------------------------- 10) O Livro de Vida pontuação: 0.19059065534780495 Acertar Locais: campo:título prazo:livro fragmento:[O <marca>Livro</marca> de Vida] ----------------------------- 11) O Selva Livro pontuação: 0.19059065534780495 Acertar Locais: campo:título prazo:livro fragmento:[O Selva <marca>Livro</marca>] ----------------------------- 12) Voltar para o Futuro pontuação: 0.17061000968368 Acertar Locais: campo:título prazo:voltar fragmento:[<marca>Voltar</marca> para o Futuro] ----------------------------- |
Nada mal. Obtivemos o filme que estávamos procurando como o primeiro resultado. No entanto, uma consulta de disjunção ainda traria um conjunto melhor de resultados.
Mas, ainda assim, parece que temos uma nova propriedade interessante aqui; o efeito colateral da correspondência de imprecisão diminui ligeiramente à medida que o número de palavras-chave aumenta. Uma busca por "Livro preto" com imprecisão 1 ainda pode trazer resultados como back look ou lack cook (algumas combinações com edit distance 1), mas é improvável que esses sejam títulos de filmes reais.
Uma busca por "livro eli" com a imprecisão 2 ainda o traria como o terceiro resultado:
1 2 3 4 5 6 |
Cordas indexName = "movies_index"; MatchQuery consulta = Pesquisa.partida("book eli").campo("título").imprecisão(2); Resultado da pesquisa resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( novo Pesquisa(indexName, consulta).destaque().limite(12)); printResults(filmes); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
1) Ed Madeira pontuação: 0.030723793900031805 Acertar Locais: campo:título prazo:madeira fragmento:[<marca>Ed</marca> <marca>Madeira</marca>] ----------------------------- campo:título prazo:ed fragmento:[<marca>Ed</marca> <marca>Madeira</marca>] ----------------------------- 2) Homem de Tai Chi pontuação: 0.0271042761982626 Acertar Locais: campo:título prazo:chi fragmento:[Homem de <marca>Tai</marca> <marca>Chi</marca>] ----------------------------- campo:título prazo:tai fragmento:[Homem de <marca>Tai</marca> <marca>Chi</marca>] ----------------------------- 3) O Livro de Eli pontuação: 0.02608335441670756 Acertar Locais: campo:título prazo:eli fragmento:[O <marca>Livro</marca> de <marca>Eli</marca>] ----------------------------- campo:título prazo:livro fragmento:[O <marca>Livro</marca> de <marca>Eli</marca>] ----------------------------- 4) O Bom Mentira pontuação: 0.02439822770591834 Acertar Locais: campo:título prazo:mentira fragmento:[O <marca>Bom</marca> <marca>Mentira</marca>] ----------------------------- campo:título prazo:bom fragmento:[O <marca>Bom</marca> <marca>Mentira</marca>] ----------------------------- |
Entretanto, como a palavra média em inglês tem 5 letras, eu NÃO recomendamos o uso de uma distância de edição maior que 2, a menos que o usuário esteja pesquisando palavras longas e fáceis de serem escritas incorretamente, como "Schwarzenegger", por exemplo (pelo menos para quem não é alemão ou austríaco).
Conclusão
Neste artigo, discutimos a correspondência difusa e como superar seu principal efeito colateral sem prejudicar sua relevância. Lembre-se de que a correspondência difusa é apenas um dos muitos recursos que você deve aproveitar ao implementar uma pesquisa relevante e fácil de usar. Vamos discutir alguns deles durante esta série: N-Gramas, Stopwords, Steeming, Shingle, Elision. Etc.
Confira também o Parte 1 e Parte 2 desta série.
Enquanto isso, se você tiver alguma dúvida, envie-me um tweet para @deniswsrosa.
Como a pontuação retornada do índice está relacionada a uma correspondência percentual? Como seria possível construir uma consulta para a qual a pontuação de relevância dos resultados representasse uma relevância de > 50%, por exemplo?
Se você quiser saber mais sobre como os documentos são pontuados, tanto o lucene quanto o bleve têm o método "explain". No couchbase, você pode definir explain(true) para ver exatamente como a pontuação é calculada. Por padrão, os resultados são classificados por pontuação, de modo que os mais relevantes devem ser os primeiros da lista. O que você está tentando alcançar exatamente?