How to implement sublime text like fuzzy search?

前端 未结 7 1964
小蘑菇
小蘑菇 2020-12-24 12:41

How can i implement a sublime-like fuzzy search on select2?

Example, typing \"sta jav sub\" would match \"Stackoverflow javascript sublime like\"

相关标签:
7条回答
  • 2020-12-24 13:08

    had difficulties with new select2, here what worked

     $("#foo").select2({
       matcher: matcher
     });
    
    function matcher(params, data) {
      // return all opts if seachbox is empty
      if(!params.term) {
        return data;
      } else if(data) {
        var term = params.term.toUpperCase();
        var option = data.text.toUpperCase();
        var j = -1; // remembers position of last found character
    
        // consider each search character one at a time
        for (var i = 0; i < term.length; i++) {
          var l = term[i];
          if (l == ' ') continue;     // ignore spaces
    
          j = option.indexOf(l, j+1);     // search for character & update position
          if (j == -1) return false;  // if it's not found, exclude this item
        }
        return data; // return option
      }
    }
    
    0 讨论(0)
  • 2020-12-24 13:14

    Here's an alternate matching function. http://jsfiddle.net/trevordixon/pXzj3/4/

    function match(search, text) {
        search = search.toUpperCase();
        text = text.toUpperCase();
    
        var j = -1; // remembers position of last found character
    
        // consider each search character one at a time
        for (var i = 0; i < search.length; i++) {
            var l = search[i];
            if (l == ' ') continue;     // ignore spaces
    
            j = text.indexOf(l, j+1);     // search for character & update position
            if (j == -1) return false;  // if it's not found, exclude this item
        }
        return true;
    }
    

    This one's faster (according to this test in Chrome), which may start to matter if you're filtering a lot of items.

    0 讨论(0)
  • 2020-12-24 13:14

    I wrote some that works very much long Sublime Text's fuzzy match. Achieving this requires a few things.

    First, match all characters from a pattern in sequence. Second, score matches such that certain matched characters are worth more points than other.

    I came up with a few factors to check for. "CamelCase" letters or letters following a separator (space or underscore) are worth a lot of points. Consecutive matches are worth more. Results found near the start are worth more.

    A critically important trick is to find the best matching character. Which is not necessarily the first. Consider fuzzy_match("tk", "The Black Knight"). There are two Ks which could be matched. The second is worth more points because it follows a space.

    JavaScript code is below. There is some nuance which is described in more detail in a blog post. There's also an interactive demo. And full source (includes demo, plus C++ implementation) on GitHub.

    • Blog Post
    • Interactive Demo
    • GitHub

      // Returns [bool, score, formattedStr]
      // bool: true if each character in pattern is found sequentially within str
      // score: integer; higher is better match. Value has no intrinsic meaning. Range varies with pattern. 
      //        Can only compare scores with same search pattern.
      // formattedStr: input str with matched characters marked in <b> tags. Delete if unwanted.
      
      function fuzzy_match(pattern, str) {
          // Score consts
          var adjacency_bonus = 5;                // bonus for adjacent matches
          var separator_bonus = 10;               // bonus if match occurs after a separator
          var camel_bonus = 10;                   // bonus if match is uppercase and prev is lower
          var leading_letter_penalty = -3;        // penalty applied for every letter in str before the first match
          var max_leading_letter_penalty = -9;    // maximum penalty for leading letters
          var unmatched_letter_penalty = -1;      // penalty for every letter that doesn't matter
      
          // Loop variables
          var score = 0;
          var patternIdx = 0;
          var patternLength = pattern.length;
          var strIdx = 0;
          var strLength = str.length;
          var prevMatched = false;
          var prevLower = false;
          var prevSeparator = true;       // true so if first letter match gets separator bonus
      
          // Use "best" matched letter if multiple string letters match the pattern
          var bestLetter = null;
          var bestLower = null;
          var bestLetterIdx = null;
          var bestLetterScore = 0;
      
          var matchedIndices = [];
      
          // Loop over strings
          while (strIdx != strLength) {
              var patternChar = patternIdx != patternLength ? pattern.charAt(patternIdx) : null;
              var strChar = str.charAt(strIdx);
      
              var patternLower = patternChar != null ? patternChar.toLowerCase() : null;
              var strLower = strChar.toLowerCase();
              var strUpper = strChar.toUpperCase();
      
              var nextMatch = patternChar && patternLower == strLower;
              var rematch = bestLetter && bestLower == strLower;
      
              var advanced = nextMatch && bestLetter;
              var patternRepeat = bestLetter && patternChar && bestLower == patternLower;
              if (advanced || patternRepeat) {
                  score += bestLetterScore;
                  matchedIndices.push(bestLetterIdx);
                  bestLetter = null;
                  bestLower = null;
                  bestLetterIdx = null;
                  bestLetterScore = 0;
              }
      
              if (nextMatch || rematch) {
                  var newScore = 0;
      
                  // Apply penalty for each letter before the first pattern match
                  // Note: std::max because penalties are negative values. So max is smallest penalty.
                  if (patternIdx == 0) {
                      var penalty = Math.max(strIdx * leading_letter_penalty, max_leading_letter_penalty);
                      score += penalty;
                  }
      
                  // Apply bonus for consecutive bonuses
                  if (prevMatched)
                      newScore += adjacency_bonus;
      
                  // Apply bonus for matches after a separator
                  if (prevSeparator)
                      newScore += separator_bonus;
      
                  // Apply bonus across camel case boundaries. Includes "clever" isLetter check.
                  if (prevLower && strChar == strUpper && strLower != strUpper)
                      newScore += camel_bonus;
      
                  // Update patter index IFF the next pattern letter was matched
                  if (nextMatch)
                      ++patternIdx;
      
                  // Update best letter in str which may be for a "next" letter or a "rematch"
                  if (newScore >= bestLetterScore) {
      
                      // Apply penalty for now skipped letter
                      if (bestLetter != null)
                          score += unmatched_letter_penalty;
      
                      bestLetter = strChar;
                      bestLower = bestLetter.toLowerCase();
                      bestLetterIdx = strIdx;
                      bestLetterScore = newScore;
                  }
      
                  prevMatched = true;
              }
              else {
                  // Append unmatch characters
                  formattedStr += strChar;
      
                  score += unmatched_letter_penalty;
                  prevMatched = false;
              }
      
              // Includes "clever" isLetter check.
              prevLower = strChar == strLower && strLower != strUpper;
              prevSeparator = strChar == '_' || strChar == ' ';
      
              ++strIdx;
          }
      
          // Apply score for last match
          if (bestLetter) {
              score += bestLetterScore;
              matchedIndices.push(bestLetterIdx);
          }
      
          // Finish out formatted string after last pattern matched
          // Build formated string based on matched letters
          var formattedStr = "";
          var lastIdx = 0;
          for (var i = 0; i < matchedIndices.length; ++i) {
              var idx = matchedIndices[i];
              formattedStr += str.substr(lastIdx, idx - lastIdx) + "<b>" + str.charAt(idx) + "</b>";
              lastIdx = idx + 1;
          }
          formattedStr += str.substr(lastIdx, str.length - lastIdx);
      
          var matched = patternIdx == patternLength;
          return [matched, score, formattedStr];
      }
      
    0 讨论(0)
  • 2020-12-24 13:22

    albertein's answer doesn't match Trevor's version because the original function performs matching on character basis and not on word basis. Here's a simpler one matching on character basis:

    $("#element").select2({
      matcher: function(term, text, opts) {
        var pattern = term.replace(/\s+/g, '').split('').join('.*');
        text.match(new RegExp(pattern, 'i'))
      }
    })
    
    0 讨论(0)
  • 2020-12-24 13:22
    var fuzzysearch = function (querystrings, values) {
        return !querystrings.some(function (q) {
            return !values.some(function (v) {
                return v.toLocaleLowerCase().indexOf(q) !== -1;
            });
        });
    }
    

    Example searching for title and author in book collection http://jsfiddle.net/runjep/r887etnh/2/

    For a 9kb alternative which ranks the search result: http://kiro.me/projects/fuse.html

    You may need a polyfill for the 'some' function https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some

    var books = [{
        id: 1,
        title: 'The Great Gatsby',
        author: 'F. Scott Fitzgerald'
    }, {
        id: 2,
        title: 'The DaVinci Code',
        author: 'Dan Brown'
    }, {
        id: 3,
        title: 'Angels & Demons',
        author: 'Dan Brown'
    }];
    search = function () {
        var queryarray = document.getElementById('inp').value.trim().toLowerCase().split(' ');
        var res = books.filter(function (b) {
            return fs(queryarray, [b.title, b.author]);
        });
        document.getElementById('res').innerHTML = res.map(function (b) {
            return b.title + ' <i> ' + b.author + '</i>';
        }).join('<br/> ');
    }
    fs = function (qs, vals) {
        return !qs.some(function (q) {
            return !vals.some(function (v) {
                return v.toLocaleLowerCase().indexOf(q) !== -1;
            });
        });
    }
    <input id="inp" />
    <button id="but" onclick="search()">Search</button>
    <div id="res"></div>

    0 讨论(0)
  • 2020-12-24 13:27
    function fuzzyMe(term, query) {
      var score = 0;
      var termLength = term.length;
      var queryLength = query.length;
      var highlighting = '';
      var ti = 0;
      // -1 would not work as this would break the calculations of bonus
      // points for subsequent character matches. Something like
      // Number.MIN_VALUE would be more appropriate, but unfortunately
      // Number.MIN_VALUE + 1 equals 1...
      var previousMatchingCharacter = -2;
      for (var qi = 0; qi < queryLength && ti < termLength; qi++) {
        var qc = query.charAt(qi);
        var lowerQc = qc.toLowerCase();
    
        for (; ti < termLength; ti++) {
          var tc = term.charAt(ti);
    
          if (lowerQc === tc.toLowerCase()) {
            score++;
    
            if ((previousMatchingCharacter + 1) === ti) {
              score += 2;
            }
    
            highlighting += "<em>" + tc + "</em>";
            previousMatchingCharacter = ti;
            ti++;
            break;
          } else {
            highlighting += tc;
          }
        }
      }
    
      highlighting += term.substring(ti, term.length);
    
      return {
        score: score,
        term: term,
        query: query,
        highlightedTerm: highlighting
      };
    }
    

    The above takes care of the fuzziness. Then you can just iterate over all your select 2 elements

    $("#element").select2({
      matcher: function(term, text, opt) {
        return fuzzyMe(term, text).highlightedTerm;
      }
    });
    

    Credit for fuzzy code -: https://github.com/bripkens/fuzzy.js

    0 讨论(0)
提交回复
热议问题