【问题标题】:Javascript fuzzy search that makes sense有意义的 Javascript 模糊搜索
【发布时间】:2014-06-11 20:51:48
【问题描述】:

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

在阅读了一些关于 Levenshtein 距离的内容后,我觉得它与用户在键入时所寻找的内容的近似值很差。对于不知道的人,系统会计算出使两个字符串匹配需要多少次插入删除替换。 p>

在 Levenshtein-Demerau 模型中修复的一个明显缺陷是 blubboob 被认为与 bulb 相同(每个都需要两个替换)。然而,很明显 bulbblubboob 更相似,而且我刚才提到的模型通过允许 换位。

我想在文本完成的上下文中使用它,所以如果我有一个数组 ['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 公制 (Jaro 1995; 1989; Winkler 1999)。在记录链接文献中,使用这种方法的变体获得了良好的结果,该方法基于两个字符串之间公共字符的数量和顺序。

Winkler (1999) 的变体也使用最长公共前缀的长度 P

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

出于文本完成的目的,Monger-Elkan 和 Jaro-Winkler 方法似乎最有意义。 Winkler 对 Jaro 度量的添加有效地加重了单词的开头。 Monger-Elkan 的仿射方面意味着完成一个单词的必要性(它只是一个添加序列)不会太不喜欢它。

结论:

TFIDF 排名在几个基于令牌的距离中表现最好 Monge 和 Elkan 提出的调整仿射间隙编辑距离度量在几个指标中表现最好 字符串编辑距离指标。出奇的好距离 metric 是一种快速启发式方案,由 Jaro 提出,后来由 Winkler 扩展。 这几乎和 Monge-Elkan 方案一样有效,但是 快了一个数量级。 一种将 TFIDF 方法与 Jaro-Winkler 将替换在 TFIDF 与基于 Jaro- 的近似令牌匹配 温克勒方案。平均而言,这种组合的性能略好于 Jaro-Winkler 或 TFIDF,偶尔也会表现得更好。它的性能也接近于几个最佳指标的学习组合 本文考虑。

【问题讨论】:

  • 好问题。我正在寻找类似的东西,但具有相同的字符串比较注意事项。你有没有找到/构建你的字符串比较的 javascript 实现?谢谢。
  • @nicholas 我只是在 github 上分叉了模糊集.js 以解决较小的查询字符串,虽然它不考虑加权字符串操作,但结果对于我预期的字符串补全应用来说非常好。见the repo
  • 谢谢。我会试试看。我还发现了这个字符串比较函数:github.com/zdyn/jaro-winkler-js。似乎也很好用。
  • @michaelday 这没有考虑错别字。在演示中,输入krole 不会返回Final Fantasy V: Krile,尽管我希望这样。它要求查询中的所有字符在结果中以相同的顺序出现,这是相当短视的。似乎获得良好模糊搜索的唯一方法是拥有一个常见拼写错误的数据库。

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


【解决方案1】:

好问题!但我的想法是,与其尝试修改 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 都会强烈偏爱 similar 字符串——或者也许只是尝试使用 Jaccard?

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

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

【讨论】:

    【解决方案2】:

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

    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

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

    【讨论】:

    • 谢谢,这正是我想要的。如果是纯 js 那就更好了 ;)
    • 函数 get_bigrams(string){ var s = string.toLowerCase() var v = s.split(''); for(var i=0; i0 && str2.length>0){ var pairs1 = get_bigrams(str1); var pair2 = get_bigrams(str2); var union = pair1.length + pairs2.length;变量命中 = 0; for(var x=0; x0) return ((2.0 * hits) / union); } 返回 0.0 }
    • 如何在要在多个键中搜索的对象中使用它?
    • 这有几个问题:1)它低估了字符串开头和结尾的字符。 2)二元比较是O(n ^ 2)。 3)由于实现,相似度得分可以超过1。这显然没有任何意义。我在下面的答案中解决了所有这些问题。
    【解决方案3】:
    (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/

    【讨论】:

      【解决方案4】:

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

      它在 npm 上可用,有简单的 API,对我来说工作正常。

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

      【讨论】:

      【解决方案5】:

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

      【讨论】:

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

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

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

      【讨论】:

      • 虽然在大多数情况下可能不是您想要的,但它确实适合我。
      • 你可以忽略这个命令吗? fuzzyMatch('c a', 'a b c') 应该返回 true
      • 这里的一个改进是前两行应该从函数中取出,因为RegExp 解析需要相当长的时间。我假设使用大量字符串重复调用此方法,即 str 用于一个 pattern
      • 不转义正则表达式。如果有人搜索“(”或其他内容,这会搞砸。现在提交编辑!
      • @Explosion 代码编辑可能会被拒绝。如果你的答案没有通过,请提交你自己的答案,也许可以归功于这个答案(你甚至可以通过将你的答案设为“社区维基”来避免代表获得,尽管我认为这里不需要它) .
      【解决方案7】:

      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 条记录,而且我的搜索几乎是即时的;即每次搜索所花费的时间并不明显。

      【讨论】:

      • 这个项目很臃肿 (&gt; 100kb) 并且有大量无人关注的问题和 PR。由于这两个原因,我不会使用它。
      【解决方案8】:

      这里是@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
      }
      

      【讨论】:

        【解决方案9】:

        我修复了 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"))
        
        

        Try it in the TypeScript Playground

        【讨论】:

          【解决方案10】:

          Fuzzy Sort 是一个 JavaScript 库,有助于从大量数据中执行字符串匹配。

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

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

            npm install fuzzysort
            
          2. 做一个引用变量,

            const fuzzysort = require('fuzzysort')
            
          3. 使用 go() 方法查找匹配的字符串

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

          react.js 中的完整演示代码

          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;
          

          更多信息请参考FuzzySort

          【讨论】:

          • 这只是原始库的精确副本:github.com/farzher/fuzzysort
          • 你没有检查我的回购。这里我在 react 中使用了fuzzysort 包。没有可用于将模糊排序集成到反应中的默认解决方案。
          【解决方案11】:

          多年来,我一直喜欢模糊匹配,只是偶然发现了这个话题。这里的谈话比大多数人更深入地讨论杂草,并且看起来涉及实施者。这些年来,我已经用不同的语言编写了其中的几种算法,并希望将一些技巧传递给任何编写 JS 版本的人:

          Monge-Elkan 规则!

          这真是太棒了,将 n-gram 的许多优点与最好的短字符串比较算法(例如 Jaro-Winkler)结合在一起。 (这就是我在 Monge-Elkan 代码中使用的内容。)几年前,我遇到了一篇论文,您可以在网上找到一份 PDF,名为 用于近似文本字符串比较的广义 Mongue-Elkan 方法。要点是,不要使用算术平均值,而是使用二次平均值。我试了一下,它在搜索结果方面取得了显着的改进,涵盖了各种文本。

          N-Grams 规则!

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

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

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

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

          'and'
          'nde'
          'der'
          

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

          【讨论】:

            【解决方案12】:

            这可以通过使用正则表达式来实现。

            例子:

              const fuzzySearch = (list, searchValue) => {
                let buf = ".*" + searchValue.replace(/(.)/g, "$1.*").toLowerCase();
                var reg = new RegExp(buf);
                let newList = list.filter(function (e) {
                  return reg.test(e.title.toLowerCase());
                });
                return newList;
              };
            

            工作示例: https://codesandbox.io/s/jovial-fermat-cilh1?file=/src/App.js:28894-29167

            【讨论】:

            • 如前所述,如果您输入括号,您的示例将中断:)
            • 这真的不符合要求。
            猜你喜欢
            • 1970-01-01
            • 2013-02-18
            • 1970-01-01
            • 1970-01-01
            • 2020-03-15
            • 2014-08-20
            • 2011-10-28
            • 2016-06-12
            • 2017-06-09
            相关资源
            最近更新 更多