スミスのテーブルを次のように検索して、1分散以内にあるすべてのものを取得できるようにしたいと思います。
データ:
O'Brien Smithe Dolan Smuth Wong Smoth Gunther Smiht
レーベンシュタイン距離の使用を検討しましたが、これを実装する方法を知っている人はいますか?
レベンシュタイン距離を使用して効率的に検索するには、 bk-tree などの効率的で特殊なインデックスが必要です。残念ながら、MySQLを含め、私が知っているデータベースシステムでは、bkツリーインデックスを実装していません。行ごとに1つの用語だけではなく、全文検索を探している場合、これはさらに複雑になります。率直に言って、レベンシュタイン距離に基づいた検索を可能にする方法でフルテキストインデックスを作成できる方法は考えられません。
レーベンシュタイン距離関数のmysql UDF実装があります
https://github.com/jmcejuela/Levenshtein-MySQL-UDF
Cで実装されており、schnaaderが言及した「MySQL Levenshtein distance query」よりも優れたパフォーマンスを発揮します。
Damerau-levenshtein距離の実装は、ここで見つけることができます: Damerau-Levenshteinアルゴリズム:転置を伴うLevenshtein 文字が考慮されます。シュナーダーのリンクのコメントで見つけました、ありがとう!
上記のlevenshtein <= 1に指定された関数は正しくありません。たとえば、 "bed"や "bid"に対して誤った結果を与えます。
最初の回答で、上記の「MySQL Levenshtein distance query」を変更して、少しだけ高速化する「制限」を受け入れました。基本的に、Levenshtein <= 1のみに関心がある場合、制限を "2"に設定すると、関数は0または1の場合、正確なLevenshtein距離を返します。または、正確なレベンシュタイン距離が2以上の場合は2。
このMODを使用すると、15〜50%高速になります。検索ワードが長くなればなるほど、利点は大きくなります(アルゴリズムが早く救済できるためです)。 「ギグ」、オリジナルは私のラップトップでは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$$
この機能を使用できます
CREATE FUNCTION `levenshtein`(s1 text、s2 text)戻り値int(11) DETERMINISTIC BEGIN DECLARE s1_len、s2_len、i 、j、c、c_temp、コストINT; DECLARE 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の場合 RETURN 0; ELSEIF s1_len = 0 THEN RETURN s2_len; ELSEIF s2_len = 0 THEN RETURN s1_len; ELSE j <= s2_len DO SET cv1 = CONCAT(cv1、UNHEX(HEX(j)))、j = j + 1; END WHILE; i <= s1_len DO SET s1_char = SUBSTRING(s1、i、1)、c = i、cv0 = UNHEX(HEX(i))、j = 1; j <= s2_len DO SET c = c + 1; IF s1_char = SUBSTRING(s2、j、1)THEN SET cost = 0; ELSE SETコスト= 1; END IF; SET c_temp = CONV(HEX(SUBSTRING(cv1、j、1))、16、10)+ cost; IF c> c_temp THEN SET c = c_temp;終了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; 終わり
xX%として取得するには、この関数を使用します
CREATE関数 `levenshtein_ratio`(s1 text、s2 text)戻り値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); 終わり
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
これは基本的に、レーベンシュタイン距離の再帰的記述における単一のステップです。関数は、距離が最大1の場合は1を返し、そうでない場合は0を返します。
この関数は、レーベンシュタイン距離を完全には計算しないため、はるかに高速です。
また、この関数を、再帰的に呼び出すことにより、レベンシュタイン距離が最大2または3である場合にtrue
を返すように変更できます。 MySQLが再帰呼び出しをサポートしていない場合、この関数のわずかに変更されたバージョンを2回コピーし、代わりに呼び出すことができます。ただし、正確なレベンシュタイン距離を計算するために再帰関数を使用しないでください。
Gonzalo NavarroとRicardo Baeza-yatesの論文に基づいて、インデックス付きテキストを複数回検索するために、LevenshteinまたはDamerau-Levenshtein(おそらく後者)に基づく検索を設定しています: link text
サフィックス配列( wikipediaを参照 )を作成した後、検索文字列に対して最大でk個の不一致がある文字列に関心がある場合、検索文字列をk + 1個に分割します。それらの少なくとも1つは無傷でなければなりません。接尾辞配列のバイナリ検索で部分文字列を見つけてから、一致した各部分の周りのパッチに距離関数を適用します。
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 ;
K距離検索の特殊なケースがあり、MySQLにDamerau-Levenshtein UDFをインストールした後、クエリに時間がかかりすぎることがわかりました。私は次の解決策を思いつきました:
ターゲットフィールドの各文字位置の列を使用して、新しいテーブルを作成します(またはターゲットテーブルに列を追加します)。すなわち。 VARCHAR(9)は、メインテーブルに一致する9 TINYINT列+ 1 ID列になりました(各列にインデックスを追加します)。メインテーブルが更新されたときに、これらの新しい列が常に更新されるようにトリガーを追加しました。
K-distanceクエリを実行するには、次の述語を使用します。
(Column1 = s [0])+(Column2 = s [1])+(Column3 = s [2])+(Column4 = s [3])+ ...> = m
ここで、sは検索文字列で、mは一致する文字の必要数です(または、私の場合、m = 9-dで、dは返される最大距離です)。
テスト後、平均で4.6秒かかった100万行を超えるクエリが、一致するIDを1秒未満で返していることがわかりました。メインテーブル内の一致する行のデータを返す2番目のクエリも、同様に1秒かかりました。 (これら2つのクエリをサブクエリまたは結合として組み合わせると、実行時間が大幅に長くなり、その理由はわかりません。)
これはDamerau-Levenshteinではありませんが(置換を考慮していません)、それは私の目的には十分です。
このソリューションは、おそらく、より大きな(長さの)サーチスペースに対してはうまくスケーリングしませんが、この制限的なケースでは非常にうまく機能しました。