我希望能够在如下表中搜索 smith 以获得它在 1 个方差内的所有内容。
数据:
奥布莱恩 史密斯 多兰 斯穆斯 黄 斯莫斯 冈瑟 史密斯
我已经研究过使用 Levenshtein distance 有人知道如何用它来实现吗?
我希望能够在如下表中搜索 smith 以获得它在 1 个方差内的所有内容。
数据:
奥布莱恩 史密斯 多兰 斯穆斯 黄 斯莫斯 冈瑟 史密斯
我已经研究过使用 Levenshtein distance 有人知道如何用它来实现吗?
为了使用 levenshtein 距离进行高效搜索,您需要一个高效的专用索引,例如bk-tree。不幸的是,我所知道的任何数据库系统,包括 MySQL,都没有实现 bk-tree 索引。如果您正在寻找全文搜索,而不是每行只有一个词,这会更加复杂。顺便说一句,我想不出任何方式可以以允许基于 levenshtein 距离进行搜索的方式进行全文索引。
Levenshtein Distance函数有一个mysql UDF实现
https://github.com/jmcejuela/Levenshtein-MySQL-UDF
它是用C实现的,比schnaader提到的“MySQL Levenshtein距离查询”有更好的性能
可以在此处找到 damerau-levenshtein 距离的实现: Damerau-Levenshtein algorithm: Levenshtein with transpositions 对纯 Levenshtein 距离的改进是考虑了字符的交换。我在 schnaader 链接的评论中找到了它,谢谢!
上面为 levenshtein <= 1 给出的函数是不正确的——它给出了不正确的结果,例如“床”和“出价”。
我在第一个答案中修改了上面给出的“MySQL Levenshtein 距离查询”,以接受一个“限制”,这将加快一点速度。基本上,如果您只关心 Levenshtein <= 1,请将限制设置为“2”,如果它是 0 或 1,该函数将返回确切的 Levenshtein 距离;如果确切的 levenshtein 距离为 2 或更大,则为 2。
这个 mod 让它快了 15% 到 50% - 你的搜索词越长,优势就越大(因为算法可以更早地保释。)例如,在搜索 200,000 个词以找到距离 1 内的所有匹配词“咯咯”,原版在我的笔记本电脑上需要 3 分 47 秒,而“限制”版本需要 1 分 39 秒。当然,对于任何实时使用来说,这些都太慢了。
代码:
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$$
你可以使用这个功能
创建函数`levenshtein`(s1文本,s2文本)返回int(11) 确定性 开始 声明 s1_len、s2_len、i、j、c、c_temp、成本 INT; 声明 s1_char CHAR; 声明 cv0、cv1 文本; SET s1_len = CHAR_LENGTH(s1), s2_len = CHAR_LENGTH(s2), cv1 = 0x00, j = 1, i = 1, c = 0; 如果 s1 = s2 那么 返回 0; ELSEIF s1_len = 0 那么 返回 s2_len; ELSEIF s2_len = 0 那么 返回 s1_len; 别的 当 j <= s2_len 做 SET cv1 = CONCAT(cv1, UNHEX(HEX(j))), j = j + 1; 结束; 当我 <= s1_len 做 SET s1_char = SUBSTRING(s1, i, 1), c = i, cv0 = UNHEX(HEX(i)), j = 1; 当 j <= s2_len 做 设置 c = c + 1; 如果 s1_char = SUBSTRING(s2, j, 1) 那么 设置成本 = 0;其他设置成本 = 1; 万一; SET c_temp = CONV(HEX(SUBSTRING(cv1, j, 1)), 16, 10) + 成本; 如果 c > c_temp 那么设置 c = c_temp; 万一; SET c_temp = CONV(HEX(SUBSTRING(cv1, j+1, 1)), 16, 10) + 1; 如果 c > c_temp 那么 设置 c = c_temp; 万一; SET cv0 = CONCAT(cv0, UNHEX(HEX(c))), j = j + 1; 结束; 设置 cv1 = cv0, i = i + 1; 结束; 万一; 返回 c; 结尾
并将其作为 XX% 使用此功能
创建函数`levenshtein_ratio`(s1文本,s2文本)返回int(11) 确定性 开始 声明 s1_len、s2_len、max_len INT; 设置 s1_len = 长度(s1),s2_len = 长度(s2); 如果 s1_len > s2_len 那么 设置 max_len = s1_len; 别的 设置 max_len = s2_len; 万一; 返回回合((1 - LEVENSHTEIN(s1,s2)/ max_len)* 100); 结尾
如果只想知道 levenshtein-distance 是否最多为 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 距离,因此它要快得多。
您还可以修改此函数,使其true
在 levenshtein-distance 最多为 2 或 3 时返回,方法是递归地调用它。如果 MySQL 不支持递归调用,您可以将该函数的稍微修改的版本复制两次并调用它们。但是您不应该使用递归函数来计算精确的 levenshtein 距离。
根据Chella 的回答和 Ryan Ginstrom 的文章,可以这样实现模糊搜索:
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 ;
I had a specialized case of k-distance searching and after installing the Damerau-Levenshtein UDF in MySQL found that the query was taking too long. I came up with the following solution:
Create a new table (or append columns to your target table) with columns for each character position in your target field. ie. My VARCHAR(9) ended up as 9 TINYINT columns + 1 Id column that matches my main table (add indexes for each column). I added triggers to ensure that these new columns always get updated when my main table gets updated.
To perform a k-distance query use the following predicate:
(Column1=s[0]) + (Column2=s[1]) + (Column3=s[2]) + (Column4=s[3]) + ... >= m
where s is your search string and m is the required number of matching characters (or m = 9 - d in my case where d is the maximum distance I want returned).
After testing I found that a query over 1 million rows that was taking 4.6 seconds on average was returning matching ids in less than a second. A second query to return the data for the matching rows in my main table similarly took under a second. (Combining these two queries as a subquery or join resulted in significantly longer execution times and I'm not sure why.)
Though this is not Damerau-Levenshtein (doesn't account for substitution) it suffices for my purposes.
Though this solution probably doesn't scale well for a larger (length) search space it worked for this restrictive case very well.