web-dev-qa-db-fra.com

Rechercher des lignes avec des valeurs de chaîne similaires

J'ai une table de base de données Microsoft SQL Server 2012 avec environ 7 millions d'enregistrements issus de la foule, contenant principalement une valeur de nom de chaîne avec des détails connexes. Pour presque tous les enregistrements, il semble qu'il existe une douzaine d'enregistrements de faute de frappe similaires et j'essaie de faire des correspondances floues pour identifier des groupes d'enregistrements tels que "Apple", "Aple", "Apples", "Spple", etc. Ces noms peuvent également contiennent plusieurs mots avec des espaces entre eux.

J'ai trouvé une solution en utilisant une fonction scalaire de distance d'édition qui renvoie le nombre de frappes nécessaires pour la transformation de chaîne1 en chaîne2 et en utilisant cette fonction pour joindre la table à elle-même. Comme vous pouvez l'imaginer, cela ne fonctionne pas très bien car il doit exécuter la fonction des millions de fois pour évaluer une jointure.

J'ai donc mis cela dans un curseur pour qu'au moins une seule chaîne1 soit évaluée à la fois, cela donne au moins des résultats, mais après l'avoir laissé fonctionner pendant des semaines, il n'a réussi à évaluer que 150 000 enregistrements. Avec 7 millions à évaluer, je ne pense pas avoir le temps que prendra ma méthode.

J'ai mis des index de texte intégral sur les noms de chaîne, mais je n'ai pas vraiment trouvé de moyen d'utiliser les prédicats de texte intégral lorsque je n'avais pas de valeur statique que je recherchais.

Des idées sur la façon dont je pourrais faire quelque chose comme ce qui suit d'une manière qui ne prendrait pas des mois à courir?

  SELECT t1.name, t2.name
  FROM names AS t1
  INNER JOIN names AS t2
       ON EditDistance(t1.name,t2.name) = 1
       AND t1.id != t2.id

J'ai essayé soundex, mais comme les noms peuvent contenir des espaces et plusieurs mots par valeur, j'obtiens trop de faux positifs pour l'utiliser de manière fiable.

4
kscott

Après avoir résolu ce problème, le moyen le plus efficace et le plus performant est de créer une fonction CLR qui calcule la distance de Levenshtein. Vous pourrez marquer l'assembly comme SÉCURISÉ (si vous êtes préoccupé par la sécurité), et il s'exécute beaucoup plus rapidement que SOUNDEX () ou toutes les fonctions SQL Server intégrées.

Voici le code pour configurer l'assemblage et la fonction dans la base de données, ainsi qu'une version de base de l'algorithme Levenshtein Distance implémenté en C # à partir de https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance #C #

C #:

using System;
using System.Security.Cryptography;

namespace LevenshteinDistance
{
    public class LevenshteinDistance
    {
        private int LevenshteinDistance(string a, string b)
        {
            if (string.IsNullOrEmpty(a))
            {
                if (!string.IsNullOrEmpty(b))
                {
                    return b.Length;
                }
                return 0;
            }

            if (string.IsNullOrEmpty(b))
            {
                if (!string.IsNullOrEmpty(a))
                {
                    return a.Length;
                }
                return 0;
            }

            int cost;
            int[,] d = new int[a.Length + 1, b.Length + 1];
            int min1;
            int min2;
            int min3;

            for (int i = 0; i <= d.GetUpperBound(0); i += 1)
            {
                d[i, 0] = i;
            }

            for (int i = 0; i <= d.GetUpperBound(1); i += 1)
            {
                d[0, i] = i;
            }

            for (int i = 1; i <= d.GetUpperBound(0); i += 1)
            {
                for (int j = 1; j <= d.GetUpperBound(1); j += 1)
                {
                    cost = (a[i-1] != b[j-1])? 1 : 0; 

                    min1 = d[i - 1, j] + 1;
                    min2 = d[i, j - 1] + 1;
                    min3 = d[i - 1, j - 1] + cost;
                    d[i, j] = Math.Min(Math.Min(min1, min2), min3);
                }
            }
            return d[d.GetUpperBound(0), d.GetUpperBound(1)];
        }        
    }
}

T-SQL:

use [master];
go

exec sp_configure 'clr enabled', 1;
go
reconfigure with override;
go

use [database_name];
go

-- Drop the function...
if exists (select 1 from sys.objects so where so.[name] = 'LevenshteinDistance')
    drop function dbo.LevenshteinDistance;
go

-- ...then the Assembly
if exists (select 1 from sys.assemblies sa where sa.[name] = 'LevenshteinDistance')
    drop Assembly [LevenshteinDistance];
go

-- Now load the Assembly from an appropriately accessible location
create Assembly [LevenshteinDistance]
from
    'd:\LevenshteinDistance.dll'
with
    permission_set = safe;
go

-- Create an asymmetric key from the Assembly file
use [master];
go

if not exists (select 1 from sys.asymmetric_keys ak where ak.[name] = 'LevenshteinDistanceKey')
begin
    create asymmetric key LevenshteinDistanceKey
    from executable file = 'd:\LevenshteinDistance.dll';
end
go

-- Create a user to associate with the Assembly from the asymmetric key, and then
-- revoke connect access. The login is used to execute the Assembly.
use [master];
go

if not exists (select 1 from sys.server_principals sp where sp.[name] = 'LevenshteinDistanceKeyUser')
begin
    create login LevenshteinDistanceKeyUser from asymmetric key LevenshteinDistanceKey;
    revoke connect sql from LevenshteinDistanceKeyUser;
end
go

grant external access Assembly to LevenshteinDistanceKeyUser;
go

use [database_name];
go
alter Assembly [LevenshteinDistance] with permission_set = safe;
go

-- Create the SQL function which will be called
create function [dbo].LevenshteinDistance
(
    @string1 nvarchar(2048)
    ,@string2 nvarchar(2048)
)
returns nvarchar(max)
as
    external name LevenshteinDistance.[LevenshteinDistance.LevenshteinDistance].LevenshteinDistance;
go
2
Stephen Falken