有意义的Javascript模糊搜索

IT技术 javascript regex pattern-matching string-matching fuzzy-search
2021-01-25 04:33:32

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

在对 Levenshtein distance 进行了一些阅读之后,我觉得它对用户在键入时正在寻找的内容的近似值很差。对于那些不知道的人,系统会计算需要多少次插入删除替换才能使两个字符串匹配。

Levenshtein-Demerau 模型中修复的一个明显缺陷是,blubboob都被认为与bulb相似(每个都需要两次替换)。然而,很明显,bulbboob更类似于blub,我刚才提到的模型通过允许换位来识别这一点

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

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

  • 加权不同的文本操作
  • 根据它们在单词中出现的位置对每个操作赋予不同的权重(早期操作比后期操作成本更高)
  • 返回按相关性排序的结果列表

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


编辑

我找到了一篇关于这个主题好论文 (pdf)一些笔记和摘录:

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

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

对于Smith-Waterman 距离(维基百科),“Smith-Waterman 算法不是查看整个序列,而是比较所有可能长度的片段并优化相似性度量。” 这是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,有时性能要好得多。它的性能也接近于本文中考虑的几个最佳指标的学习组合。

6个回答

我尝试使用现有的模糊库,如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}]

我面临的这个库的唯一问题是当单词完整但拼写错误时,例如,如果正确的单词是“XRP”,如果我搜索“XRT”,它不会给我分数
2021-03-19 04:33:32
@user4815162342 你必须自己编码。签出这个线程,它有一个代码示例github.com/farzher/fuzzysort/issues/19
2021-04-05 04:33:32
对于那些想知道这个库的人,它现在也实现了拼写检查!我推荐这个库而不是 fusejs 和其他人
2021-04-06 04:33:32
我对 Fuse.js 不满意并尝试了您的库 - 效果很好!做得好 :)
2021-04-10 04:33:32
@PirateApp 是的,我不处理拼写错误(因为 sublime 的搜索不处理)。现在人们在抱怨,我正在调查这个。您可以向我提供此搜索因 github 问题而失败的示例用例
2021-04-12 04:33:32

好问题!但我的想法是,与其尝试修改 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 的商。

也许您可以使用这些指标的加权组合(或最小值),以及 Levenshtein——共同子串、共同 N-gram 或 Jaccard 都会强烈喜欢相似的字符串——或者也许尝试只使用 Jaccard?

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

我写了一些关于 SQL 中模糊字符串搜索的笔记。看:

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

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,返回一个介于0之间的数字1.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

还有....有一个小提琴

确保您的控制台已打开,否则您将看不到任何内容:)

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); } 返回 v; } function string_similarity(str1, str2){ if(str1.length>0 && str2.length>0){ var pairs1 = get_bigrams(str1); var pair2 = get_bigrams(str2); var union =pairs1.length +pairs2.length; 无功命中 = 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); } 返回 0.0 }
2021-03-19 04:33:32
如何在要在多个键中搜索的对象中使用它?
2021-04-06 04:33:32
谢谢,这正是我要找的。如果它是普通的 js 会更好;)
2021-04-12 04:33:32
这有几个问题:1)它低估了字符串开头和结尾的字符。2) 二元组比较是 O(n^2)。3)由于实现,相似度得分可以超过1。这显然没有任何意义。我在下面的回答中解决了所有这些问题。
2021-04-13 04:33:32

这是我用于模糊匹配的简短而紧凑的函数:

function fuzzyMatch(pattern, str) {
  pattern = '.*' + pattern.split('').join('.*') + '.*';
  const re = new RegExp(pattern);
  return re.test(str);
}
不转义正则表达式。如果有人搜索“(”或其他东西,这会搞砸。现在提交编辑!
2021-03-16 04:33:32
尽管在大多数情况下可能不是您想要的,但它确实适合我。
2021-03-19 04:33:32
您可以忽略订单吗?fuzzyMatch('c a', 'a b c')应该回来true
2021-03-22 04:33:32
这里的一个改进是前两行应该从函数中取出,因为RegExp解析需要相当长的时间。我假设使用大量字符串重复调用此方法,即strs for one pattern
2021-03-22 04:33:32
@Explosion 代码编辑可能会被拒绝。如果您没有通过,请提交您自己的答案,也许归功于此答案(您甚至可以通过将您的答案设为“社区维基”来避免代表收益,尽管我认为这里不需要它) .
2021-04-10 04:33:32

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

它在 npm 上可用,具有简单的 API,对我来说还可以。

> fuzzaldrin.filter(['international', 'splint', 'tinder'], 'int');
< ["international", "splint"]
我也成功使用了 Atom 的库,它有一个简单的 API 和闪电般的速度 =)。github.com/cliffordfajardo/cato
2021-03-18 04:33:32