Javascript Fuzzy-Suche, die Sinn macht

98

Ich suche nach einer Fuzzy-Such-JavaScript-Bibliothek, um ein Array zu filtern. Ich habe versucht, fuzzyset.js und fuse.js zu verwenden , aber die Ergebnisse sind schrecklich (es gibt Demos, die Sie auf den verlinkten Seiten ausprobieren können).

Nachdem ich etwas über die Levenshtein-Entfernung gelesen habe, scheint es mir eine schlechte Annäherung an das zu sein, wonach Benutzer suchen, wenn sie tippen. Für diejenigen, die es nicht wissen, berechnet das System, wie viele Einfügungen , Löschungen und Ersetzungen erforderlich sind, damit zwei Zeichenfolgen übereinstimmen.

Ein offensichtlicher Fehler, der im Levenshtein-Demerau-Modell behoben ist, besteht darin, dass sowohl Blub als auch Boob der Glühbirne gleich ähnlich sind (für die jeweils zwei Substitutionen erforderlich sind). Es ist jedoch klar, dass die Glühbirne dem Blub ähnlicher ist als der Boob , und das Modell, das ich gerade erwähnte, erkennt dies, indem es Transpositionen zulässt .

Ich möchte dies im Zusammenhang mit der Textvervollständigung verwenden. Wenn ich also ein Array habe ['international', 'splint', 'tinder']und meine Abfrage int ist , sollte International meiner Meinung nach einen höheren Rang als Schiene haben , obwohl Ersteres eine Punktzahl (höher = schlechter) von 10 hat gegen die 3 des letzteren.

Was ich also suche (und erstellen werde, wenn es nicht existiert), ist eine Bibliothek, die Folgendes tut:

  • Gewichtet die verschiedenen Textmanipulationen
  • Gewichtet jede Manipulation unterschiedlich, je nachdem, wo sie in einem Wort vorkommt (frühe Manipulationen sind teurer als späte Manipulationen).
  • Gibt eine nach Relevanz sortierte Ergebnisliste zurück

Ist jemand auf so etwas gestoßen? Mir ist klar, dass StackOverflow nicht der richtige Ort ist, um nach Softwareempfehlungen zu fragen, aber implizit (nicht mehr!) Ist Folgendes: Denke ich richtig darüber nach?


Bearbeiten

Ich habe ein gutes Papier (pdf) zu diesem Thema gefunden. Einige Notizen und Auszüge:

Affine Bearbeitungsentfernungsfunktionen weisen einer Folge von Einfügungen oder Löschungen relativ geringere Kosten zu

die Monger-Elkan-Distanzfunktion (Monge & Elkan 1996), eine affine Variante der Smith-Waterman-Distanzfunktion (Durban et al. 1998) mit bestimmten Kostenparametern

Für die Smith-Waterman-Distanz (Wikipedia) : "Anstatt die Gesamtsequenz zu betrachten, vergleicht der Smith-Waterman-Algorithmus Segmente aller möglichen Längen und optimiert das Ähnlichkeitsmaß." Es ist der n-Gramm-Ansatz.

Eine weitgehend ähnliche Metrik, die nicht auf einem Edit-Distance-Modell basiert, ist die Jaro-Metrik (Jaro 1995; 1989; Winkler 1999). In der Literatur zu Datensatzverknüpfungen wurden gute Ergebnisse mit Varianten dieser Methode erzielt, die auf der Anzahl und Reihenfolge der gemeinsamen Zeichen zwischen zwei Zeichenfolgen basiert.

Eine Variante davon aufgrund von Winkler (1999) verwendet auch die Länge P des längsten gemeinsamen Präfixes

(scheinen hauptsächlich für kurze Saiten gedacht zu sein)

Für die Vervollständigung von Texten scheinen die Ansätze von Monger-Elkan und Jaro-Winkler am sinnvollsten zu sein. Winklers Hinzufügung zur Jaro-Metrik gewichtet die Wortanfänge effektiv stärker. Und der affine Aspekt von Monger-Elkan bedeutet, dass die Notwendigkeit, ein Wort zu vervollständigen (was einfach eine Folge von Ergänzungen ist), es nicht zu stark benachteiligt.

Fazit:

Das TFIDF-Ranking schnitt unter mehreren tokenbasierten Entfernungsmetriken am besten ab, und eine von Monge und Elkan vorgeschlagene abgestimmte affine-Gap-Editierdistanzmetrik schnitt unter mehreren String-Editierdistanzmetriken am besten ab. Eine überraschend gute Distanzmetrik ist ein schnelles heuristisches Schema, das von Jaro vorgeschlagen und später von Winkler erweitert wurde. Dies funktioniert fast so gut wie das Monge-Elkan-Schema, ist jedoch um eine Größenordnung schneller. Eine einfache Möglichkeit, die TFIDF-Methode und den Jaro-Winkler zu kombinieren, besteht darin, die in TFIDF verwendeten genauen Token-Übereinstimmungen durch ungefähre Token-Übereinstimmungen zu ersetzen, die auf dem Jaro-Winkler-Schema basieren. Diese Kombination ist im Durchschnitt etwas besser als Jaro-Winkler oder TFIDF und gelegentlich viel besser. Die Leistung kommt auch einer erlernten Kombination mehrerer der besten in diesem Dokument berücksichtigten Metriken nahe.

willlma
quelle
Gute Frage. Ich möchte etwas Ähnliches tun, aber mit den gleichen Überlegungen zum Stringvergleich. Haben Sie jemals eine Javascript-Implementierung Ihrer Zeichenfolgenvergleiche gefunden / erstellt? Vielen Dank.
Nicholas
1
@nicholas Ich habe einfach fuzzyset.js auf github gegabelt, um kleinere Abfragezeichenfolgen zu berücksichtigen, und obwohl es keine gewichteten Zeichenfolgenmanipulationen berücksichtigt, sind die Ergebnisse für meine beabsichtigte Anwendung der Zeichenfolgenvervollständigung recht gut. Siehe das Repo
Willlma
Vielen Dank. Ich werde es versuchen. Ich habe auch diese String-Vergleichsfunktion gefunden: github.com/zdyn/jaro-winkler-js . Scheint auch ziemlich gut zu funktionieren.
Nicholas
1
Versuchen Sie dies: subtexteditor.github.io/fuzzysearch.js
michaelday
1
@michaelday Tippfehler werden nicht berücksichtigt. In der Demo krolekehrt die Eingabe nicht zurück Final Fantasy V: Krile, obwohl ich es gerne hätte. Es erfordert, dass alle Zeichen in der Abfrage im Ergebnis in derselben Reihenfolge vorhanden sind, was ziemlich kurzsichtig ist. Es scheint, dass die einzige Möglichkeit für eine gute Fuzzy-Suche darin besteht, eine Datenbank mit gängigen Tippfehlern zu haben.
Willlma

Antworten:

21

Gute Frage! Aber ich denke, anstatt zu versuchen, Levenshtein-Demerau zu modifizieren, ist es vielleicht besser, einen anderen Algorithmus auszuprobieren oder die Ergebnisse von zwei Algorithmen zu kombinieren / zu gewichten.

Es fällt mir auf, dass Levenshtein-Demerau exakten oder engen Übereinstimmungen mit dem "Startpräfix" kein besonderes Gewicht beimisst - aber Ihre offensichtlichen Benutzererwartungen würden dies tun.

Ich suchte nach "besser als Levenshtein" und fand unter anderem Folgendes:

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

Dies erwähnt eine Reihe von "String Distance" -Maßnahmen. Drei, die für Ihre Anforderung besonders relevant waren, wären:

  1. Längster gemeinsamer Teilzeichenfolgenabstand: Mindestanzahl von Symbolen, die in beiden Zeichenfolgen entfernt werden müssen, bis die resultierenden Teilzeichenfolgen identisch sind.

  2. q-Gramm-Abstand: Summe der absoluten Differenzen zwischen N-Gramm-Vektoren beider Strings.

  3. Jaccard-Abstand: 1 miniert den Quotienten aus gemeinsam genutzten N-Gramm und allen beobachteten N-Gramm.

Vielleicht könnten Sie eine gewichtete Kombination (oder ein Minimum) dieser Metriken mit Levenshtein verwenden - gemeinsame Teilzeichenfolge, gemeinsames N-Gramm oder Jaccard bevorzugen alle stark ähnliche Zeichenfolgen - oder versuchen Sie es einfach mit Jaccard?

Abhängig von der Größe Ihrer Liste / Datenbank können diese Algorithmen mäßig teuer sein. Für eine von mir implementierte Fuzzy-Suche habe ich eine konfigurierbare Anzahl von N-Gramm als "Abrufschlüssel" aus der Datenbank verwendet und dann das teure Zeichenfolgenabstandsmaß ausgeführt, um sie in der bevorzugten Reihenfolge zu sortieren.

Ich habe einige Notizen zur Fuzzy-String-Suche in SQL geschrieben. Sehen:

Thomas W.
quelle
63

Ich habe versucht, vorhandene Fuzzy-Bibliotheken wie fuse.js zu verwenden, und fand sie auch schrecklich. Deshalb habe ich eine geschrieben, die sich im Grunde wie die Suche von sublime verhält. https://github.com/farzher/fuzzysort

Der einzige Tippfehler, den es erlaubt, ist eine Transponierung. Es ist ziemlich solide (1k Sterne, 0 Ausgaben) , sehr schnell und behandelt Ihren Fall einfach:

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

Farzher
quelle
4
Ich war mit Fuse.js unzufrieden und habe Ihre Bibliothek ausprobiert - funktioniert hervorragend! Gut gemacht :)
Dave
1
Das einzige Problem mit dieser Bibliothek, mit dem ich konfrontiert war, war, wenn das Wort vollständig ist, aber falsch geschrieben wurde, zum Beispiel, wenn das richtige Wort "XRP" war und wenn ich "XRT" suchte, gibt es mir keine Punktzahl
PirateApp
1
@PirateApp yup, ich behandle keine Rechtschreibfehler (weil die Suche von sublime dies nicht tut). Ich untersuche das jetzt, wo sich die Leute beschweren. Sie können mir Beispiele für Anwendungsfälle geben, in denen diese Suche als Github-Problem
fehlschlägt
3
Für diejenigen unter Ihnen, die sich über diese Bibliothek wundern, ist jetzt auch die Rechtschreibprüfung implementiert! Ich empfehle diese Bibliothek über Fusejs und andere
PirateApp
1
@ user4815162342 Sie müssen es selbst codieren. Kasse diesen Thread, es hat ein Codebeispiel github.com/farzher/fuzzysort/issues/19
Farzher
19

Hier ist eine Technik, die ich einige Male angewendet habe ... Sie liefert ziemlich gute Ergebnisse. Tut aber nicht alles, wonach Sie gefragt haben. Dies kann auch teuer sein, wenn die Liste umfangreich ist.

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

Übergeben Sie zwei Zeichenfolgen, string_similaritydenen eine Zahl zwischen 0und 1.0je nachdem, wie ähnlich sie sind, zurückgegeben wird. In diesem Beispiel wird Lo-Dash verwendet

Anwendungsbeispiel ....

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

Auch ... habe eine Geige

Stellen Sie sicher, dass Ihre Konsole geöffnet ist oder Sie nichts sehen :)

InternalFX
quelle
3
Danke, genau das habe ich gesucht. Es wäre nur besser, wenn es einfach wäre js;)
lucaswxp
1
Funktion get_bigrams (string) {var s = string.toLowerCase () var v = s.split (''); für (var i = 0; i <v.Länge; i ++) {v [i] = s.Slice (i, i + 2); } return v; } function string_similarity (str1, str2) {if (str1.length> 0 && str2.length> 0) {var pair1 = get_bigrams (str1); var pair2 = get_bigrams (str2); var union = Paare1.Länge + Paare2.Länge; var Hits = 0; für (var x = 0; x <Paare1.Länge; x ++) {für (var y = 0; y <Paare2.Länge; y ++) {if (Paare1 [x] == Paare2 [y]) hit_count ++; }} if (Treffer> 0) return ((2.0 * Treffer) / union); } return 0.0}
jaya
Wie verwende ich dies in Objekten, nach denen Sie in mehreren Schlüsseln suchen möchten?
user3808307
Dies hat einige Probleme: 1) Die Zeichen am Anfang und Ende der Zeichenfolge werden untergewichtet. 2) Die Bigram-Vergleiche sind O (n ^ 2). 3) Der Ähnlichkeitswert kann aufgrund der Implementierung über 1 liegen. Das macht offensichtlich keinen Sinn. Ich behebe all diese Probleme in meiner Antwort unten.
MgSam
8

Dies ist meine kurze und kompakte Funktion für Fuzzy Match:

function fuzzyMatch(pattern, str) {
  pattern = '.*' + pattern.split('').join('.*') + '.*';
  const re = new RegExp(pattern);
  return re.test(str);
}
Roi Dayan
quelle
Obwohl in den meisten Fällen wahrscheinlich nicht das, was Sie wollen, war es genau für mich.
schmijos
6

Sie können einen Blick auf Atoms https://github.com/atom/fuzzaldrin/ lib werfen .

Es ist auf npm verfügbar, hat eine einfache API und hat bei mir gut funktioniert.

> fuzzaldrin.filter(['international', 'splint', 'tinder'], 'int');
< ["international", "splint"]
Yury Solovyov
quelle
Ich hatte auch Erfolg mit Atoms Bibliothek, die eine einfache API und blitzschnell hat =). github.com/cliffordfajardo/cato
cacoder
2

November 2019 Update. Ich fand Sicherung, um einige ziemlich anständige Upgrades zu haben. Ich konnte es jedoch nicht dazu bringen, Bools (dh OR-, AND- usw. Operatoren) zu verwenden, und ich konnte auch nicht die API-Suchoberfläche verwenden, um Ergebnisse zu filtern.

Ich habe Folgendes entdeckt nextapps-de/flexsearch: https://github.com/nextapps-de/flexsearch und ich glaube, es übertrifft bei weitem viele der anderen Javascript-Suchbibliotheken, die ich ausprobiert habe, und es unterstützt booldas Filtern von Suchanfragen und Paginierungen.

Sie können eine Liste von Javascript-Objekten für Ihre Suchdaten (dh Speicher) eingeben, und die API ist ziemlich gut dokumentiert: https://github.com/nextapps-de/flexsearch#api-overview

Bisher habe ich fast 10.000 Datensätze indiziert, und meine Suche erfolgt nahezu sofort. dh nicht wahrnehmbare Zeit für jede Suche.

David John Coleman II
quelle
2

Hier ist die Lösung von @InternalFX, aber in JS (ich habe es so geteilt):

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
}
Jaya
quelle
2

Ich habe die Probleme mit der CoffeeScript-Bigram-Lösung von InternalFx behoben und daraus eine generische n-Gramm-Lösung gemacht (Sie können die Größe der Gramme anpassen).

Dies ist TypeScript, aber Sie können die Typanmerkungen entfernen und es funktioniert auch gut als Vanille-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;
}

Beispiele:

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

Probieren Sie es auf dem TypeScript-Spielplatz aus

MgSam
quelle
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/

guest271314
quelle
0

Schauen Sie sich mein Google Sheets-Add-On namens Flookup an und verwenden Sie diese Funktion:

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

Die Parameterdetails sind:

  1. lookupValue: der Wert, den Sie suchen
  2. tableArray: die Tabelle, die Sie durchsuchen möchten
  3. lookupCol: die Spalte, die Sie suchen möchten
  4. indexNum: Die Spalte, aus der Daten zurückgegeben werden sollen
  5. threshold: Die prozentuale Ähnlichkeit, unterhalb derer Daten nicht zurückgegeben werden sollten
  6. rank: das n-te beste Spiel (dh wenn das erste Spiel nicht Ihren Wünschen entspricht)

Dies sollte Ihren Anforderungen entsprechen ... obwohl ich mir bei Punkt 2 nicht sicher bin.

Weitere Informationen finden Sie auf der offiziellen Website .


quelle