web-dev-qa-db-fra.com

Implémentation de la distance de Levenshtein pour la recherche mysql / floue?

Je voudrais être en mesure de rechercher une table comme suit pour smith comme obtenir tout ce qu'il y a dans 1 variance.

Les données:

 O'Brien 
 Smithe 
 Dolan 
 Smuth 
 Wong 
 Smoth 
 Gunther 
 Smiht 

J'ai étudié l'utilisation de la distance Levenshtein. Quelqu'un sait-il comment implémenter cela avec?

44
Andrew Clark

Afin de rechercher efficacement à l'aide de la distance de levenshtein, vous avez besoin d'un index efficace et spécialisé, tel qu'un bk-tree . Malheureusement, aucun système de base de données que je connaisse, y compris MySQL, n'implémente les index bk-tree. C'est encore plus compliqué si vous recherchez une recherche en texte intégral, au lieu d'un seul terme par ligne. D'un autre côté, je ne vois pas comment vous pourriez faire une indexation de texte intégral d'une manière qui permette une recherche basée sur la distance du levenshtein.

11
Nick Johnson

Il existe une implémentation mysql UDF de la fonction Distance de Levenshtein

https://github.com/jmcejuela/Levenshtein-MySQL-UDF

Il est implémenté en C et a de meilleures performances que la "requête de distance MySQL Levenshtein" mentionnée par schnaader

7
Hongzheng

Une implémentation pour la distance damerau-levenshtein peut être trouvée ici: algorithme Damerau-Levenshtein: Levenshtein avec transpositions L'amélioration par rapport à la distance Levenshtein pure est que l'échange de caractères est considéré. Je l'ai trouvé dans les commentaires du lien de schnaader, merci!

5
Ponk

La fonction donnée pour levenshtein <= 1 ci-dessus n'est pas correcte - elle donne des résultats incorrects pour, par exemple, "lit" et "enchère".

J'ai modifié la "requête de distance MySQL Levenshtein" donnée ci-dessus, dans la première réponse, pour accepter une "limite" qui l'accélérera un peu. Fondamentalement, si vous ne vous souciez que de Levenshtein <= 1, définissez la limite sur "2" et la fonction renverra la distance exacte de levenshtein si elle est 0 ou 1; ou 2 si la distance exacte du levenshtein est de 2 ou plus.

Ce mod le rend 15% à 50% plus rapide - plus votre mot de recherche est long, plus l'avantage est grand (car l'algorithme peut être libéré plus tôt). Par exemple, lors d'une recherche contre 200 000 mots pour trouver toutes les correspondances à la distance 1 du mot "rire", l'original prend 3 min 47 sec sur mon ordinateur portable, contre 1:39 pour la version "limite". Bien sûr, ils sont tous deux trop lents pour une utilisation en temps réel.

Code:

DELIMITER $$
CREATE FUNCTION levenshtein_limit_n( s1 VARCHAR(255), s2 VARCHAR(255), n INT) 
  RETURNS INT 
  DETERMINISTIC 
  BEGIN 
    DECLARE s1_len, s2_len, i, j, c, c_temp, cost, c_min INT; 
    DECLARE s1_char CHAR; 
    -- max strlen=255 
    DECLARE cv0, cv1 VARBINARY(256); 
    SET s1_len = CHAR_LENGTH(s1), s2_len = CHAR_LENGTH(s2), cv1 = 0x00, j = 1, i = 1, c = 0, c_min = 0; 
    IF s1 = s2 THEN 
      RETURN 0; 
    ELSEIF s1_len = 0 THEN 
      RETURN s2_len; 
    ELSEIF s2_len = 0 THEN 
      RETURN s1_len; 
    ELSE 
      WHILE j <= s2_len DO 
        SET cv1 = CONCAT(cv1, UNHEX(HEX(j))), j = j + 1; 
      END WHILE; 
      WHILE i <= s1_len and c_min < n DO -- if actual levenshtein dist >= limit, don't bother computing it
        SET s1_char = SUBSTRING(s1, i, 1), c = i, c_min = i, cv0 = UNHEX(HEX(i)), j = 1; 
        WHILE j <= s2_len DO 
          SET c = c + 1; 
          IF s1_char = SUBSTRING(s2, j, 1) THEN  
            SET cost = 0; ELSE SET cost = 1; 
          END IF; 
          SET c_temp = CONV(HEX(SUBSTRING(cv1, j, 1)), 16, 10) + cost; 
          IF c > c_temp THEN SET c = c_temp; END IF; 
            SET c_temp = CONV(HEX(SUBSTRING(cv1, j+1, 1)), 16, 10) + 1; 
            IF c > c_temp THEN  
              SET c = c_temp;  
            END IF; 
            SET cv0 = CONCAT(cv0, UNHEX(HEX(c))), j = j + 1;
            IF c < c_min THEN
              SET c_min = c;
            END IF; 
        END WHILE; 
        SET cv1 = cv0, i = i + 1; 
      END WHILE; 
    END IF;
    IF i <= s1_len THEN -- we didn't finish, limit exceeded    
      SET c = c_min; -- actual distance is >= c_min (i.e., the smallest value in the last computed row of the matrix) 
    END IF;
    RETURN c;
  END$$
4
stopthe

vous pouvez utiliser cette fonction

 
 CRÉER UNE FONCTION `levenshtein` (texte s1, texte s2) RETOURS int (11) 
 DÉTERMINISTIQUE 
 COMMENCER 
 DÉCLARER s1_len, s2_len, i , j, c, c_temp, coût INT; 
 DECLARE s1_char CHAR; 
 DECLARE cv0, cv1 text; 
 SET s1_len = CHAR_LENGTH (s1), s2_len = CHAR_LENGTH (s2), cv1 = 0x00, j = 1, i = 1, c = 0; 
 SI s1 = s2 ALORS 
 RETOUR 0; 
 ELSEIF s1_len = 0 ALORS 
 RETOUR s2_len; 
 ELSEIF s2_len = 0 ALORS 
 RETOUR s1_len; 
 AUTRE 
 PENDANT j <= s2_len FAIRE 
 SET cv1 = CONCAT (cv1, UNHEX (HEX (j))), j = j + 1; 
 FIN EN COURS; 
 TANDIS QUE i <= s1_len DO 
 SET s1_char = SUBSTRING (s1, i, 1), c = i, cv0 = UNHEX (HEX (i)), j = 1; 
 PENDANT j <= s2_len FAIRE 
 SET c = c + 1; 
 SI s1_char = SUBSTRING (s2, j, 1) ALORS 
 SET cost = 0; ELSE SET cost = 1; 
          FIN SI; 
 SET c_temp = CONV (HEX (SUBSTRING (cv1, j, 1)), 16, 10) + cost; 
 SI c> c_temp ALORS SET c = c_temp; FIN SI; 
 SET c_temp = CONV (HEX (SUBSTRING (cv1, j + 1, 1)), 16, 10) + 1; 
 SI c> c_temp ALORS 
 SET c = c_temp; 
            FIN SI; 
 SET cv0 = CONCAT (cv0, UNHEX (HEX (c))), j = j + 1; 
 FIN EN COURS; 
 SET cv1 = cv0, i = i + 1; 
 FIN EN COURS; 
    FIN SI; 
 RETOUR c; 
  FIN

et pour l'obtenir comme XX% utilisez cette fonction

 
 CRÉER UNE FONCTION `levenshtein_ratio` (texte s1, texte s2) RETOURS int (11) 
 DÉTERMINISTIQUE 
 COMMENCER 
 DÉCLARER s1_len, s2_len, max_len INT; 
 SET s1_len = LONGUEUR (s1), s2_len = LONGUEUR (s2); 
 SI s1_len> s2_len ALORS 
 SET max_len = s1_len; 
 AUTRE 
 SET max_len = s2_len; 
    FIN SI; 
 TOUR DE RETOUR ((1 - LEVENSHTEIN (s1, s2)/max_len) * 100); 
  FIN
3
Alaa

Si vous voulez seulement savoir si la distance de levenshtein est au plus 1, vous pouvez utiliser la fonction MySQL suivante.

CREATE FUNCTION `lv_leq_1` (
`s1` VARCHAR( 255 ) ,
`s2` VARCHAR( 255 )
) RETURNS TINYINT( 1 ) DETERMINISTIC
BEGIN
    DECLARE s1_len, s2_len, i INT;
    SET s1_len = CHAR_LENGTH(s1), s2_len = CHAR_LENGTH(s2), i = 1;
    IF s1 = s2 THEN
        RETURN TRUE;
    ELSEIF ABS(s1_len - s2_len) > 1 THEN
        RETURN FALSE;
    ELSE
        WHILE SUBSTRING(s1,s1_len - i,1) = SUBSTRING(s2,s2_len - i,1) DO
            SET i = i + 1;
        END WHILE;
        RETURN SUBSTRING(s1,1,s1_len-i) = SUBSTRING(s2,1,s2_len-i) OR SUBSTRING(s1,1,s1_len-i) = SUBSTRING(s2,1,s2_len-i+1) OR SUBSTRING(s1,1,s1_len-i+1) = SUBSTRING(s2,1,s2_len-i);
    END IF;
END

Il s'agit essentiellement d'une étape unique dans la description récursive de la distance du levenshtein. La fonction retourne 1, si la distance est au plus 1, sinon elle retourne 0.

Étant donné que cette fonction ne calcule pas complètement la distance de levenshtein, elle est beaucoup plus rapide.

Vous pouvez également modifier cette fonction de telle sorte qu'elle renvoie true si la distance de levenshtein est au plus 2 ou 3, en l'appelant automatiquement récursivement. Si MySQL ne prend pas en charge les appels récursifs, vous pouvez copier deux fois les versions légèrement modifiées de cette fonction et les appeler à la place. Mais vous ne devez pas utiliser la fonction récursive pour calculer la distance de levenshtein exacte.

2
AbcAeffchen

Je mets en place une recherche basée sur Levenshtein ou Damerau-Levenshtein (probablement ce dernier) pour plusieurs recherches sur un texte indexé, basée sur un article de Gonzalo Navarro et Ricardo Baeza-yates: texte du lien

Après avoir construit un tableau de suffixes ( voir wikipedia ), si vous êtes intéressé par une chaîne avec au plus k disparités avec la chaîne de recherche, divisez la chaîne de recherche en k + 1 morceaux; l'un au moins doit être intact. Trouvez les sous-chaînes par recherche binaire sur le tableau des suffixes, puis appliquez la fonction de distance au patch autour de chaque pièce correspondante.

2
Hugh

Sur la base de réponse de Chella et de Ryan Ginstrom article , une recherche floue pourrait être implémentée comme suit:

DELIMITER $$
CREATE FUNCTION fuzzy_substring( s1 VARCHAR(255), s2 VARCHAR(255) )
    RETURNS INT
    DETERMINISTIC
BEGIN
    DECLARE s1_len, s2_len, i, j, c, c_temp, cost INT;
    DECLARE s1_char CHAR;
    -- max strlen=255
    DECLARE cv0, cv1 VARBINARY(256);
    SET s1_len = CHAR_LENGTH(s1), s2_len = CHAR_LENGTH(s2), cv1 = 0x00, j = 1, i = 1, c = 0;
    IF s1 = s2 THEN
        RETURN 0;
    ELSEIF s1_len = 0 THEN
        RETURN s2_len;
    ELSEIF s2_len = 0 THEN
        RETURN s1_len;
    ELSE
        WHILE j <= s2_len DO
            SET cv1 = CONCAT(cv1, UNHEX(HEX(0))), j = j + 1;
        END WHILE;
        WHILE i <= s1_len DO
            SET s1_char = SUBSTRING(s1, i, 1), c = i, cv0 = UNHEX(HEX(i)), j = 1;
            WHILE j <= s2_len DO
                SET c = c + 1;
                IF s1_char = SUBSTRING(s2, j, 1) THEN
                    SET cost = 0; ELSE SET cost = 1;
                END IF;
                SET c_temp = CONV(HEX(SUBSTRING(cv1, j, 1)), 16, 10) + cost;
                IF c > c_temp THEN SET c = c_temp; END IF;
                    SET c_temp = CONV(HEX(SUBSTRING(cv1, j+1, 1)), 16, 10) + 1;
                IF c > c_temp THEN
                    SET c = c_temp;
                END IF;
                SET cv0 = CONCAT(cv0, UNHEX(HEX(c))), j = j + 1;
            END WHILE;
            SET cv1 = cv0, i = i + 1;
        END WHILE;
    END IF;
    SET j = 1;
    WHILE j <= s2_len DO
        SET c_temp = CONV(HEX(SUBSTRING(cv1, j, 1)), 16, 10);
        IF c > c_temp THEN
            SET c = c_temp;
        END IF;
        SET j = j + 1;
    END WHILE;
    RETURN c;
END$$
DELIMITER ;
0
D. Savina

J'avais un cas spécialisé de recherche à k-distance et après avoir installé l'UDF Damerau-Levenshtein dans MySQL, j'ai trouvé que la requête prenait trop de temps. J'ai trouvé la solution suivante:

  • J'ai un espace de recherche très restrictif (chaîne de 9 caractères limitée aux valeurs numériques).

Créez une nouvelle table (ou ajoutez des colonnes à votre table cible) avec des colonnes pour chaque position de caractère dans votre champ cible. c'est à dire. Mon VARCHAR (9) a fini par 9 colonnes TINYINT + 1 colonne Id qui correspond à ma table principale (ajoutez des index pour chaque colonne). J'ai ajouté des déclencheurs pour m'assurer que ces nouvelles colonnes sont toujours mises à jour lorsque ma table principale est mise à jour.

Pour effectuer une requête à distance k, utilisez le prédicat suivant:

(Colonne1 = s [0]) + (Colonne2 = s [1]) + (Colonne3 = s [2]) + (Colonne4 = s [3]) + ...> = m

où s est votre chaîne de recherche et m est le nombre requis de caractères correspondants (ou m = 9 - d dans mon cas où d est la distance maximale que je veux renvoyer).

Après avoir testé, j'ai constaté qu'une requête de plus d'un million de lignes qui prenait en moyenne 4,6 secondes renvoyait des identifiants correspondants en moins d'une seconde. Une deuxième requête pour renvoyer les données des lignes correspondantes dans ma table principale a également pris moins d'une seconde. (La combinaison de ces deux requêtes en tant que sous-requête ou jointure a entraîné des temps d'exécution considérablement plus longs et je ne sais pas pourquoi.)

Bien que ce ne soit pas Damerau-Levenshtein (ne tient pas compte de la substitution), cela suffit à mes fins.

Bien que cette solution ne soit probablement pas adaptée à un espace de recherche plus grand (longueur), elle a très bien fonctionné pour ce cas restrictif.

0
greg