【问题标题】:JavaScript fuzzy searchJavaScript 模糊搜索
【发布时间】:2012-02-09 05:49:50
【问题描述】:

我正在处理这个过滤问题,我有大约 50-100 个列表项。每个项目都有这样的标记:

<li>
  <input type="checkbox" name="services[]" value="service_id" />
  <span class="name">Restaurant in NY</span>
  <span class="filters"><!-- hidden area -->
    <span class="city">@city: new york</span>
    <span class="region">@reg: ny</span>
    <span class="date">@start: 02/05/2012</span>
    <span class="price">@price: 100</span>
  </span>
</li>

我创建这样的标记是因为我最初使用了List.js

所以,您可能已经猜到了,我想要进行这样的搜索:@region: LA @price: 124 等等。问题是我还想显示多个项目,以便选择多个...一个:)

我认为这需要模糊搜索,但问题是我没有找到任何功能。

有什么想法或起点吗?

// 编辑:因为我有相当少量的项目,我想要一个客户端解决方案。

【问题讨论】:

  • 看看这个:code.google.com/p/yeti-witch - 可能会有帮助。
  • 还看看您的要求是否允许您将模糊搜索部分移动到服务器端(使用 AJAX) - 如果是这样,使用 solr 执行此操作将是最简单的事情。除了您可以立即搜索数千个项目之外。 lucene.apache.org/solr
  • Techfoobar:谢谢,但 Yeti 似乎更像 java 而不是 javascript。我不知道如何在我现有的代码上使用它。此外, solr 似乎也是 java 。我需要一些客户端或 PHP。
  • 我看不到你在哪里看到了模糊搜索。你没有提到任何让我认为你需要模糊搜索的东西。还是我错过了什么?模糊搜索使用没有严格定义“边界”的模糊“类别”。我在您的案例中看到的是严格搜索,它将匹配多个属性。
  • @Matjaz 我不太确定如何调用它。只是我的假设:) 感谢您的澄清,希望我能够进行更有针对性的搜索。

标签: javascript fuzzy-search


【解决方案1】:

我在 javascript 中寻找“模糊搜索”,但在这里没有找到解决方案,所以我编写了自己的函数来满足我的需要。

算法非常简单:循环遍历针状字母并检查它们在大海捞针中是否以相同的顺序出现:

String.prototype.fuzzy = function (s) {
    var hay = this.toLowerCase(), i = 0, n = -1, l;
    s = s.toLowerCase();
    for (; l = s[i++] ;) if (!~(n = hay.indexOf(l, n + 1))) return false;
    return true;
};

例如:

('a haystack with a needle').fuzzy('hay sucks');    // false
('a haystack with a needle').fuzzy('sack hand');    // true

【讨论】:

  • 这很简洁(字符串原型操作除外),并且适合某些用例,但更复杂的模糊搜索也需要返回最相关的结果。我想这将基于连续的字符数。
  • 这太棒了。我发现自己几乎在我构建的所有东西中都使用了它。感谢分享@tborychowski!
  • 这太棒了
【解决方案2】:

一年后,List.js 为fuzzy search 找到了一个非常棒的插件。

【讨论】:

  • 它会进行搜索,但它并不是真正的“模糊搜索”。例如在他们的演示“模糊”查询中:“bruwo”没有找到“Guybrush Threepwood”...
  • 您可以指定您希望搜索的模糊程度。您可以输入 bruw 并显示“Guybrush Treepwood”。当你从第二个单词中得到两个字符时,结果被过滤掉了。
【解决方案3】:

另一个(简单)解决方案。不区分大小写并忽略字母顺序。

它会检查搜索词的每个字母。如果原始字符串包含该字母,它将向上计数(如果不包含则向下计数)。根据匹配/字符串长度的比率,它将返回真或假。

String.prototype.fuzzy = function(term, ratio) {
    var string = this.toLowerCase();
    var compare = term.toLowerCase();
    var matches = 0;
    if (string.indexOf(compare) > -1) return true; // covers basic partial matches
    for (var i = 0; i < compare.length; i++) {
        string.indexOf(compare[i]) > -1 ? matches += 1 : matches -=1;
    }
    return (matches/this.length >= ratio || term == "")
};

例子:

("Test").fuzzy("st", 0.5) // returns true
("Test").fuzzy("tes", 0.8) // returns false cause ratio is too low (0.75)
("Test").fuzzy("stet", 1) // returns true
("Test").fuzzy("zzzzzest", 0.75) // returns false cause too many alien characters ("z")
("Test").fuzzy("es", 1) // returns true cause partial match (despite ratio being only 0.5)

【讨论】:

    【解决方案4】:

    我对 list.js 不满意,所以我创建了自己的。这可能不是完全模糊搜索,但我不知道该怎么称呼它。我只是想让它匹配一个查询,而不考虑我在查询中的单词顺序。

    考虑以下场景:

    • 内存中存在文章集合
    • 查询词出现的顺序无关紧要(例如“hello world”与“world hello”)
    • 代码应该易于阅读

    这是一个例子:

    var articles = [{
      title: '2014 Javascript MVC Frameworks Comparison',
      author: 'Guybrush Treepwood'
    }, {
      title: 'Javascript in the year 2014',
      author: 'Herman Toothrot'
    },
    {
      title: 'Javascript in the year 2013',
      author: 'Rapp Scallion'
    }];
    
    var fuzzy = function(items, key) {
      // Returns a method that you can use to create your own reusable fuzzy search.
    
      return function(query) {
        var words  = query.toLowerCase().split(' ');
    
        return items.filter(function(item) {
          var normalizedTerm = item[key].toLowerCase();
    
          return words.every(function(word) {
            return (normalizedTerm.indexOf(word) > -1);
          });
        });
      };
    };
    
    
    var searchByTitle = fuzzy(articles, 'title');
    
    searchByTitle('javascript 2014') // returns the 1st and 2nd items
    

    嗯,我希望这对那里的人有所帮助。

    【讨论】:

      【解决方案5】:

      我有一个小函数,在数组中搜索字符串 (至少对我来说,它比 levenshtein 产生更好的结果):

      function fuzzy(item,arr) {
        function oc(a) {
          var o = {}; for (var i=0; i<a.length; i++) o[a[i]] = ""; return o;
        }
        var test = [];
        for (var n=1; n<=item.length; n++)
          test.push(item.substr(0,n) + "*" + item.substr(n+1,item.length-n));
        var result = [];
        for (var r=0; r<test.length; r++) for (var i=0; i<arr.length; i++) {
          if (arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[0]) != -1)
          if (arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[1]) != -1)
          if (0 < arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[1]) 
                - arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[0] < 2 ) )
          if (!(arr[i] in oc(result)))  result.push(arr[i]);
        }
        return result;
      }
      

      【讨论】:

      • 你为什么一遍又一遍地打电话给arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")?!
      【解决方案6】:

      我自己做了。它使用,更像是一个概念验证,因为它完全没有经过压力测试。

      享受 javascript 模糊搜索/模糊匹配 http://unamatasanatarai.github.io/FuzzyMatch/test/index.html

      【讨论】:

        【解决方案7】:

        此处提供的解决方案返回true/false,并且没有关于哪些部分匹配以及哪些部分不匹配的信息。

        在某些情况下,您可能需要知道它,例如。使您的部分输入在搜索结果中变为粗体

        我已经在 typescript 中创建了自己的解决方案(如果你想使用它 - 我已经在这里发布了 - https://github.com/pie6k/fuzzystring)并在此处演示 https://pie6k.github.io/fuzzystring/

        它的工作原理是:

        fuzzyString('liolor', 'lorem ipsum dolor sit');
        
        // returns
        {
          parts: [
            { content: 'l', type: 'input' },
            { content: 'orem ', type: 'fuzzy' },
            { content: 'i', type: 'input' },
            { content: 'psum d', type: 'fuzzy' },
            { content: 'olor', type: 'input' },
            { content: ' sit', type: 'suggestion' },
          ],
          score: 0.87,
        }
        

        这是完整的实现(Typescript)

        type MatchRoleType = 'input' | 'fuzzy' | 'suggestion';
        
        interface FuzzyMatchPart {
          content: string;
          type: MatchRoleType;
        }
        
        interface FuzzyMatchData {
          parts: FuzzyMatchPart[];
          score: number;
        }
        
        interface FuzzyMatchOptions {
          truncateTooLongInput?: boolean;
          isCaseSesitive?: boolean;
        }
        
        function calculateFuzzyMatchPartsScore(fuzzyMatchParts: FuzzyMatchPart[]) {
          const getRoleLength = (role: MatchRoleType) =>
            fuzzyMatchParts
              .filter((part) => part.type === role)
              .map((part) => part.content)
              .join('').length;
        
          const fullLength = fuzzyMatchParts.map((part) => part.content).join('')
            .length;
          const fuzzyLength = getRoleLength('fuzzy');
          const inputLength = getRoleLength('input');
          const suggestionLength = getRoleLength('suggestion');
        
          return (
            (inputLength + fuzzyLength * 0.7 + suggestionLength * 0.9) / fullLength
          );
        }
        
        function compareLetters(a: string, b: string, isCaseSensitive = false) {
          if (isCaseSensitive) {
            return a === b;
          }
          return a.toLowerCase() === b.toLowerCase();
        }
        
        function fuzzyString(
          input: string,
          stringToBeFound: string,
          { truncateTooLongInput, isCaseSesitive }: FuzzyMatchOptions = {},
        ): FuzzyMatchData | false {
          // make some validation first
        
          // if input is longer than string to find, and we dont truncate it - it's incorrect
          if (input.length > stringToBeFound.length && !truncateTooLongInput) {
            return false;
          }
        
          // if truncate is enabled - do it
          if (input.length > stringToBeFound.length && truncateTooLongInput) {
            input = input.substr(0, stringToBeFound.length);
          }
        
          // if input is the same as string to be found - we dont need to look for fuzzy match - return it as match
          if (input === stringToBeFound) {
            return {
              parts: [{ content: input, type: 'input' }],
              score: 1,
            };
          }
        
          const matchParts: FuzzyMatchPart[] = [];
        
          const remainingInputLetters = input.split('');
        
          // let's create letters buffers
          // it's because we'll perform matching letter by letter, but if we have few letters matching or not matching in the row
          // we want to add them together as part of match
          let ommitedLettersBuffer: string[] = [];
          let matchedLettersBuffer: string[] = [];
        
          // helper functions to clear the buffers and add them to match
          function addOmmitedLettersAsFuzzy() {
            if (ommitedLettersBuffer.length > 0) {
              matchParts.push({
                content: ommitedLettersBuffer.join(''),
                type: 'fuzzy',
              });
              ommitedLettersBuffer = [];
            }
          }
        
          function addMatchedLettersAsInput() {
            if (matchedLettersBuffer.length > 0) {
              matchParts.push({
                content: matchedLettersBuffer.join(''),
                type: 'input',
              });
              matchedLettersBuffer = [];
            }
          }
        
          for (let anotherStringToBeFoundLetter of stringToBeFound) {
            const inputLetterToMatch = remainingInputLetters[0];
        
            // no more input - finish fuzzy matching
            if (!inputLetterToMatch) {
              break;
            }
        
            const isMatching = compareLetters(
              anotherStringToBeFoundLetter,
              inputLetterToMatch,
              isCaseSesitive,
            );
        
            // if input letter doesnt match - we'll go to the next letter to try again
            if (!isMatching) {
              // add this letter to buffer of ommited letters
              ommitedLettersBuffer.push(anotherStringToBeFoundLetter);
              // in case we had something in matched letters buffer - clear it as matching letters run ended
              addMatchedLettersAsInput();
              // go to the next input letter
              continue;
            }
        
            // we have input letter matching!
        
            // remove it from remaining input letters
            remainingInputLetters.shift();
        
            // add it to matched letters buffer
            matchedLettersBuffer.push(anotherStringToBeFoundLetter);
            // in case we had something in ommited letters buffer - add it to the match now
            addOmmitedLettersAsFuzzy();
        
            // if there is no more letters in input - add this matched letter to match too
            if (!remainingInputLetters.length) {
              addMatchedLettersAsInput();
            }
          }
        
          // if we still have letters left in input - means not all input was included in string to find - input was incorrect
          if (remainingInputLetters.length > 0) {
            return false;
          }
        
          // lets get entire matched part (from start to last letter of input)
          const matchedPart = matchParts.map((match) => match.content).join('');
        
          // get remaining part of string to be found
          const suggestionPart = stringToBeFound.replace(matchedPart, '');
        
          // if we have remaining part - add it as suggestion
          if (suggestionPart) {
            matchParts.push({ content: suggestionPart, type: 'suggestion' });
          }
          const score = calculateFuzzyMatchPartsScore(matchParts);
        
          return {
            score,
            parts: matchParts,
          };
        }
        

        【讨论】:

          猜你喜欢
          • 2014-06-11
          • 2020-03-15
          • 2014-08-20
          • 2011-10-28
          • 2016-06-12
          • 2017-06-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多