¿Se ha preguntado alguna vez qué es la concordancia difusa? La concordancia difusa permite identificar coincidencias no exactas del elemento buscado. Es la piedra angular de muchos motores de búsqueda y una de las principales razones por las que puede obtener resultados de búsqueda relevantes incluso si tiene un error tipográfico en su consulta o un tiempo verbal diferente.
Como cabría esperar, existen muchos algoritmos de búsqueda difusa que pueden utilizarse para el texto, pero prácticamente todos los marcos de los motores de búsqueda (incluido bleve) utilizan principalmente la distancia Levenshtein para la coincidencia difusa de cadenas:
Distancia Levenshtein
También conocido como Editar Distanciaes el número de transformaciones (supresiones, inserciones o sustituciones) necesarias para transformar una cadena de origen en la de destino. Para un ejemplo de búsqueda difusa, si el término de destino es "libro" y el de origen es "espalda", será necesario cambiar la primera "o" por "a" y la segunda "o" por "c", lo que nos dará una Distancia de Levenshtein de 2.Editar Distancia es muy fácil de implementar, y es un reto popular durante las entrevistas de código (Puedes encontrar implementaciones de Levenshtein en JavaScript, Kotlin, Java y muchos otros aquí).
Además, algunos marcos también admiten la distancia Damerau-Levenshtein:
Distancia Damerau-Levenshtein
Es una extensión de la distancia Levenshtein, que permite una operación extra: Transposición de dos caracteres adyacentes:
Ex: De TSAR a STAR
Distancia Damerau-Levenshtein = 1 (La conmutación de las posiciones S y T sólo cuesta una operación)
Distancia Levenshtein = 2 (Sustituir S por T y T por S)
Correspondencia difusa y relevancia
La concordancia difusa tiene un gran efecto secundario: estropea la relevancia. Aunque Damerau-Levenshtein es un algoritmo de concordancia difusa que tiene en cuenta la mayoría de los errores ortográficos comunes de los usuarios, también puede incluir un número significativo de falsos positivossobre todo cuando utilizamos una lengua con un media de sólo 5 letras por palabracomo el inglés. Por eso, la mayoría de los motores de búsqueda prefieren utilizar la distancia Levenshtein. Veamos un ejemplo real de concordancia difusa:
En primer lugar, vamos a utilizar este conjunto de datos de catálogos de películas. Lo recomiendo encarecidamente si quieres jugar con la búsqueda de texto completo. A continuación, busquemos películas con "Libro"en el título. Un código sencillo sería el siguiente:
|
1 2 3 4 5 6 |
Cadena indexName = "movies_index"; MatchQuery consulta = BúsquedaQuery.match("libro").campo("título); SearchQueryResult resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( nuevo BúsquedaQuery(indexName, consulta).destacar().límite(6)); imprimirResultados(películas); |
El código anterior dará los siguientes 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) En Reserve Ladrón puntuación: 4.826942606027416 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> Ladrón] ----------------------------- 2) En Reserve de Eli puntuación: 4.826942606027416 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Eli] ----------------------------- 3) En Reserve de Vida puntuación: 4.826942606027416 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Vida] ----------------------------- 4) Negro Reserve puntuación: 4.826942606027416 Hit Ubicaciones: campo:título plazo:Libro fragmento:[Negro <marca>Reserve</marca>] ----------------------------- 5) En Selva Reserve puntuación: 4.826942606027416 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca>] ----------------------------- 6) En Selva Reserve 2 puntuación: 3.9411821308689627 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca> 2] ----------------------------- |
Por defecto, los resultados no distinguen entre mayúsculas y minúsculas, pero puede cambiar fácilmente este comportamiento creando nuevos índices con diferentes analizadores.
Ahora, añadamos una capacidad de coincidencia difusa a nuestra consulta estableciendo la difusidad en 1 (distancia Levenshtein 1), lo que significa que "Libro" y "mira" tendrá la misma relevancia.
|
1 2 3 4 5 6 |
Cadena indexName = "movies_index"; MatchQuery consulta = BúsquedaQuery.match("libro").campo("título).desenfoque(1); SearchQueryResult resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( nuevo BúsquedaQuery(indexName, consulta).destacar().límite(6)); imprimirResultados(películas); |
Y aquí está el resultado de la búsqueda 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 puntuación: 1.1012248063242538 Hit Ubicaciones: campo:título plazo:gancho fragmento:[<marca>Gancho</marca>] ----------------------------- 2) Aquí Viene el Boom puntuación: 0.7786835148361213 Hit Ubicaciones: campo:título plazo:pluma fragmento:[Aquí Viene el <marca>Boom</marca>] ----------------------------- 3) Mira Quiéntambién habla puntuación: 0.7047266634351538 Localizaciones de impacto: campo:título término:mirada fragmento:[Mira Quién's Hablar Demasiado] ----------------------------- 4) Mira QuiénHablar puntuación: 0.7047266634351538 Localizaciones de impacto: campo:título término:mirada fragmento:[Mira Quién's Hablar] ----------------------------- 5) En Reserve Ladrón puntuación: 0.5228811753737184 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> Ladrón] ----------------------------- 6) En Reserve de Eli puntuación: 0.5228811753737184 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Eli] ----------------------------- |
Ahora, la película llamada "Gancho" es el primer resultado de la búsqueda, que puede no ser exactamente lo que el usuario espera en una búsqueda de "Reserve".
Cómo minimizar los falsos positivos en las búsquedas difusas
En un mundo ideal, los usuarios nunca cometerían errores tipográficos mientras buscan algo. Sin embargo, ese no es el mundo en el que vivimos, y si quieres que tus usuarios tengan una experiencia agradable, tienes que manejar al menos una distancia de edición de 1. Por lo tanto, la verdadera pregunta es: ¿Cómo podemos hacer coincidir cadenas difusas minimizando la pérdida de relevancia?
Podemos aprovechar una característica de la mayoría de los marcos de los motores de búsqueda: Una coincidencia con una distancia de edición menor suele tener una puntuación más alta. Esa característica nos permite combinar en una sola esas dos consultas con distintos niveles de imprecisión:
|
1 2 3 4 5 6 7 8 |
Cadena indexName = "movies_index"; Cadena palabra = "Libro"; MatchQuery titleFuzzy = BúsquedaQuery.match(palabra).desenfoque(1).campo("título); MatchQuery titleSimple = BúsquedaQuery.match(palabra).campo("título); DisjunctionQuery ftsQuery = BúsquedaQuery.disyuntos(titleSimple, titleFuzzy); SearchQueryResult resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( nuevo BúsquedaQuery(indexName, ftsQuery).destacar().límite(20)); imprimirResultados(resultado); |
He aquí el resultado de la consulta difusa anterior:
|
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) En Reserve Ladrón puntuación: 2.398890092610849 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> Ladrón] ----------------------------- campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> Ladrón] ----------------------------- 2) En Reserve de Eli puntuación: 2.398890092610849 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Eli] ----------------------------- campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Eli] ----------------------------- 3) En Reserve de Vida puntuación: 2.398890092610849 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Vida] ----------------------------- campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Vida] ----------------------------- 4) Negro Reserve puntuación: 2.398890092610849 Hit Ubicaciones: campo:título plazo:Libro fragmento:[Negro <marca>Reserve</marca>] ----------------------------- campo:título plazo:Libro fragmento:[Negro <marca>Reserve</marca>] ----------------------------- 5) En Selva Reserve puntuación: 2.398890092610849 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca>] ----------------------------- campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca>] ----------------------------- 6) En Selva Reserve 2 puntuación: 1.958685557004688 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca> 2] ----------------------------- campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca> 2] ----------------------------- 7) Nacional Tesoro: Reserve de Secretos puntuación: 1.6962714808368062 Hit Ubicaciones: campo:título plazo:Libro fragmento:[Nacional Tesoro: <marca>Reserve</marca> de Secretos] ----------------------------- campo:título plazo:Libro fragmento:[Nacional Tesoro: <marca>Reserve</marca> de Secretos] ----------------------------- 8) Americana Pastel Presenta: En Reserve de Amor puntuación: 1.517191317611584 Hit Ubicaciones: campo:título plazo:Libro fragmento:[Americana Pastel Presenta: En <marca>Reserve</marca> de Amor] ----------------------------- campo:título plazo:Libro fragmento:[Americana Pastel Presenta: En <marca>Reserve</marca> de Amor] ----------------------------- 9) Gancho puntuación: 0.5052232518679681 Hit Ubicaciones: campo:título plazo:gancho fragmento:[<marca>Gancho</marca>] ----------------------------- 10) Aquí Viene el Boom puntuación: 0.357246781294941 Hit Ubicaciones: campo:título plazo:pluma fragmento:[Aquí Viene el <marca>Boom</marca>] ----------------------------- 11) Mira Quiéntambién habla puntuación: 0.32331663301992025 Localizaciones de impacto: campo:título término:mirada fragmento:[Mira Quién's Hablar Demasiado] ----------------------------- 12) Mira QuiénHablar puntuación: 0.32331663301992025 Localizaciones de impacto: campo:título término:mirada fragmento:[Mira Quién's Hablar] ----------------------------- |
Como puede ver, este resultado se acerca mucho más a lo que el usuario podría esperar. Tenga en cuenta que ahora estamos utilizando una clase llamada DisjunctionQuery, las disyunciones son un equivalente a la clase "O"en SQL.
¿Qué más podríamos mejorar para reducir el efecto secundario negativo de un algoritmo de emparejamiento difuso? Analicemos de nuevo nuestro problema para saber si necesita más mejoras:
Ya sabemos que las búsquedas difusas pueden producir algunos resultados inesperados (por ejemplo, Book -> Look, Hook). Sin embargo, una búsqueda de un solo término suele ser una consulta terrible, ya que apenas nos da una pista de lo que el usuario intenta conseguir exactamente.
Ni siquiera Google, que cuenta con uno de los algoritmos de búsqueda difusa más desarrollados que existen, sabe exactamente lo que busco cuando busco "tabla":

Entonces, ¿cuál es la longitud media de las palabras clave en una consulta de búsqueda? Para responder a esta pregunta, mostraré un gráfico de Presentación de Rand Fishkin en 2016. (Es uno de los gurús más famosos del mundo SEO)

Según el gráfico anterior, ~80% de las consultas de búsqueda tienen 2 o más palabras clave, así que vamos a intentar buscar la película "Black Book" utilizando fuzziness 1:
|
1 2 3 4 5 6 |
Cadena indexName = "movies_index"; MatchQuery consulta = BúsquedaQuery.match("Libro Negro").campo("título).desenfoque(1); SearchQueryResult resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( nuevo BúsquedaQuery(indexName, consulta).destacar().límite(12)); imprimirResultados(películas); |
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) Negro Reserve puntuación: 0.6946442424065404 Hit Ubicaciones: campo:título plazo:Libro fragmento:[<marca>Negro</marca> <marca>Reserve</marca>] ----------------------------- campo:título plazo:negro fragmento:[<marca>Negro</marca> <marca>Reserve</marca>] ----------------------------- 2) Gancho puntuación: 0.40139742528039857 Hit Ubicaciones: campo:título plazo:gancho fragmento:[<marca>Gancho</marca>] ----------------------------- 3) Ataque el Bloque puntuación: 0.2838308365090324 Hit Ubicaciones: campo:título plazo:bloque fragmento:[Ataque el <marca>Bloque</marca>] ----------------------------- 4) Aquí Viene el Boom puntuación: 0.2838308365090324 Hit Ubicaciones: campo:título plazo:pluma fragmento:[Aquí Viene el <marca>Boom</marca>] ----------------------------- 5) Mira Quiéntambién habla puntuación: 0.25687349813115684 Localizaciones de impacto: campo:título término:mirada fragmento:[Mira Quién's Hablar Demasiado] ----------------------------- 6) Mira QuiénHablar puntuación: 0.25687349813115684 Localizaciones de impacto: campo:título término:mirada fragmento:[Mira Quién's Hablar] ----------------------------- 7) Grosse Punta En blanco puntuación: 0.2317469073782136 Hit Ubicaciones: campo:título plazo:en blanco fragmento:[Grosse Punta <marca>En blanco</marca>] ----------------------------- 8) En Reserve Ladrón puntuación: 0.19059065534780495 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> Ladrón] ----------------------------- 9) En Reserve de Eli puntuación: 0.19059065534780495 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Eli] ----------------------------- 10) En Reserve de Vida puntuación: 0.19059065534780495 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de Vida] ----------------------------- 11) En Selva Reserve puntuación: 0.19059065534780495 Hit Ubicaciones: campo:título plazo:Libro fragmento:[En Selva <marca>Reserve</marca>] ----------------------------- 12) Volver a el Futuro puntuación: 0.17061000968368 Hit Ubicaciones: campo:título plazo:volver fragmento:[<marca>Volver</marca> a el Futuro] ----------------------------- |
No está mal. Obtuvimos la película que buscábamos como primer resultado. Sin embargo, una consulta disyuntiva nos daría un conjunto de resultados mejor.
Pero aún así, parece que tenemos una nueva propiedad agradable aquí; el efecto secundario de la concordancia difusa disminuye ligeramente a medida que aumenta el número de palabras clave. Una búsqueda de "Libro negro" con desenfoque 1 todavía puede traer resultados como la mirada hacia atrás o la falta de cocinero (algunas combinaciones con la distancia de edición 1), pero estos son poco probable que sean títulos de películas reales.
Una búsqueda de "libro eli" con borrosidad 2 aún lo traería como tercer resultado:
|
1 2 3 4 5 6 |
Cadena indexName = "movies_index"; MatchQuery consulta = BúsquedaQuery.match("libro eli").campo("título).desenfoque(2); SearchQueryResult resultado = movieRepository.getCouchbaseOperations().getCouchbaseBucket().consulta( nuevo BúsquedaQuery(indexName, consulta).destacar().límite(12)); imprimirResultados(películas); |
|
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 Madera puntuación: 0.030723793900031805 Hit Ubicaciones: campo:título plazo:madera fragmento:[<marca>Ed</marca> <marca>Madera</marca>] ----------------------------- campo:título plazo:ed fragmento:[<marca>Ed</marca> <marca>Madera</marca>] ----------------------------- 2) Hombre de Tai Chi puntuación: 0.0271042761982626 Hit Ubicaciones: campo:título plazo:chi fragmento:[Hombre de <marca>Tai</marca> <marca>Chi</marca>] ----------------------------- campo:título plazo:tai fragmento:[Hombre de <marca>Tai</marca> <marca>Chi</marca>] ----------------------------- 3) En Reserve de Eli puntuación: 0.02608335441670756 Hit Ubicaciones: campo:título plazo:eli fragmento:[En <marca>Reserve</marca> de <marca>Eli</marca>] ----------------------------- campo:título plazo:Libro fragmento:[En <marca>Reserve</marca> de <marca>Eli</marca>] ----------------------------- 4) En Bien Mentira puntuación: 0.02439822770591834 Hit Ubicaciones: campo:título plazo:mentir fragmento:[En <marca>Bien</marca> <marca>Mentira</marca>] ----------------------------- campo:título plazo:bien fragmento:[En <marca>Bien</marca> <marca>Mentira</marca>] ----------------------------- |
Sin embargo, como la palabra inglesa media tiene 5 letras, me gustaría NO recomiendan utilizar una distancia de edición superior a 2 a menos que el usuario busque palabras largas y fáciles de escribir mal, como "Schwarzenegger" por ejemplo (al menos para los no alemanes o no austriacos).
Conclusión
En este artículo, hablamos de la concordancia difusa y de cómo superar su principal efecto secundario sin estropear su relevancia. Sin embargo, la concordancia difusa es sólo una de las muchas características que debe aprovechar al implementar una búsqueda relevante y fácil de usar. En esta serie hablaremos de algunas de ellas: N-Gramas, Stopwords, Steeming, Shingle, Elisión. Etc.
Consulte también el Parte 1 y Parte 2 de esta serie.
Mientras tanto, si tienes alguna pregunta, envíame un tweet a @deniswsrosa.
¿Cómo se relaciona la puntuación obtenida del índice con un porcentaje de coincidencia? ¿Cómo construir una consulta para la que la puntuación de relevancia de los resultados represente una relevancia > 50%, por ejemplo?
Si quieres saber más sobre cómo se puntúan los documentos, tanto lucene como bleve tienen el método "explain". En couchbase puede establecer explain(true) para ver exactamente cómo se calcula la puntuación.Los resultados se ordenan por defecto por puntuación, por lo que los más relevantes deberían ser los primeros de la lista. ¿Qué quieres conseguir exactamente?