Ricerca sfocata Javascript che abbia senso


98

Sto cercando una libreria JavaScript di ricerca fuzzy per filtrare un array. Ho provato a usare fuzzyset.js e fuse.js , ma i risultati sono pessimi (ci sono demo che puoi provare sulle pagine collegate).

Dopo aver letto alcune letture a distanza di Levenshtein, mi sembra una scarsa approssimazione di ciò che gli utenti cercano quando digitano. Per chi non lo sapesse, il sistema calcola quanti inserimenti , cancellazioni e sostituzioni sono necessari per far corrispondere due stringhe.

Un ovvio difetto, che è stato risolto nel modello Levenshtein-Demerau, è che sia il blub che il boob sono considerati ugualmente simili al bulbo (ciascuno richiede due sostituzioni). È chiaro, tuttavia, che la lampadina è più simile al blub che al boob , e il modello che ho appena citato lo riconosce consentendo le trasposizioni .

Voglio usarlo nel contesto del completamento del testo, quindi se ho un array ['international', 'splint', 'tinder']e la mia query è int , penso che international dovrebbe classificarsi più in alto di splint , anche se il primo ha un punteggio (più alto = peggiore) di 10 rispetto a quest'ultimo 3.

Quindi quello che sto cercando (e creerò se non esiste), è una libreria che faccia quanto segue:

  • Pesa le diverse manipolazioni del testo
  • Pesa ogni manipolazione in modo diverso a seconda di dove compaiono in una parola (le prime manipolazioni sono più costose delle ultime)
  • Restituisce un elenco di risultati ordinati per pertinenza

Qualcuno si è imbattuto in qualcosa di simile? Mi rendo conto che StackOverflow non è il posto giusto per chiedere consigli sul software, ma implicito (non più!) In quanto sopra è: sto pensando a questo nel modo giusto?


modificare

Ho trovato un buon documento (pdf) sull'argomento. Alcune note ed estratti:

Le funzioni di distanza di modifica affine assegnano un costo relativamente inferiore a una sequenza di inserimenti o eliminazioni

la funzione distanza Monger-Elkan (Monge & Elkan 1996), che è una variante affine della funzione distanza Smith-Waterman (Durban et al.1998) con parametri di costo particolari

Per la distanza Smith-Waterman (wikipedia) , "Invece di guardare la sequenza totale, l'algoritmo Smith-Waterman confronta segmenti di tutte le lunghezze possibili e ottimizza la misura di somiglianza". È l'approccio n-gram.

Una metrica sostanzialmente simile, che non si basa su un modello di distanza di modifica, è la metrica Jaro (Jaro 1995; 1989; Winkler 1999). Nella letteratura record-linkage, sono stati ottenuti buoni risultati utilizzando varianti di questo metodo, che si basa sul numero e sull'ordine dei caratteri comuni tra due stringhe.

Una variante di ciò dovuta a Winkler (1999) utilizza anche la lunghezza P del prefisso comune più lungo

(sembra essere inteso principalmente per stringhe brevi)

Ai fini del completamento del testo, gli approcci Monger-Elkan e Jaro-Winkler sembrano avere più senso. L'aggiunta di Winkler alla metrica Jaro appesantisce efficacemente l'inizio delle parole in modo più pesante. E l'aspetto affine di Monger-Elkan significa che la necessità di completare una parola (che è semplicemente una sequenza di aggiunte) non la sfavorirà troppo pesantemente.

Conclusione:

la classifica TFIDF si è comportata meglio tra diverse metriche di distanza basate su token e una metrica di distanza di modifica affine-gap ottimizzata proposta da Monge ed Elkan ha funzionato meglio tra diverse metriche di distanza di modifica di stringa. Una metrica della distanza sorprendentemente buona è uno schema euristico veloce, proposto da Jaro e successivamente esteso da Winkler. Funziona quasi come lo schema Monge-Elkan, ma è un ordine di grandezza più veloce. Un modo semplice per combinare il metodo TFIDF e Jaro-Winkler è sostituire le corrispondenze di token esatte utilizzate in TFIDF con corrispondenze di token approssimative basate sullo schema Jaro-Winkler. Questa combinazione ha prestazioni leggermente migliori rispetto a Jaro-Winkler o TFIDF in media e occasionalmente funziona molto meglio. È anche vicino in termini di prestazioni a una combinazione appresa di molte delle migliori metriche considerate in questo documento.


Ottima domanda. Sto cercando di fare qualcosa di simile, ma con le stesse considerazioni sul confronto delle stringhe. Hai mai trovato / costruito un'implementazione javascript dei tuoi confronti di stringhe? Grazie.
nicholas

1
@nicholas Ho semplicemente biforcato fuzzyset.js su GitHub per tenere conto di stringhe di query più piccole e, sebbene non tenga conto delle manipolazioni di stringhe ponderate, i risultati sono abbastanza buoni per la mia applicazione prevista per il completamento delle stringhe. Vedi il repo
willlma

Grazie. Lo proverò. Ho anche trovato questa funzione di confronto delle stringhe: github.com/zdyn/jaro-winkler-js . Sembra funzionare anche abbastanza bene.
nicholas


1
@michaelday Questo non tiene conto degli errori di battitura. Nella demo, la digitazione krolenon viene restituita Final Fantasy V: Krile, anche se mi piacerebbe. Richiede che tutti i caratteri della query siano presenti nello stesso ordine nel risultato, il che è piuttosto miope. Sembra che l'unico modo per avere una buona ricerca fuzzy sia avere un database di errori di battitura comuni.
willlma

Risposte:


21

Buona domanda! Ma il mio pensiero è che, piuttosto che provare a modificare Levenshtein-Demerau, potresti essere meglio provare un algoritmo diverso o combinare / pesare i risultati di due algoritmi.

Mi colpisce il fatto che le corrispondenze esatte o vicine al "prefisso iniziale" siano qualcosa a cui Levenshtein-Demerau non attribuisce un peso particolare, ma le tue apparenti aspettative degli utenti lo farebbero.

Ho cercato "meglio di Levenshtein" e, tra le altre cose, ho trovato questo:

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

Questo menziona una serie di misure di "distanza tra le corde". Tre che sembravano particolarmente rilevanti per le tue esigenze, sarebbero:

  1. Distanza sottostringa comune più lunga: numero minimo di simboli che devono essere rimossi in entrambe le stringhe finché le sottostringhe risultanti non sono identiche.

  2. Distanza q-gram: somma delle differenze assolute tra i vettori N-gram di entrambe le stringhe.

  3. Distanza di Jaccard: 1 minues il quoziente di N-grammi condivisi e tutti gli N-grammi osservati.

Forse potresti usare una combinazione ponderata (o minima) di queste metriche, con Levenshtein - sottostringa comune, N-gram comune o Jaccard preferiranno fortemente stringhe simili - o forse provare a usare solo Jaccard?

A seconda delle dimensioni del tuo elenco / database, questi algoritmi possono essere moderatamente costosi. Per una ricerca fuzzy che ho implementato, ho usato un numero configurabile di N-grammi come "chiavi di recupero" dal DB, quindi ho eseguito la costosa misura della distanza tra le stringhe per ordinarli in ordine di preferenza.

Ho scritto alcune note sulla ricerca di stringhe fuzzy in SQL. Vedere:


63

Ho provato a utilizzare le librerie fuzzy esistenti come fuse.js e le ho trovate anche terribili, quindi ne ho scritta una che si comporta sostanzialmente come la ricerca di sublime. https://github.com/farzher/fuzzysort

L'unico errore di battitura che consente è una trasposizione. È abbastanza solido (1k stelle, 0 problemi) , molto veloce e gestisce facilmente il tuo caso:

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


4
Non ero soddisfatto di Fuse.js e ho provato la tua libreria: funziona alla grande! Ben fatto :)
dave

1
L'unico problema con questa libreria che ho dovuto affrontare è stato quando la parola è completa ma scritta in modo errato, ad esempio, se la parola corretta era "XRP" e se ho cercato "XRT" non mi dà un punteggio
PirateApp

1
@PirateApp sì, non gestisco gli errori di ortografia (perché la ricerca di sublime no). Sto cercando di approfondire questo aspetto ora che le persone si lamentano. puoi fornirmi esempi di casi d'uso in cui questa ricerca non riesce come problema di GitHub
Farzher

3
Per quelli di voi che si stanno chiedendo di questa libreria, ora ha implementato anche il controllo ortografico! Raccomando questa libreria a fusejs e altri
PirateApp

1
@ user4815162342 devi codificarlo tu stesso. controlla questo thread, ha un esempio di codice github.com/farzher/fuzzysort/issues/19
Farzher

19

Ecco una tecnica che ho usato alcune volte ... Dà risultati piuttosto buoni. Tuttavia, non fa tutto ciò che hai chiesto. Inoltre, questo può essere costoso se l'elenco è enorme.

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

Passa due stringhe a string_similaritycui restituirà un numero compreso tra 0e a 1.0seconda di quanto sono simili. Questo esempio utilizza Lo-Dash

Esempio di utilizzo ...

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

Inoltre .... avere un violino

Assicurati che la tua console sia aperta o non vedrai nulla :)


3
Grazie, è esattamente quello che stavo cercando. Sarebbe solo meglio se fosse semplice js;)
lucaswxp

1
funzione 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; } funzione string_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 hits = 0; for (var x = 0; x <pair1.length; x ++) {for (var y = 0; y <pair2.length; y ++) {if (pair1 [x] == pair2 [y]) hit_count ++; }} if (hits> 0) return ((2.0 * hits) / union); } return 0.0}
jaya

Come utilizzarlo negli oggetti in cui desideri cercare in più chiavi?
user3808307

Questo ha alcuni problemi: 1) Sottopesa i caratteri all'inizio e alla fine della stringa. 2) I confronti di bigram sono O (n ^ 2). 3) Il punteggio di somiglianza può essere superiore a 1 a causa dell'implementazione. Questo ovviamente non ha senso. Risolvo tutti questi problemi nella mia risposta di seguito.
MgSam

8

questa è la mia funzione breve e compatta per la corrispondenza fuzzy:

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

Anche se probabilmente non è quello che vuoi nella maggior parte dei casi, lo era esattamente per me.
schmijos


2

Aggiornamento di novembre 2019. Ho scoperto che il fusibile ha degli aggiornamenti abbastanza decenti. Tuttavia, non sono riuscito a convincerlo a utilizzare gli operatori bool (cioè OR, AND, ecc.) Né ho potuto utilizzare l'interfaccia di ricerca API per filtrare i risultati.

Ho scoperto nextapps-de/flexsearch: https://github.com/nextapps-de/flexsearch e credo che superi di gran lunga molte delle altre librerie di ricerca javascript che ho provato, e ha il supporto bool, filtraggio di ricerche e impaginazione.

Puoi inserire un elenco di oggetti javascript per i tuoi dati di ricerca (cioè archiviazione) e l'API è abbastanza ben documentata: https://github.com/nextapps-de/flexsearch#api-overview

Finora ho indicizzato quasi 10.000 record e le mie ricerche sono prossime a quelle immediate; cioè quantità di tempo impercettibile per ogni ricerca.


2

ecco la soluzione fornita da @InternalFX, ma in JS (l'ho usata così in condivisione):

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

Ho risolto i problemi con la soluzione bigram CoffeeScript di InternalFx e l'ho resa una soluzione generica di n-grammi (puoi personalizzare la dimensione dei grammi).

Questo è TypeScript ma puoi rimuovere le annotazioni del tipo e funziona bene anche come JavaScript vanilla.

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

Esempi:

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

Provalo nel campo giochi 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

Controlla il mio componente aggiuntivo di Fogli Google chiamato Flookup e usa questa funzione:

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

I dettagli del parametro sono:

  1. lookupValue: il valore che stai cercando
  2. tableArray: la tabella che desideri cercare
  3. lookupCol: la colonna che desideri cercare
  4. indexNum: la colonna da cui si desidera che vengano restituiti i dati
  5. threshold: la percentuale di somiglianza al di sotto della quale i dati non devono essere restituiti
  6. rank: l'ennesima corrispondenza migliore (ovvero se la prima corrispondenza non è di tuo gradimento)

Questo dovrebbe soddisfare le tue esigenze ... anche se non sono sicuro del punto numero 2.

Scopri di più sul sito ufficiale .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.