合理的JavaScript模糊搜索


98

我正在寻找一个模糊搜索JavaScript库来过滤数组。我尝试使用Fuzzyset.jsfuse.js,但是结果很糟糕(可以在链接的页面上尝试一些演示)。

在对Levenshtein距离进行了一些阅读之后,我对用户输入时所寻找的内容的近似性感到不满意。对于那些不知道的人,系统会计算要使两个字符串匹配,需要多少个插入删除替换

在Levenshtein-Demerau模型中修复的一个明显缺陷是,blubboob都被认为与bulb相似(都需要两次替换)。很明显,但是,灯泡更类似于咕噜布布是的,我刚才提到的模型识别到,允许换位

我想在文本补全的背景下使用它,因此,如果我有一个数组['international', 'splint', 'tinder'],并且我的查询是int,我认为International应该比splint排名更高,即使前者的得分(高=差)为10与后者的3。

因此,我正在寻找(并且如果不存在的话将创建)一个执行以下操作的库:

  • 权衡不同的文本操作
  • 根据每个单词在单词中出现的位置,对每个操作进行加权加权(较早的操作比较晚的操作成本更高)
  • 返回按相关性排序的结果列表

有没有人遇到过这样的事情?我意识到,StackOverflow并不是要求软件推荐的地方,但是上面的隐式(不再是!)是:我是否正在以正确的方式考虑?


编辑

我找到了一篇很好的论文(pdf)。一些注释和摘录:

仿射编辑距离功能为插入或删除序列分配了相对较低的开销

Monger-Elkan距离函数(Monge&Elkan 1996),它是Smith-Waterman距离函数(Durban et al。1998)的仿射变体,具有特定的成本参数

对于史密斯-沃特曼距离(Wikipedia),“史密斯-沃特曼算法无需查看总序列,而是比较所有可能长度的片段并优化相似性度量。” 这是n-gram方法。

大致上类似的度量标准(不是基于编辑距离模型)是Jaro度量标准(Jaro 1995; 1989; Winkler 1999)。在记录链接文献中,使用此方法的变体已获得了良好的结果,该变体基于两个字符串之间的公共字符的数量和顺序。

由于Winkler(1999)的不同,它也使用最长公共前缀的长度P

(似乎主要用于短字符串)

为了完成文本,Monger-Elkan和Jaro-Winkler方法似乎最有意义。Winkler对Jaro度量标准的添加有效地对单词的开头进行了更重的加权。而且,Monger-Elkan的仿射方面意味着完成一个单词的必要性(这只是一系列加法)不会太不利于它。

结论:

TFIDF排名在几个基于令牌的距离度量中表现最好,而Monge和Elkan提出的经过调整的仿射间隔编辑距离度量在几个字符串编辑距离度量中表现最好。令人惊讶的良好距离度量是Jaro提出并随后由Winkler扩展的快速启发式方案。这几乎和Monge-Elkan方案一样有效,但是速度要快一个数量级。结合TFIDF方法和Jaro-Winkler的一种简单方法是用基于Jaro-Winkler方案的近似令牌匹配替换TFIDF中使用的精确令牌匹配。平均而言,此组合的性能比Jaro-Winkler或TFIDF都要好,偶尔也要好得多。在性能上也接近本文所考虑的几种最佳指标的学习组合。


好问题。我正在寻找做类似的事情,但具有相同的字符串比较注意事项。您是否找到/构建了字符串比较的javascript实现?谢谢。
nicholas 2014年

1
@nicholas我只是在github上派生了Fuzzyset.js来说明较小的查询字符串,尽管它不考虑加权字符串操作,但结果对于我打算的字符串完成应用程序来说是相当不错的。查看回购协议
willlma 2014年

谢谢。我会试试看。我还发现了此字符串比较功能:github.com/zdyn/jaro-winkler-js。似乎也能很好地工作。
nicholas 2014年


1
@michaelday不会把错别字考虑在内。在演示中,输入krole不返回Final Fantasy V: Krile,尽管我希望返回。它要求查询中的所有字符在结果中以相同的顺序出现,这是很短视的。进行良好模糊搜索的唯一方法似乎是拥有一个常见错别字数据库。
willlma 2015年

Answers:


21

好问题!但是我的想法是,与其尝试修改Levenshtein-Demerau,不如尝试使用其他算法或对两种算法的结果进行合并/加权,可能会更好。

令我惊讶的是,Levenshtein-Demerau并没有特别重视与“起始前缀”的完全匹配或接近匹配-但您明显的用户期望会如此。

我搜索“比Levenshtein更好”,并且发现了以下内容:

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

这提到了许多“字符串距离”度量。看起来与您的需求特别相关的三个是:

  1. 最长公共子字符串距离:两个字符串中必须删除的最小符号数,直到得到的子字符串相同为止。

  2. q-gram距离:两个字符串的N-gram矢量之间的绝对差之和。

  3. 雅卡距离: 1分钟,表示共享的N-gram与所有观察到的N-gram的商。

也许您可以使用这些度量的加权组合(或最小值),与Levenshtein一起使用-常见的子字符串,常见的N-gram或Jaccard都将非常喜欢相似的字符串-或尝试仅使用Jaccard吗?

根据列表/数据库的大小,这些算法可能会适度昂贵。对于我实施的模糊搜索,我使用了可配置数量的N-gram作为数据库中的“检索关键字”,然后运行了昂贵的字符串距离度量以将它们按首选项顺序进行排序。

我写了一些有关SQL模糊字符串搜索的注释。看到:


63

我尝试使用像fuse.js这样的现有模糊库,也发现它们很糟糕,所以我写了一个基本上像sublime搜索一样的库。https://github.com/farzher/fuzzysort

它允许的唯一错字是移调。它非常可靠(1k星,0期)非常快,可以轻松处理您的案件:

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


4
我对Fuse.js不满意,并尝试了您的库-效果很好!做得好:)
dave

1
我遇到的这个库的唯一问题是单词完整但拼写错误时,例如,如果正确的单词是“ XRP”,并且如果我搜索“ XRT”,则它不会给我分数
PirateApp

1
@PirateApp是的,我不会处理拼写错误(因为sublime的搜索不会)。有人抱怨的时候,我现在正在研究这个问题。您可以为我提供示例使用案例,其中该搜索因github问题而失败
Farzher

3
对于那些想知道这个库的人来说,它现在也实现了拼写检查!我推荐这个lib而不是fusejs和其他文件
PirateApp '17

1
@ user4815162342您必须自己编写代码。检出此线程,它具有代码示例github.com/farzher/fuzzysort/issues/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将两个字符串传递给它们,它们之间将返回一个数字01.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 2015年

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); } return v; }函数string_likeity(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 <pairs1.length; x ++){for(var y = 0; y <pairs2.length; y ++){if(pairs1 [x] == pairs2 [y])hit_count ++; }} if(hits> 0)return((2.0 * hits)/ union); } return 0.0}
jaya

如何在要在多个键中进行搜索的对象中使用它?
user3808307

这有一些问题:1)减低了字符串开头和结尾的字符的权重。2)二元比较为O(n ^ 2)。3)由于实施,相似性评分可能会超过1。这显然是没有道理的。我在下面的答案中解决了所有这些问题。
6

8

这是我的模糊匹配的简短函数:

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

尽管在大多数情况下可能不是您想要的,但这正是我想要的。
schmijos


2

2019年11月更新。我发现保险丝有一些不错的升级。但是,我无法使用bool(即OR,AND等操作符),也无法使用API​​搜索界面来过滤结果。

我发现了nextapps-de/flexsearchhttps : //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

我通过InternalFx修复了CoffeeScript bigram解决方案的问题,并使其成为通用的n-gram解决方案(您可以自定义克的大小)。

这是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点。

官方网站上找到更多信息

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.