web-dev-qa-db-ja.com

mysql /ファジー検索のためのレーベンシュタイン距離の実装?

スミスのテーブルを次のように検索して、1分散以内にあるすべてのものを取得できるようにしたいと思います。

データ:

 O'Brien 
 Smithe 
 Dolan 
 Smuth 
 Wong 
 Smoth 
 Gunther 
 Smiht 

レーベンシュタイン距離の使用を検討しましたが、これを実装する方法を知っている人はいますか?

44
Andrew Clark

レベンシュタイン距離を使用して効率的に検索するには、 bk-tree などの効率的で特殊なインデックスが必要です。残念ながら、MySQLを含め、私が知っているデータベースシステムでは、bkツリーインデックスを実装していません。行ごとに1つの用語だけではなく、全文検索を探している場合、これはさらに複雑になります。率直に言って、レベンシュタイン距離に基づいた検索を可能にする方法でフルテキストインデックスを作成できる方法は考えられません。

11
Nick Johnson

レーベンシュタイン距離関数のmysql UDF実装があります

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

Cで実装されており、schnaaderが言及した「MySQL Levenshtein distance query」よりも優れたパフォーマンスを発揮します。

7
Hongzheng

Damerau-levenshtein距離の実装は、ここで見つけることができます: Damerau-Levenshteinアルゴリズム:転置を伴うLevenshtein 文字が考慮されます。シュナーダーのリンクのコメントで見つけました、ありがとう!

5
Ponk

上記の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$$
4
stopthe

この機能を使用できます

 
 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); 
  終わり
3
Alaa

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回コピーし、代わりに呼び出すことができます。ただし、正確なレベンシュタイン距離を計算するために再帰関数を使用しないでください。

2
AbcAeffchen

Gonzalo NavarroとRicardo Baeza-yatesの論文に基づいて、インデックス付きテキストを複数回検索するために、LevenshteinまたはDamerau-Levenshtein(おそらく後者)に基づく検索を設定しています: link text

サフィックス配列( wikipediaを参照 )を作成した後、検索文字列に対して最大でk個の不一致がある文字列に関心がある場合、検索文字列をk + 1個に分割します。それらの少なくとも1つは無傷でなければなりません。接尾辞配列のバイナリ検索で部分文字列を見つけてから、一致した各部分の周りのパッチに距離関数を適用します。

2
Hugh

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 ;
0
D. Savina

K距離検索の特殊なケースがあり、MySQLにDamerau-Levenshtein UDFをインストールした後、クエリに時間がかかりすぎることがわかりました。私は次の解決策を思いつきました:

  • 検索スペースが非常に制限されています(9文字の文字列は数値に制限されています)。

ターゲットフィールドの各文字位置の列を使用して、新しいテーブルを作成します(またはターゲットテーブルに列を追加します)。すなわち。 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ではありませんが(置換を考慮していません)、それは私の目的には十分です。

このソリューションは、おそらく、より大きな(長さの)サーチスペースに対してはうまくスケーリングしませんが、この制限的なケースでは非常にうまく機能しました。

0
greg