JavaScript fuzzy search, który ma sens

Szukam fuzzy search JavaScript library do filtrowania tablicy. Próbowałem użyć fuzzyset.JS i bezpiecznik.js, ale wyniki są fatalne(są dema, które możesz wypróbować na linkowanych stronach).

Po przeczytaniu odległości Levenshteina, wydaje mi się to słabym przybliżeniem tego, czego użytkownicy szukają podczas pisania. Dla tych, którzy nie wiedzą, system oblicza ile wstawek, delecje, oraz podstawienia są potrzebne do dopasowania dwóch łańcuchów.

Jedną oczywistą wadą, która jest naprawiona w modelu Levenshtein-Demerau, jest to, że zarówno blub, jak i boob są uważane za równie podobne do bulb (każda wymaga dwóch podstawień). Jest jednak jasne, że bulb jest bardziej podobna do blub niż boob , a model, o którym właśnie wspomniałem, rozpoznaje to, pozwalając na transpozycje .

Chcę użyć to w kontekście dopełniania tekstu, więc jeśli mam tablicę ['international', 'splint', 'tinder'], a moje zapytanie to int, myślę, że international powinna być wyższa niż splint , mimo że pierwsza ma wynik (wyższy=gorszy) 10, a druga 3.

Więc to, czego szukam (i stworzę, jeśli nie istnieje), to biblioteka, która wykonuje następujące czynności:

  • różne manipulacje tekstowe
  • wagi każdej manipulacji inaczej w zależności od kiedy pojawiają się w słowie (wczesne manipulacje są bardziej kosztowne niż późne manipulacje) {]}
  • zwraca listę wyników posortowanych według trafności
Czy ktoś natknął się na coś takiego? Zdaję sobie sprawę, że StackOverflow nie jest miejscem do proszenia o rekomendacje oprogramowania, ale dorozumiane (już nie!) w powyższym jest: czy myślę o tym w odpowiedni sposób?

Edit

Znalazłem dobrą pracę (pdf) na ten temat. Kilka uwag i fragmenty:

Affiniczne funkcje edit-distance przypisują stosunkowo niższy koszt sekwencji wstawiania lub usuwania

Funkcja odległości Mongera-Elkana (Monge & Elkan 1996), która jest affinicznym wariantem funkcji odległości Smitha-Watermana (Durban et al. 1998) ze szczególnymi parametrami kosztów

Algorytm Smitha-Watermana porównuje segmenty, z których wynika, że algorytm Smitha–Watermana jest bardzo podobny do algorytmu Smitha-Watermana. wszystkie możliwe długości i optymalizuje miarę podobieństwa."To podejście n-gram.

Ogólnie podobną metryką, która nie jest oparta na modelu edit-distance, jest Jaro metric (Jaro 1995; 1989; Winkler 1999). W literaturze rekordowo-łącznikowej dobre wyniki uzyskano przy użyciu wariantów tej metody, która opiera się na liczbie i kolejności wspólnych znaków między dwoma ciągami.

Wariant tego ze względu na Winkler (1999) również używa długości P najdłuższy wspólny przedrostek

(wydaje się być przeznaczone głównie do krótkich strun)

W celu uzupełnienia tekstu, podejście Monger-Elkan i Jaro-Winkler wydaje się mieć największy sens. Dodanie Winklera do metryki Jaro skutecznie obciąża początki słów. A affiniczny aspekt Monger-Elkan oznacza, że konieczność uzupełnienia słowa (które jest po prostu sekwencją uzupełnień) również go nie zniekształci mocno.

Wniosek:

TFIDF ranking najlepszy spośród kilku dystansów metryki, oraz strojony afiniczny edit-distance metric zaproponowany przez Monge ' a i Elkana wypadł najlepiej spośród kilku string edit-distance metrics. Zaskakująco dobry dystans metryka jest szybkim schematem heurystycznym, zaproponowanym przez Jaro, a później rozszerzonym przez Winklera. Działa to prawie tak dobrze jak schemat Monge-Elkan, ale jest o rząd wielkości szybszy. Jeden prosty sposób łączenia TFIDF metoda i Jaro-Winkler ma zastąpić dokładny Token używany w TFIDF z przybliżonymi dopasowaniami tokenów na podstawie Jaro- Schemat Winklera. Ta kombinacja działa nieco lepiej niż Jaro-Winkler lub Tfidf średnio, a czasami wykonuje się znacznie lepiej. Jest również blisko wydajności do wyuczonej kombinacji kilku najlepszych wskaźników rozważane w tym artykule.

Author: illiteratewriter, 2014-04-26

10 answers

Dobre pytanie! Ale moim zdaniem, zamiast próbować modyfikować Levenshtein-Demerau, lepiej wypróbować inny algorytm lub połączyć / zważyć wyniki z dwóch algorytmów.

Uderza mnie, że dokładne lub bliskie dopasowania do "prefiksu początkowego" są czymś, co Levenshtein-Demerau nie nadaje szczególnej wagi -- ale twoje oczywiste oczekiwania użytkownika.

Szukałem "lepszego niż Levenshtein" i między innymi znalazłem to:

Http://www.joyofdata.de/blog/comparison-of-string-distance-algorithms/

To wymienia kilka miar "odległości łańcucha". Trzy, które wyglądały szczególnie istotne dla Twoich wymagań, to:

  1. Najdłuższa wspólna odległość podłańcucha: minimalna liczba symboli, które muszą zostać usunięte w obu łańcuchach, dopóki wynikowe podłańcuchy nie będą identyczne.

  2. Odległość Q-gram: suma różnic bezwzględnych pomiędzy N-gramowymi wektorami obu ciągów.

  3. Odległość Jaccarda: 1 minuje iloraz dzielonych N-gramów i wszystkich obserwowanych N-gramów.

Może mógłbyś użyć ważonej kombinacji (lub minimum) tych metryk, z Levenshtein -- common substring, common N-gram lub Jaccard będą zdecydowanie preferować podobne ciągi -- a może spróbuj po prostu użyć Jaccard?

W zależności od wielkości listy/ bazy danych algorytmy te mogą być umiarkowanie drogie. Dla rozmytego wyszukiwania zaimplementowałem, użyłem konfigurowalnej liczby n-gramów jako "kluczy pobierania" z DB, a następnie uruchomiłem kosztowną miarę odległości ciąg, aby posortować je w kolejności preferencji.

Napisałem kilka uwag na temat Fuzzy String Search w SQL. Zobacz:

 23
Author: Thomas W,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-03-27 02:21:57

Próbowałem użyć istniejących bibliotek rozmytych, takich jak fuse.js, a także okazało się, że są straszne, więc napisałem jeden, który zachowuje się w zasadzie jak poszukiwania sublime. https://github.com/farzher/fuzzysort

Jedyną dopuszczalną literówką jest transpozycja. To całkiem solidne (1K stars, 0 issues), bardzo szybko i łatwo załatwia sprawę:

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

 71
Author: Farzher,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-04 05:11:22

Oto technika, której użyłem kilka times...It daje całkiem dobre wyniki. Nie robi jednak wszystkiego, o co prosiłeś. Ponadto może to być kosztowne, jeśli lista jest ogromna.

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

Przekaż dwa ciągi znaków do string_similarity, które zwrócą liczbę pomiędzy 0 i 1.0 w zależności od tego, jak są podobne. Ten przykład używa Lo-Dash

Przykład Użycia....

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
Również....mieć

Upewnij się, że konsola jest otwarta, bo nic nie zobaczysz:)

 18
Author: InternalFX,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-26 01:19:10

To jest moja krótka i zwarta funkcja dla dopasowania rozmytego:

function fuzzyMatch(pattern, str) {
  pattern = '.*' + pattern.split('').join('.*') + '.*';
  const re = new RegExp(pattern);
  return re.test(str);
}
 9
Author: Roi Dayan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-03-25 13:08:02

Możesz spojrzeć na Atom https://github.com/atom/fuzzaldrin / lib.

Jest dostępny na npm, ma proste API i Działa ok dla mnie.

> fuzzaldrin.filter(['international', 'splint', 'tinder'], 'int');
< ["international", "splint"]
 5
Author: Yury Solovyov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-22 17:32:49

Aktualizacja Z Listopada 2019 R. Znalazłem bezpiecznik, który ma całkiem przyzwoite ulepszenia. Nie mogłem jednak zmusić go do używania operatorów bool ' A (tj. operatorów OR, AND, etc) ani nie mogłem użyć interfejsu wyszukiwania API do filtrowania wyników.

Odkryłem nextapps-de/flexsearch: https://github.com/nextapps-de/flexsearch i wierzę, że znacznie przewyższa wiele innych bibliotek wyszukiwania javascript, które próbowałem, i ma wsparcie bool ' s, filtrowanie wyszukiwania & paginacji.

Możesz wpisać listę z obiektów javascript dla danych wyszukiwania (tj. przechowywania), A API jest dość dobrze udokumentowane: https://github.com/nextapps-de/flexsearch#api-overview

Do tej pory zindeksowałem blisko 10 000 rekordów, a moje wyszukiwania są niemal natychmiastowe; tj. niezauważalna ilość czasu dla każdego wyszukiwania.

 2
Author: David John Coleman II,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-12-02 23:44:01

Oto rozwiązanie dostarczone przez @ InternalFX, ale w JS (użyłem go tak dzieląc):

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
Author: jaya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-28 12:32:21

Naprawiłem problemy z rozwiązaniem coffeescript bigram przez InternalFx i zrobiłem z niego ogólne rozwiązanie n-gram (możesz dostosować rozmiar gramów).

Jest to TypeScript, ale można usunąć adnotacje typu i działa dobrze jako vanilla JavaScript, jak również.

/**
 * 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;
}

Przykłady:

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"))

Spróbuj w maszynopisie plac zabaw

 2
Author: MgSam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-05 14:26:32
(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
Author: guest271314,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-26 22:51:07

Fuzzy Sort jest biblioteką javascript jest pomocny do wykonywania dopasowania łańcuchów z dużej kolekcji danych.

Poniższy kod pomoże użyć sortowania rozmytego w reaccie.js.

  1. Install fuzzy sort through npm,

    npm install fuzzysort
    
  2. Stwórz zmienną referencyjną,

    const fuzzysort = require('fuzzysort')
    
  3. Użyj metody go (), aby znaleźć dopasowane łańcuchy

    search(keyword, category) {  
      return fuzzysort.go(keyword, data[category]);
    }
    

Pełny kod demo w reaccie.js

import React from 'react';
import './App.css';
import data from './testdata';
const fuzzysort = require('fuzzysort');

class App extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      keyword: '',
      results: [],
    }
    console.log("data: ", data["steam_games"]);
  }

  search(keyword, category) {  
    return fuzzysort.go(keyword, data[category]);
  }

  render(){
    return (
      <div className="App">
        <input type="text" onChange={(e)=> this.setState({keyword: e.target.value})}
          value={this.state.keyword}
        />
        <button onClick={()=>this.setState({results: this.search(this.state.keyword, "steam_games")})}>Search</button>
        {this.state.results !== null && this.state.results.length > 0 ?
          <h3>Results:</h3> : null
        }
        <ul>
        {this.state.results.map((item, index) =>{
            return(
              <li key={index}>{item.score} : {item.target}</li>
            )
          })
        }
        </ul>
      </div>
    );
  }
}

export default App;

Po Więcej patrz FuzzySort

 -1
Author: Codemaker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-11-20 11:00:49