How to implement sublime text like fuzzy search?

前端 未结 7 1999
小蘑菇
小蘑菇 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: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  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) + "" + str.charAt(idx) + "";
              lastIdx = idx + 1;
          }
          formattedStr += str.substr(lastIdx, str.length - lastIdx);
      
          var matched = patternIdx == patternLength;
          return [matched, score, formattedStr];
      }
      

提交回复
热议问题