Нечіткий пошук у JavaScript, який має сенс


98

Я шукаю бібліотеку нечіткого пошуку JavaScript для фільтрації масиву. Я спробував використовувати fuzzyset.js і fuse.js , але результати жахливі (є демо-версії, які можна спробувати на пов'язаних сторінках).

Прочитавши деякий час на відстані Левенштейна, це здається мені поганим наближенням того, що користувачі шукають під час введення. Для тих, хто не знає, система обчислює, скільки потрібно вставити , видалити та замінити , щоб відповідати дві рядки.

Один очевидний недолік, який закріплений в моделі Левенштейна-Demerau, що обидва ревіти і бовдура вважаються однаково схожі на цибулини (кожен з яких вимагає дві заміни). Зрозуміло, однак, що цибулина більше схожа на ревіти , ніж бовдура є, і модель , яку я тільки що згадав визнає , що, дозволяючи транспозиція .

Я хочу використати це в контексті завершення тексту, тому, якщо у мене є масив ['international', 'splint', 'tinder'], і мій запит є int , я думаю, що міжнародний повинен оцінювати більш високу, ніж шпагату , навіть якщо у першого є оцінка (вище = гірше) 10 порівняно з останніми 3.

Отож, що я шукаю (і буду створювати його, якщо його не існує) - це бібліотека, яка робить наступне:

  • Важить різні текстові маніпуляції
  • Кожна маніпуляція зважує по-різному, залежно від місця їх появи (ранні маніпуляції коштують дорожче, ніж пізні маніпуляції)
  • Повертає список результатів, відсортований за релевантністю

Хтось натрапив на щось подібне? Я усвідомлюю, що StackOverflow не є запитом для програмних рекомендацій, але неявно (вже не!) У вищесказаному: чи я думаю про це правильно?


Редагувати

Я знайшов хороший статтю (pdf) на цю тему. Деякі примітки та уривки:

Функції Affine для редагування відстані призначають відносно нижчу вартість послідовності вставок або видалень

функція дистанції Монгера-Елкана (Monge & Elkan 1996), яка є афінним варіантом функції відстані Сміта-Уотермана (Durban et al. 1998) з конкретними параметрами витрат

Для відстані Сміта-Уотермана (вікіпедія) "Замість того, щоб дивитись на загальну послідовність, алгоритм Сміта-Уотермана порівнює сегменти всіх можливих довжин і оптимізує міру подібності". Це n-грамовий підхід.

Широко подібною метрикою, яка не ґрунтується на моделі дистанційного редагування, є метрика Яро (Jaro 1995; 1989; Winkler 1999). У літературі про зв'язування записів добрі результати отримані за допомогою варіантів цього методу, який ґрунтується на кількості та порядку загальних символів між двома рядками.

Варіант цього через Winkler (1999) також використовує довжину P найдовшого загального префікса

(здається, призначені в основному для коротких струн)

З метою доповнення тексту, здається, підхід Монгер-Елкан та Яро-Вінклер має найбільш сенс. Доповнення Вінклера до метрики Джаро значно виважує початки слів. І споріднений аспект Монгер-Елкана означає, що необхідність завершити слово (що є просто послідовністю доповнень) не буде занадто сильно недобросовісною.

Висновок:

рейтинг TFIDF виявився найкращим серед кількох метрик відстані на основі лексем, а налаштована метрика коректування відстані на афінному розриві, запропонована Монже та Елканом, найкраща серед кількох рядкових показників редагування відстані. Напрочуд гарна метрика відстані - це швидка евристична схема, запропонована Джаро та пізніше розширена Вінклером. Це працює майже так само, як і схема Монже-Елкана, але на порядок швидше. Один з простих способів поєднання методу TFIDF і Jaro-Winkler - це заміна точних збігів токенів, які використовуються в TFIDF, на приблизні відповідність жетонів на основі схеми Jaro-Winkler. Ця комбінація в середньому трохи краща, ніж або Яро-Вінклер, або TFIDF, а іноді і набагато краще. Це також є близьким по продуктивності до вивченого поєднання декількох найкращих показників, розглянутих у цій роботі.


Чудове запитання. Я хочу зробити щось подібне, але з однаковими міркуваннями щодо порівняння рядків. Ви коли-небудь знаходили / будували реалізацію javascript для ваших порівнянь рядків? Дякую.
ніколас

1
@nicholas Я просто відправив fuzzyset.js на github для обліку менших рядків запитів, і, хоча він не враховує зважених маніпуляцій з рядками, результати цілком хороші для мого наміченого застосування завершення рядка. Дивіться репо
willlma

Дякую. Я спробую. Я також знайшов цю функцію порівняння рядків: github.com/zdyn/jaro-winkler-js . Здається, теж непогано працює.
нікола


1
@michaelday Це не враховує помилки друку. У демонстраційному режимі введення тексту kroleне повертається Final Fantasy V: Krile, хоча я цього хотів би. Це вимагає, щоб усі символи запиту були присутні в одному порядку в результаті, що є досить короткозорим. Здається, єдиний спосіб здійснити нечіткий пошук - це мати базу даних загальних помилок.
willlma

Відповіді:


21

Гарне питання! Але я думаю, що замість того, щоб намагатися модифікувати Левенштайн-Демерау, вам може бути краще спробувати інший алгоритм або комбінувати / зважувати результати двох алгоритмів.

Мене вражає, що точні чи близькі відповідники "стартовому префіксу" - це те, що Левенштейн-Демерау не надає особливої ​​ваги - але ваші очевидні очікування користувачів будуть.

Я шукав "кращого за Левенштейна" і, серед іншого, знайшов таке:

http://www.joyofdata.de/blog/comppare-of-string-distance-algorithms/

Тут згадується ряд заходів "відстань рядків". Три, які виглядали особливо актуальними для вашої вимоги:

  1. Найдовша відстань загальної підрядки: Мінімальна кількість символів, які потрібно видалити в обох рядках, поки отримані підрядки не будуть однаковими.

  2. q-грамова відстань: сума абсолютних різниць між N-грамними векторами обох рядків.

  3. Відстань Жакарда: 1 мінус коефіцієнт загальних N-грамів і всіх спостережуваних N-грамів.

Можливо, ви могли б використати зважену комбінацію (або мінімум) цих показників, а Левенштейн - загальна підрядка, загальний N-грам або Жаккард - всі вони сильніше віддають перевагу подібним рядкам - або, можливо, спробуйте просто використовувати Жакард?

Залежно від розміру вашого списку / бази даних, ці алгоритми можуть бути досить дорогими. Для нечіткого пошуку, який я здійснив, я використав налаштовувану кількість N-грамів як "ключі пошуку" з БД, потім запустив дорогу мірку відстані рядків для сортування їх у порядку уподобань.

Я написав кілька записок про нечіткий пошук рядків у SQL. Побачити:


63

Я спробував використати існуючі нечіткі бібліотеки на зразок fuse.js, а також виявив, що вони жахливі, тому я написав таку, яка в основному поводиться як піднесений пошук. https://github.com/farzher/fuzzysort

Єдиний друкарський помилок, який він дозволяє - це транспонирование. Це досить солідно (1 к. Зірок, 0 випусків) , дуже швидко і легко обробляє вашу справу:

fuzzysort.go('int', ['international', 'splint', 'tinder'])
// [{highlighted: '*int*ernational', score: 10}, {highlighted: 'spl*int*', socre: 3003}]


4
Я був незадоволений Fuse.js і спробував вашу бібліотеку - чудово працює! Молодці :)
Дейв

1
Єдина проблема з цією бібліотекою, з якою я стикався, - це коли слово закінчено, але написано неправильно, наприклад, якщо правильне слово було "XRP" і якщо я шукав "XRT", це не дає мені оцінки
PirateApp

1
@PirateApp yup, я не обробляю неправильні написання (оскільки пошук піднесеного не робить). Я щось дивлюся на це зараз, коли люди скаржаться. Ви можете надати мені приклад використання випадків, коли цей пошук не спрацьовує як проблема github
Farzher

3
Для тих із вас, хто цікавиться цією лібкою, тепер у неї також здійснена перевірка орфографії! Я рекомендую цю ліб над fusejs та іншими
PirateApp

1
@ user4815162342 ви повинні кодувати його самостійно. замовити цю тему, вона має зразок коду github.com/farzher/fuzzysort/isissue/19
Farzher

19

Ось методика, яку я кілька разів використовував ... Це дає досить непогані результати. Не робить все, про що ви просили, хоча. Також це може бути дорогим, якщо список є масовим.

get_bigrams = (string) ->
    s = string.toLowerCase()
    v = new Array(s.length - 1)
    for i in [0..v.length] by 1
        v[i] = s.slice(i, i + 2)
    return v

string_similarity = (str1, str2) ->
    if str1.length > 0 and str2.length > 0
        pairs1 = get_bigrams(str1)
        pairs2 = get_bigrams(str2)
        union = pairs1.length + pairs2.length
        hit_count = 0
        for x in pairs1
            for y in pairs2
                if x is y
                    hit_count++
        if hit_count > 0
            return ((2.0 * hit_count) / union)
    return 0.0

Пройдіть два рядки, до string_similarityяких повернеться число між собою 0та 1.0залежно від того, наскільки вони схожі. У цьому прикладі використовується Lo-Dash

Приклад використання ....

query = 'jenny Jackson'
names = ['John Jackson', 'Jack Johnson', 'Jerry Smith', 'Jenny Smith']

results = []
for name in names
    relevance = string_similarity(query, name)
    obj = {name: name, relevance: relevance}
    results.push(obj)

results = _.first(_.sortBy(results, 'relevance').reverse(), 10)

console.log results

Також .... мають загадку

Переконайтесь, що ваша консоль відкрита, або ви нічого не бачите :)


3
Дякую, саме це я шукав. Було б краще, якби це був звичайний js;)
lucaswxp

1
функція get_bigrams (string) {var s = string.toLowerCase () var v = s.split (''); for (var i = 0; i <v.length; i ++) {v [i] = s.slice (i, i + 2); } повернути v; } функція string_s similarity (str1, str2) {if (str1.length> 0 && str2.length> 0) {var pair1 = get_bigrams (str1); var pair2 = get_bigrams (str2); var union = pair1.length + pair2.length; var хітів = 0; для (var x = 0; x <pair1.length; x ++) {for (var y = 0; y <pair2.length; y ++) {if (pair1 [x] == pair2 [y]) hit_count ++; }} if (хіти> 0) повернути ((2.0 * звернень) / об'єднання); } повернення 0,0}
jaya

Як це використовувати в об'єктах, в яких потрібно шукати кілька клавіш?
user3808307

У цьому є кілька проблем: 1) Це набирає вагу символів на початку та в кінці рядка. 2) Порівняння біграм є O (n ^ 2). 3) Оцінка схожості може бути більше 1 через реалізацію. Це, очевидно, не має сенсу. Я вирішую всі ці проблеми у своїй відповіді нижче.
MgSam

8

це моя коротка та компактна функція для нечіткої відповідності:

function fuzzyMatch(pattern, str) {
  pattern = '.*' + pattern.split('').join('.*') + '.*';
  const re = new RegExp(pattern);
  return re.test(str);
}

Хоча, мабуть, не те, що ви хочете, в більшості випадків, мабуть, саме для мене.
schmijos

6

ви можете подивитися на https://github.com/atom/fuzzaldrin/ lib Atom .

він доступний в npm, має простий API, і працював нормально для мене.

> fuzzaldrin.filter(['international', 'splint', 'tinder'], 'int');
< ["international", "splint"]

Я також мав успіх у бібліотеці Atom, яка має простий API і блискавично =). github.com/cliffordfajardo/cato
какодер

2

Оновлення листопада 2019 року. Я знайшов запобіжник, щоб мати досить пристойні оновлення. Однак я не міг змусити його використовувати bool's (тобто оператори OR, AND тощо), а також не міг використовувати інтерфейс пошуку API для фільтрації результатів.

Я виявив nextapps-de/flexsearch: https://github.com/nextapps-de/flexsearch, і я вважаю, що це набагато перевершує багато інших пошукових бібліотек Javascript, які я намагався, і в ньому є підтримка bool, фільтрація пошукових запитів та пагінації.

Ви можете ввести список об’єктів javascript для ваших пошукових даних (тобто зберігання), а API досить добре зафіксований: https://github.com/nextapps-de/flexsearch#api-overview

Поки що я проіндексував близько 10 000 записів, і мої пошуки є негайними; тобто непомітна кількість часу для кожного пошуку.


2

ось рішення, яке надає @InternalFX, але в JS (я його так використовував):

function get_bigrams(string){
  var s = string.toLowerCase()
  var v = s.split('');
  for(var i=0; i<v.length; i++){ v[i] = s.slice(i, i + 2); }
  return v;
}

function string_similarity(str1, str2){
  if(str1.length>0 && str2.length>0){
    var pairs1 = get_bigrams(str1);
    var pairs2 = get_bigrams(str2);
    var union = pairs1.length + pairs2.length;
    var hits = 0;
    for(var x=0; x<pairs1.length; x++){
      for(var y=0; y<pairs2.length; y++){
        if(pairs1[x]==pairs2[y]) hits++;
    }}
    if(hits>0) return ((2.0 * hits) / union);
  }
  return 0.0
}

2

Я вирішив проблеми з рішенням біграму CoffeeScript фірмою InternalFx і зробив це загальним n-грамовим рішенням (ви можете налаштувати розмір грамів).

Це TypeScript, але ви можете видалити анотації типу, і він чудово працює як ванільний JavaScript.

/**
 * Compares the similarity between two strings using an n-gram comparison method. 
 * The grams default to length 2.
 * @param str1 The first string to compare.
 * @param str2 The second string to compare.
 * @param gramSize The size of the grams. Defaults to length 2.
 */
function stringSimilarity(str1: string, str2: string, gramSize: number = 2) {
  function getNGrams(s: string, len: number) {
    s = ' '.repeat(len - 1) + s.toLowerCase() + ' '.repeat(len - 1);
    let v = new Array(s.length - len + 1);
    for (let i = 0; i < v.length; i++) {
      v[i] = s.slice(i, i + len);
    }
    return v;
  }

  if (!str1?.length || !str2?.length) { return 0.0; }

  //Order the strings by length so the order they're passed in doesn't matter 
  //and so the smaller string's ngrams are always the ones in the set
  let s1 = str1.length < str2.length ? str1 : str2;
  let s2 = str1.length < str2.length ? str2 : str1;

  let pairs1 = getNGrams(s1, gramSize);
  let pairs2 = getNGrams(s2, gramSize);
  let set = new Set<string>(pairs1);

  let total = pairs2.length;
  let hits = 0;
  for (let item of pairs2) {
    if (set.delete(item)) {
      hits++;
    }
  }
  return hits / total;
}

Приклади:

console.log(stringSimilarity("Dog", "Dog"))
console.log(stringSimilarity("WolfmanJackIsDaBomb", "WolfmanJackIsDaBest"))
console.log(stringSimilarity("DateCreated", "CreatedDate"))
console.log(stringSimilarity("a", "b"))
console.log(stringSimilarity("CreateDt", "DateCreted"))
console.log(stringSimilarity("Phyllis", "PyllisX"))
console.log(stringSimilarity("Phyllis", "Pylhlis"))
console.log(stringSimilarity("cat", "cut"))
console.log(stringSimilarity("cat", "Cnut"))
console.log(stringSimilarity("cc", "Cccccccccccccccccccccccccccccccc"))
console.log(stringSimilarity("ab", "ababababababababababababababab"))
console.log(stringSimilarity("a whole long thing", "a"))
console.log(stringSimilarity("a", "a whole long thing"))
console.log(stringSimilarity("", "a non empty string"))
console.log(stringSimilarity(null, "a non empty string"))

Спробуйте його на майданчику TypeScript


0
(function (int) {
    $("input[id=input]")
        .on("input", {
        sort: int
    }, function (e) {
        $.each(e.data.sort, function (index, value) {
          if ( value.indexOf($(e.target).val()) != -1 
              && value.charAt(0) === $(e.target).val().charAt(0) 
              && $(e.target).val().length === 3 ) {
                $("output[for=input]").val(value);
          };
          return false
        });
        return false
    });
}(["international", "splint", "tinder"]))

jsfiddle http://jsfiddle.net/guest271314/QP7z5/


0

Ознайомтеся з додатком Таблиць Google під назвою Flookup і скористайтеся цією функцією:

Flookup (lookupValue, tableArray, lookupCol, indexNum, threshold, [rank])

Деталі параметра:

  1. lookupValue: значення, яке ви шукаєте
  2. tableArray: таблиця, яку ви хочете шукати
  3. lookupCol: стовпець, який потрібно шукати
  4. indexNum: стовпець, з якого потрібно повернути дані
  5. threshold: відсоткова схожість, нижче якої дані не підлягають поверненню
  6. rank: n-та найкраща відповідність (тобто, якщо перша відповідь вам не сподобалася)

Це повинно задовольнити ваші вимоги ... хоча я не впевнений у точці №2.

Дізнайтеся більше на офіційному веб-сайті .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.