Mantıklı Javascript bulanık arama


104

Bir diziyi filtrelemek için bulanık arama JavaScript kitaplığı arıyorum. Fuzzyset.js ve fuse.js kullanmayı denedim , ancak sonuçlar korkunç (bağlantılı sayfalarda deneyebileceğiniz demolar var).

Levenshtein mesafesi hakkında biraz okuma yaptıktan sonra, kullanıcıların yazarken ne aradıklarına dair zayıf bir yaklaşım olarak bana çarpıcı geliyor. Bilmeyenler için, sistem iki dizeyi eşleştirmek için kaç ekleme , silme ve değişiklik gerektiğini hesaplar .

Levenshtein-Demerau modeli sabitlenmiştir bariz bir kusur, hem olmasıdır blub ve memeyi eşit benzer kabul edilir ampul (her iki sübstitüsyona gerektiren). Bununla birlikte, ampülün , boob'dan daha çok blub'a benzediği açıktır ve az önce bahsettiğim model, transpozisyonlara izin vererek bunu kabul ediyor .

Ben bir dizi var eğer öyleyse, metin tamamlama bağlamında bu kullanmak istiyorum ['international', 'splint', 'tinder']ve benim sorgu int , bence uluslararası daha çok daha rütbe gerektiğini splint eski 10 puan (daha yüksek = kötü) olsa bile, ikincisine karşı 3.

Yani aradığım şey (ve yoksa yaratacağım), aşağıdakileri yapan bir kitaplık:

  • Farklı metin işlemlerini ağırlıklandırır
  • Her bir manipülasyonu, bir kelimede nerede göründüklerine bağlı olarak farklı şekilde ağırlıklandırır (erken manipülasyonlar, geç manipülasyonlardan daha maliyetlidir)
  • Alaka düzeyine göre sıralanmış bir sonuç listesi verir

Böyle bir şeye rastlayan oldu mu? StackOverflow'un yazılım önerileri istemek için bir yer olmadığının farkındayım, ancak yukarıdakiler üstü kapalı (artık değil!): Bunu doğru şekilde mi düşünüyorum?


Düzenle

Konuyla ilgili iyi bir makale (pdf) buldum . Bazı notlar ve alıntılar:

Afin düzenleme mesafesi fonksiyonları, bir dizi ekleme veya silme işlemine nispeten daha düşük bir maliyet atar

Smith-Waterman mesafe fonksiyonunun (Durban ve diğerleri 1998) belirli maliyet parametreleri ile afin bir varyantı olan Monger-Elkan mesafe fonksiyonu (Monge & Elkan 1996)

İçin Smith-Waterman mesafe (Ara) , "yerine toplam dizisinin bakarak, Smith-Waterman algoritması tüm olası uzunlukları segmentlerini karşılaştırır ve benzerlik ölçüsü optimize eder". Bu n-gram yaklaşımıdır.

Bir düzenleme mesafesi modeline dayanmayan geniş ölçüde benzer bir ölçü Jaro ölçüsüdür (Jaro 1995; 1989; Winkler 1999). Kayıt-bağlantı literatüründe, iki dizge arasındaki ortak karakterlerin sayısına ve sırasına dayanan bu yöntemin varyantları kullanılarak iyi sonuçlar elde edilmiştir.

Bunun Winkler'e (1999) bağlı bir varyantı da en uzun ortak ön ekin uzunluğunu P kullanır.

(öncelikle kısa dizeler için tasarlanmış gibi görünüyor)

Metin tamamlama amaçları için, Monger-Elkan ve Jaro-Winkler yaklaşımları en mantıklı gibi görünüyor. Winkler'in Jaro metriğine eklenmesi, kelimelerin başlangıçlarını daha ağır bir şekilde ağırlıklandırıyor. Ve Monger-Elkan'ın afin yönü, bir kelimeyi tamamlama gerekliliğinin (sadece bir dizi eklemeden ibarettir) onu çok fazla olumsuz etkilemeyeceği anlamına gelir.

Sonuç:

TFIDF sıralaması, çeşitli belirteç tabanlı mesafe ölçütleri arasında en iyi performansı gösterdi ve Monge ve Elkan tarafından önerilen ayarlanmış bir afin-boşluk düzenleme-mesafe ölçüsü, birkaç dizi düzenleme-mesafe ölçütü arasında en iyi performansı gösterdi. Şaşırtıcı derecede iyi bir mesafe metriği, Jaro tarafından önerilen ve daha sonra Winkler tarafından genişletilen hızlı bir sezgisel şemadır. Bu, neredeyse Monge-Elkan şeması kadar iyi çalışır, ancak bir kat daha hızlıdır. TFIDF yöntemini ve Jaro-Winkler'ı birleştirmenin basit bir yolu, TFIDF'de kullanılan tam jeton eşleşmelerini Jaro-Winkler şemasına dayalı yaklaşık jeton eşleşmeleriyle değiştirmektir. Bu kombinasyon, ortalama olarak Jaro-Winkler veya TFIDF'den biraz daha iyi performans gösterir ve bazen çok daha iyi performans gösterir. Ayrıca performans açısından, bu makalede ele alınan en iyi ölçümlerin birkaçının öğrenilmiş bir kombinasyonuna yakındır.


Harika soru. Benzer bir şey yapmak istiyorum, ancak aynı dize karşılaştırması dikkate alınarak. Dize karşılaştırmalarınızın bir javascript uygulamasını buldunuz / oluşturdunuz mu? Teşekkürler.
Nicholas

1
@nicholas, daha küçük sorgu dizelerini hesaba katmak için github üzerinde fuzzyset.js'yi çatalladım ve ağırlıklı dize işlemlerini hesaba katmasa da, sonuçlar benim amaçlanan dize tamamlama uygulaması için oldukça iyi. Bkz repo
willlma

Teşekkürler. Deneyeceğim. Ayrıca bu dize karşılaştırma işlevini buldum: github.com/zdyn/jaro-winkler-js . Oldukça iyi çalışıyor gibi görünüyor.
nicholas


1
@michaelday Bu yazım hatalarını hesaba katmaz. Demoda, istediğim halde yazmak krolegeri dönmüyor Final Fantasy V: Krile. Sorgudaki tüm karakterlerin sonuçta aynı sırada bulunmasını gerektirir ki bu oldukça dar görüşlüdür. İyi bir bulanık arama yapmanın tek yolu, yaygın yazım hatalarından oluşan bir veritabanına sahip olmaktır.
willlma

Yanıtlar:


22

İyi soru! Ama benim düşüncem, Levenshtein-Demerau'yu değiştirmeye çalışmak yerine, farklı bir algoritma denemek veya iki algoritmadan elde edilen sonuçları birleştirmek / ağırlıklandırmak daha iyi olabilir.

"Başlangıç ​​öneki" ile tam veya yakın eşleşmelerin Levenshtein-Demerau'nun belirli bir ağırlık vermediği bir şey olması beni şaşırtıyor - ancak görünen kullanıcı beklentileriniz bunu yapacaktır.

"Levenshtein'den daha iyi" aradım ve diğer şeylerin yanı sıra şunu buldum:

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

Bu, bir dizi "dizi mesafesi" ölçüsünden bahseder. İhtiyaçlarınıza özellikle uygun görünen üç tanesi şunlar olacaktır:

  1. En Uzun Ortak Alt Dize mesafesi: Ortaya çıkan alt dizeler aynı olana kadar her iki dizide de kaldırılması gereken minimum simge sayısı.

  2. q-gram mesafesi: Her iki dizgenin N-gram vektörleri arasındaki mutlak farkların toplamı.

  3. Jaccard mesafesi: Paylaşılan N-gram ve tüm gözlemlenen N-gramların oranını 1 dakika sürer.

Belki bu ölçütlerin ağırlıklı bir kombinasyonunu (veya minimumunu) Levenshtein ile kullanabilirsiniz - ortak alt dize, ortak N-gram veya Jaccard, benzer dizeleri şiddetle tercih eder - veya belki de sadece Jaccard kullanmayı deneyebilirsiniz?

Listenizin / veritabanınızın boyutuna bağlı olarak, bu algoritmalar orta derecede pahalı olabilir. Gerçekleştirdiğim bulanık bir arama için, DB'den "alma anahtarları" olarak yapılandırılabilir sayıda N-gram kullandım, ardından bunları tercih sırasına göre sıralamak için pahalı dizi-mesafe ölçüsünü çalıştırdım.

SQL'de Fuzzy String Search üzerine bazı notlar yazdım. Görmek:


68

Fuse.js gibi mevcut bulanık kitaplıkları kullanmayı denedim ve aynı zamanda onları korkunç buldum, bu yüzden temelde sublime'ın araması gibi davranan bir kitap yazdım. https://github.com/farzher/fuzzysort

İzin verdiği tek yazım hatası, devriktir. Oldukça sağlam (1k yıldız, 0 sayı) , çok hızlı ve durumunuzu kolayca ele alıyor:

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


4
Fuse.js'den memnun kalmadım ve kitaplığınızı denedim - harika çalışıyor! Aferin :)
dave

1
Bu kütüphaneyle ilgili karşılaştığım tek sorun, kelime tamamlandığında ancak yanlış yazıldığında, örneğin doğru kelime "XRP" ise ve "XRT"
aradıysam

1
@PirateApp evet, yazım hatalarını işlemem (çünkü sublime'ın araması yapmaz). İnsanlar şikayetçi olduklarına göre şimdi buna bakıyorum. Bana bu aramanın bir github sorunu olarak başarısız olduğu örnek kullanım durumlarını sağlayabilirsiniz
Farzher

3
Bu kitaplığı merak edenler için artık yazım denetimi de uygulandı! Bu kütüphaneyi fusejs ve diğerlerine tavsiye ederim
PirateApp

1
@ user4815162342 bunu kendiniz kodlamalısınız. bu konuyu kontrol edin, bir kod örneği var github.com/farzher/fuzzysort/issues/19
Farzher

18

İşte birkaç kez kullandığım bir teknik ... Oldukça iyi sonuçlar veriyor. Yine de istediğin her şeyi yapmıyor. Ayrıca, liste çok büyükse bu pahalı olabilir.

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_similarityAralarında 0ve 1.0ne kadar benzer olduklarına bağlı olarak bir sayı döndürecek iki dize geçirin . Bu örnek Lo-Dash kullanıyor

Kullanım Örneği ....

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

Ayrıca .... keman çal

Konsolunuzun açık olduğundan emin olun yoksa hiçbir şey görmeyeceksiniz :)


3
Teşekkürler, tam olarak aradığım buydu. Sadece düz js olsaydı daha iyi olurdu;)
lucaswxp

1
function get_bigrams (dize) {var s = string.toLowerCase () var v = s.split (''); for (var i = 0; i <v.length; i ++) {v [i] = dilim (i, i + 2); } dönüş v; } işlev string_similarity (str1, str2) {if (str1.length> 0 && str2.length> 0) {var pairs1 = get_bigrams (str1); var çiftler2 = get_bigrams (str2); var union = çiftler1.length + çiftler2.length; var hits = 0; for (var x = 0; x <çiftler1.length; x ++) {for (var y = 0; y <çiftler2.length; y ++) {if (çiftler1 [x] == çiftler2 [y]) hit_count ++; }} if (hits> 0) return ((2.0 * hits) / union); } return 0.0}
jaya

Bunu, birkaç tuşta aramak isteyeceğiniz nesnelerde nasıl kullanabilirsiniz?
user3808307

Bunun birkaç sorunu vardır: 1) Dizenin başındaki ve sonundaki karakterlerin ağırlığını azaltır. 2) Bigram karşılaştırmaları O (n ^ 2). 3) Benzerlik puanı uygulama nedeniyle 1'in üzerinde olabilir. Bu açıkça hiç mantıklı değil. Aşağıdaki cevabımda tüm bu sorunları düzelttim.
MgSam

9

bu benim bulanık eşleşme için kısa ve kompakt işlevim:

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

Muhtemelen çoğu durumda istediğiniz şey olmasa da, tam olarak benim içindi.
schmijos

Emri görmezden gelebilir misin? fuzzyMatch('c a', 'a b c')dönmelidirtrue
vsync


2

Kasım 2019 Güncellemesi. Oldukça iyi yükseltmeler için sigorta buldum. Ancak, bool'leri (yani OR, AND, vb. Operatörleri) kullanmasını sağlayamadım ve sonuçları filtrelemek için API arama arayüzünü kullanamadım.

Şunu keşfettim nextapps-de/flexsearch: https://github.com/nextapps-de/flexsearch ve denediğim diğer birçok javascript arama kitaplığını açık ara geride bıraktığına ve boolaramaları filtreleme ve sayfalandırma desteğine sahip olduğuna inanıyorum .

Arama verileriniz (yani depolama) için bir javascript nesneleri listesi girebilirsiniz ve API oldukça iyi belgelenmiştir: https://github.com/nextapps-de/flexsearch#api-overview

Şimdiye kadar 10.000'e yakın kaydı indeksledim ve aramalarım hemen yanında; yani her arama için fark edilemeyen süre.


Bu proje şişirilmiş ( > 100kb) ve çok sayıda katılımsız sorun ve PR var. Bunu bu iki nedenden dolayı kullanmazdım.
vsync

2

İşte @InternalFX tarafından sağlanan çözüm, ancak JS'de (bunu paylaşarak kullandım):

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 bigram çözümüyle ilgili sorunları InternalFx ile düzelttim ve onu genel bir n-gram çözümü yaptım (gramın boyutunu özelleştirebilirsiniz).

Bu TypeScript'tir, ancak tür ek açıklamalarını kaldırabilirsiniz ve vanilya JavaScript olarak da iyi çalışır.

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

Örnekler:

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 Playground'da deneyin


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

Flookup adlı Google E-Tablolar eklentime bakın ve şu işlevi kullanın:

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

Parametre ayrıntıları şunlardır:

  1. lookupValue: aradığınız değer
  2. tableArray: aramak istediğiniz tablo
  3. lookupCol: aramak istediğiniz sütun
  4. indexNum: verilerin döndürülmesini istediğiniz sütun
  5. threshold: altındaki verilerin döndürülmemesi gereken benzerlik yüzdesi
  6. rank: n'inci en iyi eşleşme (yani, ilk eşleşme beğeninize göre değilse)

Bu, ihtiyaçlarınızı karşılamalıdır ... 2 numaralı noktadan emin olmasam da.

Resmi web sitesinde daha fazlasını öğrenin .


-1

Bulanık Sıralama, bir javascript kitaplığıdır, geniş bir veri koleksiyonundan dize eşlemesi yapmak için yararlıdır.

Aşağıdaki kod, react.js'de bulanık sıralamanın kullanılmasına yardımcı olacaktır.

  1. npm aracılığıyla bulanık sıralamayı yükleyin,

    npm install fuzzysort
    
  2. Bir referans değişken yapın,

    const fuzzysort = require('fuzzysort')
    
  3. Eşleşen dizeleri bulmak için go () yöntemini kullanın

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

React.js'de tam demo kodu

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;

Daha fazla bilgi için FuzzySort'a bakın


Bu sadece orijinal kitaplığın tam kopyası: github.com/farzher/fuzzysort
Vladan

Depomu kontrol etmedin. Burada react'te fuzzysort paketini kullandım. React'te bu fuzzysort'u entegre etmek için varsayılan bir çözüm yoktur.
Codemaker
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.