Подтвердить что ты не робот

Улучшение результата поиска с использованием расстояния Левенштейна в Java

У меня есть следующий рабочий код Java для поиска слова против списка слов, и он работает отлично и как ожидалось:

public class Levenshtein {
    private int[][] wordMartix;

    public Set similarExists(String searchWord) {

        int maxDistance = searchWord.length();
        int curDistance;
        int sumCurMax;
        String checkWord;

        // preventing double words on returning list
        Set<String> fuzzyWordList = new HashSet<>();

        for (Object wordList : Searcher.wordList) {
            checkWord = String.valueOf(wordList);
            curDistance = calculateDistance(searchWord, checkWord);
            sumCurMax = maxDistance + curDistance;
            if (sumCurMax == checkWord.length()) {
                fuzzyWordList.add(checkWord);
            }
        }
        return fuzzyWordList;
    }

    public int calculateDistance(String inputWord, String checkWord) {
        wordMartix = new int[inputWord.length() + 1][checkWord.length() + 1];

        for (int i = 0; i <= inputWord.length(); i++) {
            wordMartix[i][0] = i;
        }

        for (int j = 0; j <= checkWord.length(); j++) {
            wordMartix[0][j] = j;
        }

        for (int i = 1; i < wordMartix.length; i++) {
            for (int j = 1; j < wordMartix[i].length; j++) {
                if (inputWord.charAt(i - 1) == checkWord.charAt(j - 1)) {
                    wordMartix[i][j] = wordMartix[i - 1][j - 1];
                } else {
                    int minimum = Integer.MAX_VALUE;
                    if ((wordMartix[i - 1][j]) + 1 < minimum) {
                        minimum = (wordMartix[i - 1][j]) + 1;
                    }

                    if ((wordMartix[i][j - 1]) + 1 < minimum) {
                        minimum = (wordMartix[i][j - 1]) + 1;
                    }

                    if ((wordMartix[i - 1][j - 1]) + 1 < minimum) {
                        minimum = (wordMartix[i - 1][j - 1]) + 1;
                    }

                    wordMartix[i][j] = minimum;
                }
            }
        }

        return wordMartix[inputWord.length()][checkWord.length()];
    }

}

Прямо сейчас, когда я ищу слово, например job, он возвращает список:

Выход

joborienterede
jobannoncer
jobfunktioner
perjacobsen
jakobsen
jobprofiler
jacob
jobtitler
jobbet
jobdatabaserne
jobfunktion
jakob
jobs
studenterjobber
johannesburg
jobmuligheder
jobannoncerne
jobbaser
job
joberfaringer

Как вы можете видеть, на выходе есть много связанных слов, но также есть несвязанные, такие как jakob, jacob и т.д., что верно в отношении формулы Левенштейна, но я хотел бы построить дальше и написать метод, который может точно настроить мой поиск, чтобы я мог получить более релевантные и связанные слова.

Я работал несколько часов на нем и потерял зрение от творчества.

Мой вопрос: Можно ли настроить существующий метод, чтобы возвращать соответствующие/связанные слова. Или я должен использовать другой подход? Или? во всех случаях ДА или НЕТ, я оценил, могу ли я получить вдохновение и вдохновение в отношении улучшения результатов поиска?


UPDATE

После того, как я задал этот вопрос долгое время назад, я действительно не нашел решение, и я вернулся к нему, потому что настало время, когда мне нужен полезный ответ, , это нормально, чтобы предоставить ответ с образцами кода JAVA, но что наиболее важным является подробный ответ с описанием доступных методов и подходов, используемых для индексации наилучших и наиболее релевантных результатов поиска и игнорирования каких-либо релевантных слов. Я знаю, что это открытая и бесконечная область, но мне нужно немного вдохновения, чтобы начать что-то там.

Примечание. Самый старый ответ прямо сейчас основан на одном из входов комментариев и не полезно (бесполезно), он просто сортирует расстояние, что не означает получение лучших результатов/качества поиска.

Итак, я сделал сортировку по расстоянию, и результаты были такими:

job
jobs
jacob
jakob
jobbet
jakobsen
jobbaser
jobtitler
jobannoncer
jobfunktion
jobprofiler
perjacobsen
johannesburg
jobannoncerne
joberfaringer
jobfunktioner
jobmuligheder
jobdatabaserne
joborienterede
studenterjobber

поэтому слово jobbaser имеет значение, а jacob/jakob не имеет значения, но расстояние для jobbaser больше, чем jacob/jakob. Так что это не помогло.


Общая обратная связь относительно ответов

  • @SergioMontoro, он решает почти проблему.
  • @uSeemSurprised, он решает проблему, но требует постоянной манипуляции.
  • Концепция @Gene превосходна, но она ретранслируется на внешний URL.

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

Особая благодарность за ответы от @SergioMontoro, @uSeemSurprised и @Gene, это разные, но действительные и полезные ответы.

@D.Kovács указывает на какое-то интересное решение.

Жаль, что я не смогу дать щедрость всем этим ответам. Выбирайте один ответ и дайте ему щедрость, это не значит, что другие ответы недействительны, но это означает только то, что конкретный ответ, который я выбрал, был полезен для меня.

4b9b3361

Ответ 1

Не понимая смысла слов, подобных @DrYap, следующая логическая единица для сравнения двух слов (если вы не ищете орфографические ошибки) - это слоги. Очень легко изменить Левенштейна для сравнения слогов вместо символов. Трудная часть разбивает слова на слоги. Существует реализация Java TeXHyphenator-J, которая может использоваться для разделения слов. Основываясь на этой библиотеке переносов, здесь представлена ​​модифицированная версия функции Левенштейна, написанная Майкл Гиллеланд и Час Эмерик. Подробнее об обнаружении syllable здесь и здесь. Конечно, вы захотите избежать сравнения слогов двух слов с одним слогом, вероятно, справляясь с этим случаем со стандартным Levenshtein.

import net.davidashen.text.Hyphenator;

public class WordDistance {

    public static void main(String args[]) throws Exception {
        Hyphenator h = new Hyphenator();
        h.loadTable(WordDistance.class.getResourceAsStream("hyphen.tex"));
        getSyllableLevenshteinDistance(h, args[0], args[1]);
    }

    /**
     * <p>
     * Calculate Syllable Levenshtein distance between two words </p>
     * The Syllable Levenshtein distance is defined as the minimal number of
     * case-insensitive syllables you have to replace, insert or delete to transform word1 into word2.
     * @return int
     * @throws IllegalArgumentException if either str1 or str2 is <b>null</b>
     */
    public static int getSyllableLevenshteinDistance(Hyphenator h, String s, String t) {
        if (s == null || t == null)
            throw new NullPointerException("Strings must not be null");

        final String hyphen = Character.toString((char) 173);
        final String[] ss = h.hyphenate(s).split(hyphen);
        final String[] st = h.hyphenate(t).split(hyphen);

        final int n = ss.length;
        final int m = st.length;

        if (n == 0)
            return m;
        else if (m == 0)
            return n;

        int p[] = new int[n + 1]; // 'previous' cost array, horizontally
        int d[] = new int[n + 1]; // cost array, horizontally

        for (int i = 0; i <= n; i++)
            p[i] = i;

        for (int j = 1; j <= m; j++) {
            d[0] = j;
            for (int i = 1; i <= n; i++) {
                int cost = ss[i - 1].equalsIgnoreCase(st[j - 1]) ? 0 : 1;
                // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
                d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
            }
            // copy current distance counts to 'previous row' distance counts
            int[] _d = p;
            p = d;
            d = _d;
        }

        // our last action in the above loop was to switch d and p, so p now actually has the most recent cost counts
        return p[n];
    }

}

Ответ 2

Вы можете изменить расстояние Левенштейна, скорректировав счет, когда совпадают последовательные символы.

Всякий раз, когда есть последовательные символы, которые соответствуют, оценка может быть уменьшена, что делает поиск более релевантным.

например: Давайте скажем, что коэффициент, по которому мы хотим уменьшить балл, равен 10, то, если одним словом мы найдем подстроку "задание", мы можем уменьшить оценку на 10, когда мы столкнемся с "j" fururur, уменьшим ее на (10 + 20), когда мы находим строку "jo" и, наконец, уменьшаем оценку (10 + 20 + 30), когда находим "работу".

Я написал код С++ ниже:

#include <bits/stdc++.h>

#define INF -10000000
#define FACTOR 10

using namespace std;

double memo[100][100][100];

double Levenshtein(string inputWord, string checkWord, int i, int j, int count){
    if(i == inputWord.length() && j == checkWord.length()) return 0;    
    if(i == inputWord.length()) return checkWord.length() - j;
    if(j == checkWord.length()) return inputWord.length() - i;
    if(memo[i][j][count] != INF) return memo[i][j][count];

    double ans1 = 0, ans2 = 0, ans3 = 0, ans = 0;
    if(inputWord[i] == checkWord[j]){
        ans1 = Levenshtein(inputWord, checkWord, i+1, j+1, count+1) - (FACTOR*(count+1));
        ans2 = Levenshtein(inputWord, checkWord, i+1, j, 0) + 1;
        ans3 = Levenshtein(inputWord, checkWord, i, j+1, 0) + 1;
        ans = min(ans1, min(ans2, ans3));
    }else{
        ans1 = Levenshtein(inputWord, checkWord, i+1, j, 0) + 1;
        ans2 = Levenshtein(inputWord, checkWord, i, j+1, 0) + 1;
        ans = min(ans1, ans2);
    }
    return memo[i][j][count] = ans;
}

int main(void) {
    // your code goes here
    string word = "job";
    string wordList[40];
    vector< pair <double, string> > ans;
    for(int i = 0;i < 40;i++){
        cin >> wordList[i];
        for(int j = 0;j < 100;j++) for(int k = 0;k < 100;k++){
            for(int m = 0;m < 100;m++) memo[j][k][m] = INF;
        }
        ans.push_back( make_pair(Levenshtein(word, wordList[i], 
            0, 0, 0), wordList[i]) );
    }
    sort(ans.begin(), ans.end());
    for(int i = 0;i < ans.size();i++){
        cout << ans[i].second << " " << ans[i].first << endl;
    }
    return 0;
}

Ссылка на демонстрацию: http://ideone.com/4UtCX3

Здесь ФАКТОР принимается за 10, вы можете экспериментировать с другими словами и выбирать подходящее значение.

Также обратите внимание, что сложность вышеизложенного Levenshtein Distance также увеличилась, теперь O(n^3) вместо O(n^2), так как теперь мы также отслеживаем счетчик, который подсчитывает, сколько последовательных символов мы встретили.

Вы можете играть со счетом, увеличивая его постепенно, после того, как найдете некоторую последовательную подстроку, а затем несоответствие, а не текущий способ, когда у нас есть фиксированный балл 1, который добавляется к общей оценке.

Кроме того, в приведенном выше решении вы можете удалить строки с оценкой >= 0, поскольку они вообще не освобождены, вы также можете выбрать другой порог для более точного поиска.

Ответ 3

Поскольку вы спросили, я покажу, как семантическая сеть UMBC может делать это. Не уверен, что вы действительно хотите:

import static java.lang.String.format;
import static java.util.Comparator.comparingDouble;
import static java.util.stream.Collectors.toMap;
import static java.util.function.Function.identity;

import java.util.Map.Entry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.regex.Pattern;

public class SemanticSimilarity {
  private static final String GET_URL_FORMAT
      = "http://swoogle.umbc.edu/SimService/GetSimilarity?"
          + "operation=api&phrase1=%s&phrase2=%s";
  private static final Pattern VALID_WORD_PATTERN = Pattern.compile("\\w+");
  private static final String[] DICT = {
    "cat",
    "building",
    "girl",
    "ranch",
    "drawing",
    "wool",
    "gear",
    "question",
    "information",
    "tank" 
  };

  public static String httpGetLine(String urlToRead) throws IOException {
    URL url = new URL(urlToRead);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestMethod("GET");
    try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(conn.getInputStream()))) {
      return reader.readLine();
    }
  }

  public static double getSimilarity(String a, String b) {
    if (!VALID_WORD_PATTERN.matcher(a).matches()
        || !VALID_WORD_PATTERN.matcher(b).matches()) {
      throw new RuntimeException("Bad word");
    }
    try {
      return Double.parseDouble(httpGetLine(format(GET_URL_FORMAT, a, b)));
    } catch (IOException | NumberFormatException ex) {
      return -1.0;
    }
  }

  public static void test(String target) throws IOException {
    System.out.println("Target: " + target);
    Arrays.stream(DICT)
        .collect(toMap(identity(), word -> getSimilarity(target, word)))
        .entrySet().stream()
        .sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
        .forEach(System.out::println);
    System.out.println();
  }

  public static void main(String[] args) throws Exception {
    test("sheep");
    test("vehicle");
    test("house");
    test("data");
    test("girlfriend");
  }
}

Результаты выглядят увлекательными:

Target: sheep
ranch=0.38563728
cat=0.37816614
wool=0.36558008
question=0.047607
girl=0.0388761
information=0.027191084
drawing=0.0039623436
tank=0.0
building=0.0
gear=0.0

Target: vehicle
tank=0.65860236
gear=0.2673374
building=0.20197356
cat=0.06057514
information=0.041832563
ranch=0.017701812
question=0.017145569
girl=0.010708235
wool=0.0
drawing=0.0

Target: house
building=1.0
ranch=0.104496084
tank=0.103863
wool=0.059761923
girl=0.056549154
drawing=0.04310725
cat=0.0418914
gear=0.026439993
information=0.020329408
question=0.0012588014

Target: data
information=0.9924584
question=0.03476312
gear=0.029112043
wool=0.019744944
tank=0.014537057
drawing=0.013742204
ranch=0.0
cat=0.0
girl=0.0
building=0.0

Target: girlfriend
girl=0.70060706
ranch=0.11062875
cat=0.09766617
gear=0.04835723
information=0.02449007
wool=0.0
question=0.0
drawing=0.0
tank=0.0
building=0.0

Ответ 4

Я попробовал предложение из комментариев по сортировке совпадений на расстояние, возвращаемое Levenshtein algo, и похоже, что оно дает лучшие результаты.

(Поскольку я не мог найти, как я не смог найти класс Searcher из вашего кода, я взял на себя смелость использовать другой источник словарного списка, реализацию Levenshtein и язык.)

Использование списка слов, представленного в Ubuntu, и реализация Levenshtein algo от https://github.com/ztane/python-Levenshtein, Я создал небольшой script, который запрашивает слово и печатает все ближайшие слова и расстояние как кортеж.

Код - https://gist.github.com/atdaemon/9f59ad886c35024bdd28

from Levenshtein import distance
import os

def read_dict() :
    with open('/usr/share/dict/words','r') as f : 
        for line in f :
            yield str(line).strip()

inp = str(raw_input('Enter a word : '))

wordlist = read_dict()
matches = []
for word in wordlist :
    dist = distance(inp,word)
    if dist < 3 :
        matches.append((dist,word))
print os.linesep.join(map(str,sorted(matches)))

Пример вывода -

Enter a word : job
(0, 'job')
(1, 'Bob')
(1, 'Job')
(1, 'Rob')
(1, 'bob')
(1, 'cob')
(1, 'fob')
(1, 'gob')
(1, 'hob')
(1, 'jab')
(1, 'jib')
(1, 'jobs')
(1, 'jog')
(1, 'jot')
(1, 'joy')
(1, 'lob')
(1, 'mob')
(1, 'rob')
(1, 'sob')
...

Enter a word : checker
(0, 'checker')
(1, 'checked')
(1, 'checkers')
(2, 'Becker')
(2, 'Decker')
(2, 'cheaper')
(2, 'cheater')
(2, 'check')
(2, "check's")
(2, "checker's")
(2, 'checkered')
(2, 'checks')
(2, 'checkup')
(2, 'cheeked')
(2, 'cheekier')
(2, 'cheer')
(2, 'chewer')
(2, 'chewier')
(2, 'chicer')
(2, 'chicken')
(2, 'chocked')
(2, 'choker')
(2, 'chucked')
(2, 'cracker')
(2, 'hacker')
(2, 'heckler')
(2, 'shocker')
(2, 'thicker')
(2, 'wrecker')

Ответ 5

Это действительно открытый вопрос, но я бы предложил альтернативный подход, который использует, например, алгоритм Smith-Waterman, как описано в this SO.

Другим (более легким) решением было бы использовать другие метрики расстояния/сходства от НЛП (например, сходство с косинусом или Расстояние Дамерау-Левенштейна).