为mysql /模糊搜索实现Levenshtein距离?

时间:2009-03-11 15:15:08

标签: mysql database algorithm search levenshtein-distance

我希望能够按照以下方式搜索一个表格,因为它可以获得1个方差范围内的所有内容。

数据:

O'Brien
Smithe
Dolan
Smuth
Wong
Smoth
Gunther
Smiht

我已经研究过使用Levenshtein距离有没有人知道如何用它实现这个?

9 个答案:

答案 0 :(得分:11)

为了使用levenshtein距离进行有效搜索,您需要一个有效的专用索引,例如bk-tree。不幸的是,我所知道的数据库系统,包括MySQL,都没有实现bk-tree索引。如果您正在寻找全文搜索,而不是每行只有一个术语,这将更加复杂。另一方面,我无法想到你可以以允许基于levenshtein距离进行搜索的方式进行全文索引。

答案 1 :(得分:7)

有一个Levenshtein距离函数的mysql UDF实现

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

它在C中实现,并且具有比schnaader提到的“MySQL Levenshtein距离查询”更好的性能

答案 2 :(得分:5)

这里可以找到damerau-levenshtein距离的实现: Damerau-Levenshtein algorithm: Levenshtein with transpositions 纯Levenshtein距离的改进是考虑字符的交换。我在schnaader链接的评论中找到了它,谢谢!

答案 3 :(得分:4)

上面给出的levenshtein< = 1的函数不正确 - 它给出了不正确的结果,例如" bed"并且"出价"。

我修改了" MySQL Levenshtein距离查询"如上所述,在第一个答案中,接受"限制"这将加快一点。基本上,如果您只关心Levenshtein< = 1,请将限制设置为" 2"如果它是0或1,函数将返回精确的levenshtein距离;如果精确的levenshtein距离为2或更大,则为2。

这个mod使它快15%到50% - 搜索词越长,优势越大(因为算法可以提前保释。)例如,搜索200,000个单词以查找距离1内的所有匹配单词" giggle,"原版在我的笔记本电脑上花了3分47秒,而在#34;限制"版。当然,这些对于任何实时使用来说都太慢了。

代码:

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 :(得分:3)

你可以使用这个功能


CREATE FUNCTION `levenshtein`( s1 text, s2 text) RETURNS int(11)
    DETERMINISTIC
BEGIN 
    DECLARE s1_len, s2_len, i, j, c, c_temp, cost 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; 
    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 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; 
    RETURN c; 
  END

并将其作为XX%使用此功能


CREATE FUNCTION `levenshtein_ratio`( s1 text, s2 text ) RETURNS int(11)
    DETERMINISTIC
BEGIN 
    DECLARE s1_len, s2_len, max_len INT; 
    SET s1_len = LENGTH(s1), s2_len = LENGTH(s2); 
    IF s1_len > s2_len THEN  
      SET max_len = s1_len;  
    ELSE  
      SET max_len = s2_len;  
    END IF; 
    RETURN ROUND((1 - LEVENSHTEIN(s1, s2) / max_len) * 100); 
  END

答案 5 :(得分:2)

根据Gonzalo Navarro和Ricardo Baeza-yates撰写的一篇论文,我根据Levenshtein或Damerau-Levenshtein(可能是后者)进行搜索,对索引文本进行多次搜索:link text

在构建后缀数组(see wikipedia)之后,如果您对与搜索字符串最多k个不匹配的字符串感兴趣,请将搜索字符串分解为k + 1个字符;其中至少有一个必须完好无损。通过后缀数组上的二进制搜索找到子串,然后将距离函数应用于每个匹配的块周围的补丁。

答案 6 :(得分:2)

如果您只想知道levenshtein距离是否最多为1,则可以使用以下MySQL函数。

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

这基本上是对levenshtein距离的递归描述中的一个步骤。 如果距离最多为1,则函数返回1,否则返回0.

由于此函数不能完全计算levenshtein距离,因此速度要快得多。

你也可以修改这个函数,使得如果levenshtein距离最多为2或3则返回true,通过递归调用它。如果MySQL不支持递归调用,您可以复制此函数的略微修改版本两次并改为调用它们。但是你不应该使用递归函数来计算精确的levenshtein距离。

答案 7 :(得分:0)

我有一个专门的k距离搜索案例,在MySQL中安装Damerau-Levenshtein UDF后发现查询耗时太长。我提出了以下解决方案:

  • 我有一个非常严格的搜索空间(9个字符串仅限于数值)。

创建一个新表(或将列附加到目标表),并为目标字段中的每个字符位置添加列。即。我的VARCHAR(9)最终为9个TINYINT列+ 1个Id列,与我的主表匹配(为每列添加索引)。我添加了触发器以确保在我的主表更新时这些新列总是会更新。

要执行k距离查询,请使用以下谓词:

(Column1 = s [0])+(Column2 = s [1])+(Column3 = s [2])+(Column4 = s [3])+ ...&gt; = m

其中s是你的搜索字符串,m是匹配字符所需的数量(或者m = 9 - d,在我的情况下,d是我想要返回的最大距离)。

经过测试,我发现超过100万行的查询平均需要4.6秒才能在不到一秒的时间内返回匹配的ID。在我的主表中返回匹配行的数据的第二个查询类似地花了不到一秒钟。 (将这两个查询组合为子查询或联接导致执行时间明显延长,我不确定原因。)

虽然这不是Damerau-Levenshtein(不考虑替代)但它足以满足我的目的。

虽然这个解决方案可能无法在较大(长度)的搜索空间中很好地扩展,但它可以很好地适用于这种限制性案例。

答案 8 :(得分:0)

基于Chella's answer和Ryan Ginstrom的article,可以实现模糊搜索:

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 ;