Реализация расстояния Левенштейна для обратной комбинации строк? - PullRequest
6 голосов
/ 25 июня 2019

У меня есть список сотрудников в моей заявке.У каждого сотрудника есть имя и фамилия, поэтому у меня есть список таких элементов, как:

["Jim Carry", "Uma Turman", "Bill Gates", "John Skeet"]

Я хочу, чтобы у моих клиентов была функция поиска сотрудников по именам с помощью алгоритма нечеткого поиска.Например, если пользователь введет «Юма Турмон», вернется ближайший элемент - «Ума Турман».Я использую алгоритм расстояния Левенштейна, я нашел здесь .

static class LevenshteinDistance
{
    /// <summary>
    /// Compute the distance between two strings.
    /// </summary>
    public static int Compute(string s, string t)
    {
        int n = s.Length;
        int m = t.Length;
        int[,] d = new int[n + 1, m + 1];

        // Step 1
        if (n == 0)
        {
            return m;
        }

        if (m == 0)
        {
            return n;
        }

        // Step 2
        for (int i = 0; i <= n; d[i, 0] = i++)
        {
        }

        for (int j = 0; j <= m; d[0, j] = j++)
        {
        }

        // Step 3
        for (int i = 1; i <= n; i++)
        {
            //Step 4
            for (int j = 1; j <= m; j++)
            {
                // Step 5
                int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;

                // Step 6
                d[i, j] = Math.Min(
                    Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                    d[i - 1, j - 1] + cost);
            }
        }
        // Step 7
        return d[n, m];
    }
}

Я повторяю ввод пользователя (полное имя) по списку имен сотрудников и сравниваю расстояние.Например, если оно меньше 3, я возвращаю найденного сотрудника.

Теперь я хочу разрешить пользователям выполнять поиск по обратным именам - например, если пользователь введет «Турмон Ума», он вернет «Ума Турман», так какна самом деле реальное расстояние равно 1, поскольку имя и фамилия совпадают с фамилией и именем.Мой алгоритм теперь считает это как разные строки, далеко.Как я могу изменить его так, чтобы имена были найдены независимо от порядка?

Ответы [ 2 ]

2 голосов
/ 25 июня 2019

Вы можете создать обратную версию имен сотрудников с помощью LINQ. Например, если у вас есть список сотрудников, например

x = ["Jim Carry", "Uma Turman", "Bill Gates", "John Skeet"]

Вы можете написать следующий код:

var reversedNames = x.Select(p=> $"{p.Split(' ')[1] p.Split(' ')[0]}");

Возвращает обратную версию, например:

xReversed = ["Carry Jim", "Turman Uma", "Gates Bill", "Skeet John"]

Затем повторите этот алгоритм с этими данными.

1 голос
/ 25 июня 2019

Несколько мыслей, так как это потенциально сложная проблема, чтобы понять:

  1. Разделить имя каждого сотрудника на список строк. Лично я бы, наверное, отказался от чего-либо с двумя или менее буквами, если только это не все из названия. Это должно помочь с фамилиями, такими как "De La Cruz", которые могут быть найдены как "Dela Cruz". Сохраните список имен для каждого сотрудника в словаре, который указывает на этого сотрудника.
  2. Разделите условия поиска так же, как вы разделяете имена в списке. Для каждого поискового термина найдите имена с наименьшим расстоянием Левенштейна, затем для каждого, начиная с самого низкого, повторите поиск с остальными поисковыми терминами, сравнивая его с другими именами этого сотрудника. Повторите этот шаг для каждого слова в запросе. Например, если запрос - John Smith, найдите наилучшие совпадения имен отдельных слов для John, затем сопоставьте оставшиеся имена для этих сотрудников с "наилучшим соответствием" на Smith и получите сумму расстояний. Затем найдите наилучшие совпадения для Smith и сопоставьте оставшиеся имена на John и суммируйте расстояния. Наилучшим совпадением является тот, который имеет наименьшее общее расстояние. Вы можете предоставить список лучших совпадений, вернув 10 лучших, скажем, отсортированных по общему расстоянию. И не имеет значения, какие имена в базе данных или условия поиска. На самом деле они могут быть совершенно не в порядке, и это не будет иметь значения.
  3. Подумайте, как обрабатывать дефисные имена. Я, вероятно, разделил бы их, как если бы они не были переносами.
  4. Рассмотрим символы верхнего / нижнего регистра, если вы этого еще не сделали. Вы должны хранить поиски в одном случае и преобразовать поисковые термины в один и тот же случай перед сравнением.
  5. Будьте осторожны с акцентированными буквами, многие люди имеют их в своих именах, например á. Ваш алгоритм не будет работать правильно с ними. Будьте еще более осторожны, если вы ожидаете, что когда-нибудь у вас будут двухбайтовые символы не-альфа, например. Китайский, японский, арабский и др.

Еще два преимущества разделения имен каждого сотрудника:

  • «Неиспользуемые» имена не будут учитываться при подсчете, поэтому, если я буду искать только по фамилии, это не будет учитываться при поиске кратчайшего расстояния.
  • В том же духе вы можете применить некоторые дополнительные правила, чтобы помочь в поиске нестандартных имен. Например, дефисные имена могут храниться как в виде переносов (например, Wells-Harvey), составных (WellsHarvey), так и индивидуальных имен (Wells и Harvey отдельно), все в отношении одного и того же сотрудника. Совпадение на малом расстоянии для любого имени - это совпадение на малом расстоянии для сотрудника, опять же, дополнительные имена не учитываются при подсчете.

Вот некоторый базовый код, который, кажется, работает, однако он действительно учитывает только пункты 1, 2 и 4:

using System;
using System.Collections.Generic;
using System.Linq;

namespace EmployeeSearch
{
    static class Program
    {
        static List<string> EmployeesList = new List<string>() { "Jim Carrey", "Uma Thurman", "Bill Gates", "Jon Skeet" };
        static Dictionary<int, List<string>> employeesById = new Dictionary<int, List<string>>();
        static Dictionary<string, List<int>> employeeIdsByName = new Dictionary<string, List<int>>();

        static void Main()
        {
            Init();
            var results = FindEmployeeByNameFuzzy("Umaa Thurrmin");
            // Returns:
            // (1) Uma Thurman  Distance: 3
            // (0) Jim Carrey  Distance: 10
            // (3) Jon Skeet  Distance: 11
            // (2) Bill Gates  Distance: 12
            Console.WriteLine(string.Join("\r\n", results.Select(r => $"({r.Id}) {r.Name}  Distance: {r.Distance}")));

            var results = FindEmployeeByNameFuzzy("Tormin Oma");
            // Returns:
            // (1) Uma Thurman  Distance: 4
            // (3) Jon Skeet  Distance: 7
            // (0) Jim Carrey  Distance: 8
            // (2) Bill Gates  Distance: 9
            Console.WriteLine(string.Join("\r\n", results.Select(r => $"({r.Id}) {r.Name}  Distance: {r.Distance}")));


            Console.Read();
        }

        private static void Init() // prepare our lists
        {
            for (int i = 0; i < EmployeesList.Count; i++)
            {
                // Preparing the list of names for each employee - add special cases such as hyphenation here as well
                var names = EmployeesList[i].ToLower().Split(new char[] { ' ' }).ToList();
                employeesById.Add(i, names);

                // This is not used here, but could come in handy if you want a unique index of names pointing to employee ids for optimisation:
                foreach (var name in names)
                {
                    if (employeeIdsByName.ContainsKey(name))
                    {
                        employeeIdsByName[name].Add(i);
                    }
                    else
                    {
                        employeeIdsByName.Add(name, new List<int>() { i });
                    }
                }
            }

        }

        private static List<SearchResult> FindEmployeeByNameFuzzy(string query)
        {
            var results = new List<SearchResult>();

            // Notice we're splitting the search terms the same way as we split the employee names above (could be refactored out into a helper method)
            var searchterms = query.ToLower().Split(new char[] { ' ' });

            // Comparison with each employee
            for (int i = 0; i < employeesById.Count; i++)
            {
                var r = new SearchResult() { Id = i, Name = EmployeesList[i] };
                var employeenames = employeesById[i];
                foreach (var searchterm in searchterms)
                {
                    int min = searchterm.Length;

                    // for each search term get the min distance for all names for this employee
                    foreach (var name in employeenames)
                    {
                        var distance = LevenshteinDistance.Compute(searchterm, name);
                        min = Math.Min(min, distance);
                    }

                    // Sum the minimums for all search terms
                    r.Distance += min;
                }
                results.Add(r);
            }

            // Order by lowest distance first
            return results.OrderBy(e => e.Distance).ToList();
        }
    }

    public class SearchResult
    {
        public int Distance { get; set; }
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public static class LevenshteinDistance
    {
        /// <summary>
        /// Compute the distance between two strings.
        /// </summary>
        public static int Compute(string s, string t)
        {
            int n = s.Length;
            int m = t.Length;
            int[,] d = new int[n + 1, m + 1];

            // Step 1
            if (n == 0)
            {
                return m;
            }

            if (m == 0)
            {
                return n;
            }

            // Step 2
            for (int i = 0; i <= n; d[i, 0] = i++)
            {
            }

            for (int j = 0; j <= m; d[0, j] = j++)
            {
            }

            // Step 3
            for (int i = 1; i <= n; i++)
            {
                //Step 4
                for (int j = 1; j <= m; j++)
                {
                    // Step 5
                    int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;

                    // Step 6
                    d[i, j] = Math.Min(
                        Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                        d[i - 1, j - 1] + cost);
                }
            }
            // Step 7
            return d[n, m];
        }
    }
}

Просто позвоните Init() при запуске, затем позвоните

var results = FindEmployeeByNameFuzzy(userquery);

чтобы вернуть упорядоченный список лучших матчей.

Отказ от ответственности: Этот код не оптимален и только кратко проверен, не проверяет наличие нулей, может взорваться и убить котенка и т. Д., И т. Д. Если у вас есть большое количество сотрудников, то это может быть очень медленно. Есть несколько улучшений, которые можно сделать, например, когда вы перебираете алгоритм Левенштейна, вы можете пропасть, если расстояние превышает текущее минимальное расстояние.

...