有意义的Javascript模糊搜索

时间:2014-04-26 00:11:34

标签: javascript regex pattern-matching string-matching fuzzy-search

我正在寻找一个模糊搜索JavaScript库来过滤数组。我已尝试使用fuzzyset.jsfuse.js,但结果非常糟糕(您可以尝试在链接网页上进行演示)。

在对Levenshtein距离进行一些阅读之后,它对我来说是一个很糟糕的近似用户在打字时所寻找的东西。对于那些不知道的人,系统会计算需要多少次插入删除替换才能使两个字符串匹配。

在Levenshtein-Demerau模型中修复的一个明显缺陷是 blub boob 被认为与 bulb 同样相似(每个需要两次替换)。但很明显,灯泡 blub 更相似,而不是 boob ,我刚才提到的模型通过允许换位

我想在文本完成的上下文中使用它,所以如果我有一个数组['international', 'splint', 'tinder'],而我的查询是 int ,我认为 international 应该比 splint 排名更高,即使前者的得分(更高=更差)为10而后者为3。

所以我正在寻找(并且如果它不存在则会创建),是一个执行以下操作的库:

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

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


修改

我在这个问题上找到了good paper (pdf)。一些注释和摘录:

  

仿射编辑距离函数为插入或删除序列分配相对较低的成本

     

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

对于Smith-Waterman distance (wikipedia),“Smith-Waterman算法不是查看总序列,而是比较所有可能长度的片段,并优化相似性度量。”这是n-gram方法。

  

一个大致相似的指标,不是基于编辑距离模型,而是   Jaro metric(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将替换使用的确切令牌匹配   TFIDF具有基于Jaro的近似令牌匹配   温克勒计划。这种组合平均比Jaro-Winkler或TFIDF略好,并且偶尔表现得更好。它与几个最佳指标的学习组合在性能上也很接近   在本文中考虑过。

12 个答案:

答案 0 :(得分:31)

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

它允许的唯一错字是转置。它非常可靠(1k星,0期)非常快,并且可以轻松处理您的案例:

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

答案 1 :(得分:18)

好问题!但我的想法是,不是试图修改Levenshtein-Demerau,你可能最好尝试不同的算法,或者将两种算法的结果组合/加权。

令我感到震惊的是,对于“起始前缀”的精确或接近匹配是Levenshtein-Demerau没有给予特别重视的东西 - 但是你明显的用户期望会。

我搜索了“比Levenshtein更好”,除其他外,发现了这个:

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

这提到了一些“字符串距离”措施。与您的要求特别相关的三个是:

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

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

  3. Jaccard距离: 1分摊N-gram和所有观察到的N-gram的商数。

  4. 也许你可以使用这些指标的加权组合(或最小值),Levenshtein - 普通子串,普通N-gram或Jaccard都非常喜欢类似的字符串 - 或者也许只是尝试使用Jaccard?

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

    我在SQL中写了一些关于模糊字符串搜索的注释。参见:

答案 2 :(得分:14)

这是我用了几次的技术......它给出了非常好的结果。不会做你要求的一切。此外,如果列表很大,这可能会很昂贵。

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

另外......有一个fiddle

确保您的控制台已打开或您看不到任何内容:)

答案 3 :(得分:6)

你可以看看Atom的https://github.com/atom/fuzzaldrin/ lib。

它在npm上可用,具有简单的API,并且对我来说没问题。

> fuzzaldrin.filter(['international', 'splint', 'tinder'], 'int');
< ["international", "splint"]

答案 4 :(得分:2)

这是@InternalFX提供的解决方案,但是在JS中(我使用它是为了共享):

for(i : {1,2,3}) {
   cout << i << "\n";
}

答案 5 :(得分:2)

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

这是TypeScript,但是您可以删除类型注释,它也可以像普通JavaScript一样正常工作。

declare @sql varchar(8000), @varch varchar(100)
set @varch = format(GETDATE(),'MMddyyyyhhmmtt')

set @sql = 'master..xp_cmdshell ''sqlcmd -E -s"~" -W -Q "set NoCount On select PirTextValue from SSIB.dbo.ReoHdrFlatFile where CompanyId=179 union all Select PirTextValue from SSIB.dbo.ReoFlatFile " | findstr /V /C:"-" /B > D:\4.ReoProject\SolidFoundation\FoundationPIR\PIRTXTFile.txt'''    

print @sql
exec(@sql)

示例:

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

Try it in the TypeScript Playground

答案 6 :(得分:1)

(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/

答案 7 :(得分:1)

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

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

答案 8 :(得分:1)

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

我发现了 nextapps-de/flexsearch https://github.com/nextapps-de/flexsearch,我相信它远远超过了我尝试过的许多其他javascript搜索库,并且它支持{{ 1}},过滤搜索和分页。

您可以为搜索数据(即存储空间)输入一个javascript对象列表,并且该API的文档记录得很好:https://github.com/nextapps-de/flexsearch#api-overview

到目前为止,我已为近10,000条记录建立索引,而我的搜索几乎是立即进行的;即每次搜索所需的时间不明显。

答案 9 :(得分:0)

查看我的Google表格加载项Flookup并使用此功能:

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

参数详细信息为:

  1. lookupValue:您要查找的值
  2. tableArray:您要搜索的表
  3. lookupCol:您要搜索的列
  4. indexNum:要从中返回数据的列
  5. threshold:相似度百分比,低于该百分比时不应返回数据
  6. rank:第n个最佳匹配项(即,如果您不喜欢第一个匹配项)

这确实可以满足您的要求...尽管我不确定第2点。

official website处了解更多信息。

答案 10 :(得分:0)

我已经爱上模糊匹配多年了,并且刚遇到这个线程。这里的谈话比大多数人更深入杂草,并且看起来涉及实施者。多年来,我已经用不同的语言编写了几个这样的算法,并希望将一些技巧传递给编写 JS 版本的任何人:

Monge-Elkan 规则!

这真是太棒了,结合了 n-gram 的许多优点和最好的短字符串比较算法,例如 Jaro-Winkler。 (这就是我在我的 Monge-Elkan 代码中使用的。)几年前,我偶然发现了一篇论文,你可以在网上找到一份名为 Generalized Mongue-Elkan Method for Approximate Text String Comparison.结论是,不要使用算术平均值,而是使用二次平均值。我试用了它,它在搜索结果方面取得了显着改进,涵盖范围广泛的文本。

N-Grams 规则!

在各种源语言和文本类型中具有非常强大的高质量性能。如果您正在查看数据库,则可以在 Postgres 中将其实现为高质量、闪电般快速的索引 K-NN 搜索。它需要适当地排列几个不同的功能,但还不错。

无论如何,在拆分 n-gram 时,有不同的方法来处理前端填充。就像,如果你有一个传统的 nqk)3,那么你会像这样拆分“ander”吗 >

'  a'
' an'
'and'
'nde'
'der'
'er '
'r  '

'  a'
' an'
'and'
'nde'
'der'

'and'
'nde'
'der'

本能地,我一直希望第一个列表效果最好,但实际上,它可能是第二个或第三个。值得尝试填充和窗口规则,并查看它们在您的上下文中的表现。很少有库提供对这种行为的控制,这将是一个很好的支持特性。提示。

答案 11 :(得分:-1)

模糊排序是一个JavaScript库,有助于从大量数据中进行字符串匹配。

以下代码将有助于在react.js中使用模糊排序。

  1. 通过npm安装模糊排序,

        std::unique_ptr<base> dv{new derived()};
    
  2. 设置参考变量,

    npm install fuzzysort
    
  3. 使用go()方法查找匹配的字符串

    const fuzzysort = require('fuzzysort')
    

react.js中的完整演示代码

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

有关更多信息,请参见FuzzySort