如果你可以向 MySQL 添加一个用户函数,那么你可以使用 Levenshtein 的距离。代码见this other question。
然后你可以查询WHERE LEVENSHTEIN(description, 'iphone 5') <= 2例如。你会发现“iphone 5S”和“ipohne 5”,这可能是一个加分项。
否则,特定案例很容易(例如,REGEX 'iphone.*' 或类似的),但一般案例将是一场噩梦。
使用 Levenshtein 作为最后的手段
“文本距离”计算的成本是惊人的。因此,在使用它之前,请尽一切可能减少所需的计算次数。
例如,基本的 Levenshtein 假定插入和删除每个花费 1。这意味着“好”和“优秀”的最小距离为 5,只是因为 1 长了五个字符。 这个信息是两个LENGTHs 的差异,几乎没有成本。
如果可以根据第一个字母对行进行分区,则可以计算两个长度相等的余数之间的距离,如果第一个字母相等则加 0,如果不相等则加 1(如果余数不 长度相等,您可能会引入错误:“SLAUGHTER”和“LAUGHTER”相隔 1 次插入,但比较“LAUGHTER”和“AUGHTER”也会产生 1,导致得分为 1+1=2 1+0=1。相反,'LAUGHTER' 和 'DAUGHTER' 的长度相同,第一个字母会产生 1 加上 'AUGHTER' 和 'AUGHTER' 之间的差值,即 0)。
这种方法(有时称为 Jordan 集合缩减)可能会将您的单词列表从 10,000 个单词分成 50 个桶,每个桶包含 200 个词,只比较基数为 4 的桶子集;这意味着大约 50*(4*200)^2 = 3200 万次比较,而不是 10000^2 = 1 亿次,减少了 68%。
(实际上,假设字长从 5 到 15 并且所有字母的表示相同,您的列表将分为大约 250 个桶,每个桶 40 个词;250*(440)^2 是 640 万。如果您希望最大 两个 字符差异,则为 250(2*40)^2 = 160 万。节省的时间可能非常值得为单词拼凑成本)。
一旦您减少了需要进行实际比较的行数,然后对剩余的行调用 Levenshtein。
表演
此版本已修改为接受第三个参数 MAXCOST。当达到完整 Levenshtein 子循环的最大成本时,搜索功能将简单地中止。这减少了Levenshtein 距离不合理 的情况,而牺牲了距离 合理的情况。
所以如果你想要距离小于 3 的字符串,你可以使用WHERE levenshtein(string1, string2, 3) < 3。 最小单循环距离为 3 或以上的所有字符串现在将返回 3,并被排除在外。这使得函数失败安全,即它永远不会过度报告距离,并且只有在失败的情况下才会少报告距离(因此,如果两个字符串的距离为 1,则函数将永远不会返回大于 1。但如果他们的距离为 14 并且限制设置为 10,则可能会低估 12 而不是 14 的距离。
这意味着如果您的大部分查询当前返回小距离,此功能对您来说将不方便。如果您的近距离比赛很少而远距离比赛很多,那么这将起作用。
测试,使用短距离。请记住,此处未修改的 Levenshtein 函数将在我的机器上在 1.49 毫秒内返回。
SELECT
BENCHMARK(1000, levenshtein('mogilifski', 'mogiliski', 999)) AS _,
levenshtein('mogilifski', 'mogiliski', 999) AS result;
1 row in set (1.656 sec)
因此,距离 1 将产生净损失 - 它慢 12%。降低限制不会改变时间,因为函数永远不会超过 2。
现在让我们尝试使用非常不同的字符串:“mogiliski”和“fridrich”。在这种情况下,实际距离为 9,将限制从 999 降低到 3 会将执行时间从 1.47 减少到 0.71 毫秒 - 节省了 50%。
更极端的情况要好得多:“Darmok and Jalad at Tanagra”和“Kiteo,他的眼睛睁开”是更长的字符串,距离为 23。执行时间通常为 9.672 ms(未修改版本为 9.05) .将限制降低到 5 会将执行时间降低到 2.89 毫秒。在 3 处进一步降低,因为 很明显 距离永远不会那么小,因此在第一个子循环之前立即终止,产生 0.03 毫秒。
这是original function 的修改代码。在定义这个新函数之前,您必须删除旧函数,除非您更改名称。
DELIMITER $$
DROP FUNCTION IF EXISTS LEVENSHTEIN $$
CREATE FUNCTION LEVENSHTEIN(s1 VARCHAR(255) CHARACTER SET utf8, s2 VARCHAR(255) CHARACTER SET UTF8, maxcost INT)
RETURNS INT
DETERMINISTIC
BEGIN
DECLARE s1_len, s2_len, i, j, c, c_temp, cost, mincost INT;
DECLARE s1_char CHAR CHARACTER SET utf8;
-- max strlen=255 for this function
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);
END IF;
IF (ABS(s1_len - s2_len) > maxcost) THEN
RETURN maxcost;
END IF;
WHILE (j <= s2_len) DO
SET cv1 = CONCAT(cv1, CHAR(j)),
j = j + 1;
END WHILE;
WHILE (i <= s1_len) DO
SET s1_char = SUBSTRING(s1, i, 1),
c = i,
cv0 = CHAR(i),
j = 1;
SET mincost = 255;
WHILE (j <= s2_len) DO
SET c = c + 1,
cost = IF(s1_char = SUBSTRING(s2, j, 1), 0, 1);
SET c_temp = ORD(SUBSTRING(cv1, j, 1)) + cost;
IF (c > c_temp) THEN
SET c = c_temp;
END IF;
SET c_temp = ORD(SUBSTRING(cv1, j+1, 1)) + 1;
IF (c > c_temp) THEN
SET c = c_temp;
END IF;
SET cv0 = CONCAT(cv0, CHAR(c)),
j = j + 1;
IF (c < mincost) THEN
SET mincost = c;
END IF;
END WHILE;
IF (mincost > maxcost) THEN
RETURN (mincost);
END IF;
SET cv1 = cv0,
i = i + 1;
END WHILE;
RETURN (c);
END $$
DELIMITER ;